11// SPDX-License-Identifier: PMPL-1.0-or-later
22
3- //! Unit tests for language-specific analyzers
3+ //! Unit tests for language-specific analyzers.
4+ //!
5+ //! All tests are panic-free: helpers return `Result`, test functions use
6+ //! `-> Result<(), Box<dyn std::error::Error>>` and propagate errors with `?`.
47
58use panic_attack:: assail;
69use panic_attack:: types:: * ;
710use std:: fs;
811use tempfile:: TempDir ;
912
10- fn create_test_file ( dir : & TempDir , name : & str , content : & str ) -> std:: path:: PathBuf {
13+ /// Write `content` to `dir/name` and return the full path.
14+ ///
15+ /// Returns an error rather than panicking so test failures report the
16+ /// actual I/O reason instead of a bare panic.
17+ fn write_test_file ( dir : & TempDir , name : & str , content : & str ) -> std:: io:: Result < std:: path:: PathBuf > {
1118 let path = dir. path ( ) . join ( name) ;
12- fs:: write ( & path, content) . unwrap ( ) ;
13- path
19+ fs:: write ( & path, content) ? ;
20+ Ok ( path)
1421}
1522
1623#[ test]
17- fn test_rust_analyzer_detects_unsafe ( ) {
18- let dir = TempDir :: new ( ) . unwrap ( ) ;
19- let content = r#"
24+ fn test_rust_analyzer_detects_unsafe ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
25+ let dir = TempDir :: new ( ) ? ;
26+ let file = write_test_file ( & dir , "test.rs" , r#"
2027fn main() {
2128 unsafe {
2229 let x = std::ptr::null::<i32>();
2330 }
2431 unsafe fn dangerous() {}
2532}
26- "# ;
27- let file = create_test_file ( & dir, "test.rs" , content) ;
28- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
33+ "# ) ?;
34+ let report = assail:: analyze ( & file) ?;
2935
3036 assert_eq ! ( report. language, Language :: Rust ) ;
3137 assert ! ( report. statistics. unsafe_blocks >= 2 ) ;
3238 assert ! ( !report. weak_points. is_empty( ) ) ;
39+ Ok ( ( ) )
3340}
3441
3542#[ test]
36- fn test_rust_analyzer_detects_unwraps ( ) {
37- let dir = TempDir :: new ( ) . unwrap ( ) ;
38- let content = r#"
43+ fn test_rust_analyzer_detects_unwraps ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
44+ let dir = TempDir :: new ( ) ? ;
45+ let file = write_test_file ( & dir , "test.rs" , r#"
3946fn main() {
4047 let x = Some(5).unwrap();
4148 let y = Ok::<i32, ()>(10).expect("should work");
4249 let z = vec![1,2,3].get(0).unwrap();
4350}
44- "# ;
45- let file = create_test_file ( & dir, "test.rs" , content) ;
46- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
51+ "# ) ?;
52+ let report = assail:: analyze ( & file) ?;
4753
4854 assert ! ( report. statistics. unwrap_calls >= 3 ) ;
55+ Ok ( ( ) )
4956}
5057
5158#[ test]
52- fn test_rust_analyzer_detects_panics ( ) {
53- let dir = TempDir :: new ( ) . unwrap ( ) ;
54- let content = r#"
59+ fn test_rust_analyzer_detects_panics ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
60+ let dir = TempDir :: new ( ) ? ;
61+ let file = write_test_file ( & dir , "test.rs" , r#"
5562fn main() {
5663 panic!("oh no");
5764 unreachable!("never happens");
5865}
59- "# ;
60- let file = create_test_file ( & dir, "test.rs" , content) ;
61- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
66+ "# ) ?;
67+ let report = assail:: analyze ( & file) ?;
6268
6369 assert ! ( report. statistics. panic_sites >= 2 ) ;
70+ Ok ( ( ) )
6471}
6572
6673#[ test]
67- fn test_c_analyzer_detects_malloc ( ) {
68- let dir = TempDir :: new ( ) . unwrap ( ) ;
69- let content = r#"
74+ fn test_c_analyzer_detects_malloc ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
75+ let dir = TempDir :: new ( ) ? ;
76+ let file = write_test_file ( & dir , "test.c" , r#"
7077#include <stdlib.h>
7178
7279int main() {
7380 int* ptr = malloc(sizeof(int) * 100);
7481 int* ptr2 = calloc(50, sizeof(int));
7582 return 0;
7683}
77- "# ;
78- let file = create_test_file ( & dir, "test.c" , content) ;
79- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
84+ "# ) ?;
85+ let report = assail:: analyze ( & file) ?;
8086
8187 assert_eq ! ( report. language, Language :: C ) ;
8288 assert ! ( report. statistics. allocation_sites >= 2 ) ;
89+ Ok ( ( ) )
8390}
8491
8592#[ test]
86- fn test_c_analyzer_detects_unchecked_malloc ( ) {
87- let dir = TempDir :: new ( ) . unwrap ( ) ;
88- let content = r#"
93+ fn test_c_analyzer_detects_unchecked_malloc ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
94+ let dir = TempDir :: new ( ) ? ;
95+ let file = write_test_file ( & dir , "test.c" , r#"
8996#include <stdlib.h>
9097
9198int main() {
9299 int* ptr = malloc(100);
93100 *ptr = 42; // Unchecked!
94101}
95- "# ;
96- let file = create_test_file ( & dir, "test.c" , content) ;
97- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
102+ "# ) ?;
103+ let report = assail:: analyze ( & file) ?;
98104
99105 let unchecked = report
100106 . weak_points
101107 . iter ( )
102108 . any ( |wp| matches ! ( wp. category, WeakPointCategory :: UncheckedAllocation ) ) ;
103109 assert ! ( unchecked, "Should detect unchecked malloc" ) ;
110+ Ok ( ( ) )
104111}
105112
106113#[ test]
107- fn test_go_analyzer_detects_goroutines ( ) {
108- let dir = TempDir :: new ( ) . unwrap ( ) ;
109- let content = r#"
114+ fn test_go_analyzer_detects_goroutines ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
115+ let dir = TempDir :: new ( ) ? ;
116+ let file = write_test_file ( & dir , "test.go" , r#"
110117package main
111118
112119func main() {
113120 go func() { println("hello") }()
114121 go processData()
115122 go handleRequest()
116123}
117- "# ;
118- let file = create_test_file ( & dir, "test.go" , content) ;
119- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
124+ "# ) ?;
125+ let report = assail:: analyze ( & file) ?;
120126
121127 assert_eq ! ( report. language, Language :: Go ) ;
122128 assert ! ( report. statistics. threading_constructs >= 3 ) ;
129+ Ok ( ( ) )
123130}
124131
125132#[ test]
126- fn test_python_analyzer_detects_unbounded_loop ( ) {
127- let dir = TempDir :: new ( ) . unwrap ( ) ;
128- let content = r#"
133+ fn test_python_analyzer_detects_unbounded_loop ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
134+ let dir = TempDir :: new ( ) ? ;
135+ let file = write_test_file ( & dir , "test.py" , r#"
129136def main():
130137 while True:
131138 process()
132- "# ;
133- let file = create_test_file ( & dir, "test.py" , content) ;
134- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
139+ "# ) ?;
140+ let report = assail:: analyze ( & file) ?;
135141
136142 assert_eq ! ( report. language, Language :: Python ) ;
137143 let unbounded = report
138144 . weak_points
139145 . iter ( )
140146 . any ( |wp| matches ! ( wp. category, WeakPointCategory :: UnboundedLoop ) ) ;
141147 assert ! ( unbounded, "Should detect unbounded loop" ) ;
148+ Ok ( ( ) )
142149}
143150
144151#[ test]
145- fn test_generic_analyzer_basic_patterns ( ) {
146- let dir = TempDir :: new ( ) . unwrap ( ) ;
147- let content = r#"
152+ fn test_generic_analyzer_basic_patterns ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
153+ let dir = TempDir :: new ( ) ? ;
154+ let file = write_test_file ( & dir , "test.unknown" , r#"
148155// Unknown language
149156function main() {
150157 let x = alloc(100);
151158 open("file.txt");
152159 thread.start();
153160}
154- "# ;
155- let file = create_test_file ( & dir, "test.unknown" , content) ;
156-
157- // Generic analyzer should still work, but language will be Unknown
158- // and we just check it doesn't crash
161+ "# ) ?;
162+ // Generic analyzer should still work; we only check it doesn't crash
159163 let _ = assail:: analyze ( & file) ;
164+ Ok ( ( ) )
160165}
161166
162167#[ test]
163- fn test_framework_detection_webserver ( ) {
164- // Framework detection for Rust relies on Cargo.toml, so we create a directory
168+ fn test_framework_detection_webserver ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
169+ // Framework detection for Rust uses Cargo.toml, so we need a directory
165170 // with both a source file and a manifest declaring the dependency.
166- let dir = TempDir :: new ( ) . unwrap ( ) ;
171+ let dir = TempDir :: new ( ) ? ;
167172 let src_dir = dir. path ( ) . join ( "src" ) ;
168- fs:: create_dir_all ( & src_dir) . unwrap ( ) ;
173+ fs:: create_dir_all ( & src_dir) ? ;
169174 fs:: write (
170175 src_dir. join ( "main.rs" ) ,
171176 "use actix_web::{web, App, HttpServer};\n fn main() {}\n " ,
172- )
173- . unwrap ( ) ;
177+ ) ?;
174178 fs:: write (
175179 dir. path ( ) . join ( "Cargo.toml" ) ,
176180 "[package]\n name = \" test\" \n [dependencies]\n actix-web = \" 4\" \n " ,
177- )
178- . unwrap ( ) ;
179- let report = assail:: analyze ( dir. path ( ) ) . expect ( "analysis should succeed" ) ;
181+ ) ?;
182+ let report = assail:: analyze ( dir. path ( ) ) ?;
180183
181184 assert ! (
182185 report. frameworks. contains( & Framework :: WebServer ) ,
183186 "expected WebServer from Cargo.toml actix-web dep, got {:?}" ,
184187 report. frameworks
185188 ) ;
189+ Ok ( ( ) )
186190}
187191
188192#[ test]
189- fn test_framework_detection_database ( ) {
190- let dir = TempDir :: new ( ) . unwrap ( ) ;
193+ fn test_framework_detection_database ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
194+ let dir = TempDir :: new ( ) ? ;
191195 let src_dir = dir. path ( ) . join ( "src" ) ;
192- fs:: create_dir_all ( & src_dir) . unwrap ( ) ;
196+ fs:: create_dir_all ( & src_dir) ? ;
193197 fs:: write (
194198 src_dir. join ( "main.rs" ) ,
195199 "use diesel::prelude::*;\n fn main() {}\n " ,
196- )
197- . unwrap ( ) ;
200+ ) ?;
198201 fs:: write (
199202 dir. path ( ) . join ( "Cargo.toml" ) ,
200203 "[package]\n name = \" test\" \n [dependencies]\n diesel = \" 2\" \n " ,
201- )
202- . unwrap ( ) ;
203- let report = assail:: analyze ( dir. path ( ) ) . expect ( "analysis should succeed" ) ;
204+ ) ?;
205+ let report = assail:: analyze ( dir. path ( ) ) ?;
204206
205207 assert ! (
206208 report. frameworks. contains( & Framework :: Database ) ,
207209 "expected Database from Cargo.toml diesel dep, got {:?}" ,
208210 report. frameworks
209211 ) ;
212+ Ok ( ( ) )
210213}
211214
212215#[ test]
213- fn test_todo_in_string_literal_does_not_trigger_unchecked_error ( ) {
214- // Regression test for 007-lang false positive: parser.rs contained
215- // 155 `.expect("TODO: handle error")` calls. The old detector did
216- // `content.matches("TODO").count()` on raw content, so each string
217- // literal incremented the TODO count. This pattern is common in
218- // stub code and shouldn't be classified as UncheckedError.
219- let dir = TempDir :: new ( ) . unwrap ( ) ;
220- let content = r#"
216+ fn test_todo_in_string_literal_does_not_trigger_unchecked_error ( ) -> Result < ( ) , Box < dyn std:: error:: Error > > {
217+ // Regression: parser.rs had 155 `.expect("TODO: handle error")` calls.
218+ // The old detector counted `content.matches("TODO")` on raw bytes, so each
219+ // string literal incremented the TODO counter. Stub code with `.expect("TODO: …")`
220+ // must not fire UncheckedError.
221+ let dir = TempDir :: new ( ) ?;
222+ let file = write_test_file ( & dir, "stubby.rs" , r#"
221223pub fn parse_stubbed(input: &str) -> String {
222224 let first = input.split(',').next().expect("TODO: handle error");
223225 let second = input.split('.').next().expect("TODO: handle error");
@@ -234,9 +236,8 @@ pub fn parse_stubbed(input: &str) -> String {
234236 first, second, third, fourth, fifth, sixth,
235237 seventh, eighth, ninth, tenth, eleventh)
236238}
237- "# ;
238- let file = create_test_file ( & dir, "stubby.rs" , content) ;
239- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
239+ "# ) ?;
240+ let report = assail:: analyze ( & file) ?;
240241
241242 let unchecked: Vec < _ > = report
242243 . weak_points
@@ -250,13 +251,14 @@ pub fn parse_stubbed(input: &str) -> String {
250251 UncheckedError markers: got {:?}",
251252 unchecked
252253 ) ;
254+ Ok ( ( ) )
253255}
254256
255257#[ test]
256- fn test_real_todo_comments_still_detected ( ) {
257- // Sanity: actual `// TODO` comments in the 11+ threshold still fire.
258- let dir = TempDir :: new ( ) . unwrap ( ) ;
259- let content = r#"
258+ fn test_real_todo_comments_still_detected ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
259+ // Sanity: actual `// TODO` comments above the 11-item threshold must still fire.
260+ let dir = TempDir :: new ( ) ? ;
261+ let file = write_test_file ( & dir , "debt.rs" , r#"
260262// TODO: implement proper error handling
261263// TODO: add tests for edge cases
262264// TODO: optimise hot path
@@ -270,9 +272,8 @@ fn test_real_todo_comments_still_detected() {
270272// XXX: this block needs review
271273// XXX: performance critical but correctness unclear
272274pub fn stub() -> i32 { 42 }
273- "# ;
274- let file = create_test_file ( & dir, "debt.rs" , content) ;
275- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
275+ "# ) ?;
276+ let report = assail:: analyze ( & file) ?;
276277
277278 let unchecked: Vec < _ > = report
278279 . weak_points
@@ -285,19 +286,19 @@ pub fn stub() -> i32 { 42 }
285286 "real // TODO / // FIXME / // HACK / // XXX comments above \
286287 the threshold should still fire the detector"
287288 ) ;
289+ Ok ( ( ) )
288290}
289291
290292#[ test]
291- fn test_per_file_stats_populated ( ) {
292- let dir = TempDir :: new ( ) . unwrap ( ) ;
293- let content = r#"
293+ fn test_per_file_stats_populated ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
294+ let dir = TempDir :: new ( ) ? ;
295+ let file = write_test_file ( & dir , "test.rs" , r#"
294296fn main() {
295297 let x = Some(5).unwrap();
296298 unsafe { std::ptr::null::<i32>() };
297299}
298- "# ;
299- let file = create_test_file ( & dir, "test.rs" , content) ;
300- let report = assail:: analyze ( & file) . expect ( "analysis should succeed" ) ;
300+ "# ) ?;
301+ let report = assail:: analyze ( & file) ?;
301302
302303 assert ! (
303304 !report. file_statistics. is_empty( ) ,
@@ -306,4 +307,5 @@ fn main() {
306307 let stats = & report. file_statistics [ 0 ] ;
307308 assert ! ( stats. file_path. contains( "test.rs" ) ) ;
308309 assert ! ( stats. lines > 0 ) ;
310+ Ok ( ( ) )
309311}
0 commit comments