1- use std:: collections:: HashSet ;
1+ use std:: collections:: { HashMap , HashSet } ;
22use std:: fmt;
3- use std:: path:: { Path , PathBuf } ;
3+ use std:: path:: PathBuf ;
4+ use std:: env;
45
56use tokio:: sync:: OnceCell ;
67
@@ -12,6 +13,8 @@ use crate::ui::reporter::VoidReporter;
1213use crate :: util:: file;
1314use crate :: TCLI_HOME ;
1415
16+ /// Dirty macro to define an installer test.
17+ /// See package/install/bepinex.rs for usage example.
1518#[ macro_export]
1619macro_rules! installer_tests {
1720 (
@@ -35,7 +38,7 @@ macro_rules! installer_tests {
3538 $installer_variant,
3639 $init_index,
3740 ) ;
38- let results = test. run_test( ) . await . unwrap ( ) ;
41+ let results = test. run_test( ) . await . expect ( "Installer test failed" ) ;
3942
4043 if results. has_failures {
4144 panic!( "{results}" ) ;
@@ -59,23 +62,94 @@ pub struct TestResult {
5962
6063impl fmt:: Display for TestResult {
6164 fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
62- let total_files = self . only_proj . len ( ) + self . only_asset . len ( ) + self . in_both . len ( ) ;
63- let ratio = match total_files {
64- 0 => 100.0 ,
65- n => ( self . in_both . len ( ) as f64 / n as f64 ) * 100.0 ,
66- } ;
67-
68- write ! ( f, "\n [{:.2}%] Mismatch for packages {}:\n Project dir: {}\n Asset dir: {}\n Only in project:\n \t {}\n Only in asset:\n \t {}\n " ,
69- ratio,
70- self . packages. iter( ) . map( |p| p. to_string( ) ) . collect:: <Vec <_>>( ) . join( ", " ) ,
65+ let matches = self . in_both . len ( ) ;
66+ let mismatches = self . only_proj . len ( ) + self . only_asset . len ( ) ;
67+ let total = matches + mismatches;
68+ let accuracy = if total == 0 { 100.0 } else { ( matches as f64 / total as f64 ) * 100.0 } ;
69+
70+ write ! (
71+ f,
72+ "\n [Accuracy: {:.2}%] Installer comparison for packages {}:\n Project dir: {}\n Asset dir: {}\n Summary: matches={}, mismatches={}, total={}\n " ,
73+ accuracy,
74+ self . packages
75+ . iter( )
76+ . map( |p| p. to_string( ) )
77+ . collect:: <Vec <_>>( )
78+ . join( ", " ) ,
7179 self . project_dir. display( ) ,
7280 self . asset_dir. display( ) ,
73- format_paths( & self . only_proj) ,
74- format_paths( & self . only_asset) ,
75- )
81+ matches,
82+ mismatches,
83+ total,
84+ ) ?;
85+
86+ self . fmt_only_proj ( f) ?;
87+ self . fmt_only_asset ( f) ?;
88+ Ok ( ( ) )
89+ }
90+ }
91+
92+ impl TestResult {
93+ fn fmt_only_proj ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
94+ self . fmt_only_section ( f, " Only in project:" , & self . only_proj , & self . only_asset )
95+ }
96+
97+ fn fmt_only_asset ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
98+ self . fmt_only_section ( f, " Only in asset:" , & self . only_asset , & self . only_proj )
99+ }
100+
101+ fn fmt_only_section (
102+ & self ,
103+ f : & mut fmt:: Formatter < ' _ > ,
104+ header : & str ,
105+ primary : & HashSet < PathBuf > ,
106+ secondary : & HashSet < PathBuf > ,
107+ ) -> fmt:: Result {
108+ if primary. is_empty ( ) {
109+ return Ok ( ( ) ) ;
110+ }
111+
112+ writeln ! ( f, "{}" , header) ?;
113+
114+ let secondary_by_name: HashMap < String , Vec < PathBuf > > = secondary
115+ . iter ( )
116+ . filter_map ( |path| path. file_name ( ) . map ( |name| ( name. to_string_lossy ( ) . to_string ( ) , path. clone ( ) ) ) )
117+ . fold ( HashMap :: new ( ) , |mut acc, ( name, path) | {
118+ acc. entry ( name) . or_insert_with ( Vec :: new) . push ( path) ;
119+ acc
120+ } ) ;
121+
122+ let mut primary_paths: Vec < _ > = primary
123+ . iter ( )
124+ . filter_map ( |p| {
125+ p. file_name ( ) . map ( |name| ( name. to_string_lossy ( ) , p) )
126+ } )
127+ . filter ( |( name, _) | {
128+ !matches ! ( name. as_ref( ) , "README.md" | "icon.png" | "manifest.json" )
129+ } )
130+ . collect ( ) ;
131+ primary_paths. sort_by ( |a, b| a. 1 . cmp ( b. 1 ) ) ;
132+
133+ for ( name, primary_path) in primary_paths {
134+ writeln ! ( f, " • {}" , primary_path. display( ) ) ?;
135+ let name_str = name. to_string ( ) ;
136+ if let Some ( matching) = secondary_by_name. get ( & name_str) {
137+ writeln ! ( f, " ↳ Matching files:" ) ?;
138+ let mut matches: Vec < _ > = matching. iter ( ) . collect ( ) ;
139+ matches. sort ( ) ;
140+ for m in matches {
141+ writeln ! ( f, " • {}" , m. display( ) ) ?;
142+ }
143+ }
144+ }
145+ Ok ( ( ) )
76146 }
77147}
78148
149+ /// Testificate is a test harness providing an easy way to run regression tests on installers.
150+ ///
151+ /// It sets up a temporary project, installs the specified packages, and compares the resulting
152+ /// file structure against a known-good asset directory.
79153pub struct Testficate {
80154 packages : Vec < PackageReference > ,
81155 community : String ,
@@ -85,11 +159,20 @@ pub struct Testficate {
85159}
86160
87161impl Testficate {
88- pub fn new ( packages : Vec < PackageReference > , community : & str , assets_name : & str , installer_variant : & str , init_index : bool ) -> Self {
89- let asset_dir = Path :: new ( env ! ( "CARGO_WORKSPACE_DIR" ) )
90- . join ( "resources/test/installers" )
91- . join ( installer_variant)
92- . join ( assets_name) ;
162+ pub fn new (
163+ packages : Vec < PackageReference > ,
164+ community : & str ,
165+ assets_name : & str ,
166+ installer_variant : & str ,
167+ init_index : bool ,
168+ ) -> Self {
169+ let asset_dir = match env:: var ( "CARGO_WORKSPACE_DIR" ) {
170+ Ok ( val) => PathBuf :: from ( val) ,
171+ Err ( _) => PathBuf :: from ( "." ) , // fallback to current dir
172+ }
173+ . join ( "resources/test/installers" )
174+ . join ( installer_variant)
175+ . join ( assets_name) ;
93176
94177 Self {
95178 packages,
@@ -105,21 +188,22 @@ impl Testficate {
105188 return ;
106189 }
107190
191+ // For tests we don't need full error handling. Run the check once and
192+ // sync if needed. Use unwrap_or(true) so that an error will trigger a
193+ // sync attempt rather than bubbling complex errors in the test harness.
108194 self . init
109195 . get_or_init ( || async {
110- if PackageIndex :: requires_update ( & TCLI_HOME )
111- . await
112- . unwrap_or_default ( )
113- {
114- PackageIndex :: sync ( & TCLI_HOME ) . await . unwrap ( ) ;
196+ if PackageIndex :: requires_update ( & TCLI_HOME ) . await . unwrap_or ( true ) {
197+ // Best-effort sync for tests; ignore errors rather than panicking.
198+ let _ = PackageIndex :: sync ( & TCLI_HOME ) . await ;
115199 }
116200 } )
117201 . await ;
118202 }
119203
120204 pub async fn run_test ( self ) -> Result < TestResult , Error > {
121205 self . init_index ( ) . await ;
122-
206+
123207 let project_dir = tempfile:: tempdir ( ) ?;
124208 let project = Project :: create_new (
125209 project_dir. path ( ) ,
@@ -144,11 +228,5 @@ impl Testficate {
144228 has_failures,
145229 } )
146230 }
147-
148231}
149232
150- fn format_paths ( paths : & HashSet < PathBuf > ) -> String {
151- let mut v: Vec < _ > = paths. iter ( ) . map ( |p| p. display ( ) . to_string ( ) ) . collect ( ) ;
152- v. sort ( ) ;
153- v. join ( "\n \t " )
154- }
0 commit comments