@@ -234,3 +234,360 @@ fn get_all_keys<K: Clone + Ord + std::hash::Hash, V>(
234
234
235
235
keys_vec
236
236
}
237
+
238
+ #[ cfg( test) ]
239
+ mod tests {
240
+ use super :: * ;
241
+ use crate :: analyzer:: ClippyAnnotation ;
242
+ use std:: collections:: { HashMap , HashSet } ;
243
+ use std:: rc:: Rc ;
244
+
245
+ // Helper function to create a test AnalysisResult
246
+ fn create_analysis_result ( ) -> crate :: analyzer:: AnalysisResult {
247
+ let mut base_counts = HashMap :: new ( ) ;
248
+ let rule1 = Rc :: new ( "clippy::unwrap_used" . to_owned ( ) ) ;
249
+ let rule2 = Rc :: new ( "clippy::match_bool" . to_owned ( ) ) ;
250
+ let rule3 = Rc :: new ( "clippy::unused_imports" . to_owned ( ) ) ;
251
+
252
+ base_counts. insert ( rule1. clone ( ) , 5 ) ;
253
+ base_counts. insert ( rule2. clone ( ) , 3 ) ;
254
+ base_counts. insert ( rule3. clone ( ) , 10 ) ;
255
+
256
+ let mut head_counts = HashMap :: new ( ) ;
257
+ head_counts. insert ( rule1. clone ( ) , 3 ) ;
258
+ head_counts. insert ( rule2. clone ( ) , 4 ) ;
259
+ head_counts. insert ( rule3. clone ( ) , 5 ) ;
260
+
261
+ let mut base_crate_counts = HashMap :: new ( ) ;
262
+ let crate1 = Rc :: new ( "crate1" . to_owned ( ) ) ;
263
+ let crate2 = Rc :: new ( "crate2" . to_owned ( ) ) ;
264
+
265
+ base_crate_counts. insert ( crate1. clone ( ) , 8 ) ;
266
+ base_crate_counts. insert ( crate2. clone ( ) , 10 ) ;
267
+
268
+ let mut head_crate_counts = HashMap :: new ( ) ;
269
+ head_crate_counts. insert ( crate1. clone ( ) , 5 ) ;
270
+ head_crate_counts. insert ( crate2. clone ( ) , 12 ) ;
271
+
272
+ let mut changed_files = HashSet :: new ( ) ;
273
+ changed_files. insert ( "src/file1.rs" . to_owned ( ) ) ;
274
+ changed_files. insert ( "src/file2.rs" . to_owned ( ) ) ;
275
+
276
+ let file1 = Rc :: new ( "src/file1.rs" . to_owned ( ) ) ;
277
+ let file2 = Rc :: new ( "src/file2.rs" . to_owned ( ) ) ;
278
+
279
+ let base_annotations = vec ! [
280
+ ClippyAnnotation {
281
+ file: file1. clone( ) ,
282
+ rule: rule1. clone( ) ,
283
+ } ,
284
+ ClippyAnnotation {
285
+ file: file1. clone( ) ,
286
+ rule: rule1. clone( ) ,
287
+ } ,
288
+ ClippyAnnotation {
289
+ file: file1. clone( ) ,
290
+ rule: rule2. clone( ) ,
291
+ } ,
292
+ ClippyAnnotation {
293
+ file: file2. clone( ) ,
294
+ rule: rule1. clone( ) ,
295
+ } ,
296
+ ClippyAnnotation {
297
+ file: file2. clone( ) ,
298
+ rule: rule3. clone( ) ,
299
+ } ,
300
+ ] ;
301
+
302
+ let head_annotations = vec ! [
303
+ ClippyAnnotation {
304
+ file: file1. clone( ) ,
305
+ rule: rule1. clone( ) ,
306
+ } ,
307
+ ClippyAnnotation {
308
+ file: file1. clone( ) ,
309
+ rule: rule2. clone( ) ,
310
+ } ,
311
+ ClippyAnnotation {
312
+ file: file1. clone( ) ,
313
+ rule: rule2. clone( ) ,
314
+ } ,
315
+ ClippyAnnotation {
316
+ file: file2. clone( ) ,
317
+ rule: rule3. clone( ) ,
318
+ } ,
319
+ ] ;
320
+
321
+ crate :: analyzer:: AnalysisResult {
322
+ base_annotations,
323
+ head_annotations,
324
+ base_counts,
325
+ head_counts,
326
+ changed_files,
327
+ base_crate_counts,
328
+ head_crate_counts,
329
+ }
330
+ }
331
+
332
+ #[ test]
333
+ fn test_generate_report_basic ( ) {
334
+ let analysis = create_analysis_result ( ) ;
335
+ let rules = vec ! [
336
+ "clippy::unwrap_used" . to_owned( ) ,
337
+ "clippy::match_bool" . to_owned( ) ,
338
+ "clippy::unused_imports" . to_owned( ) ,
339
+ ] ;
340
+
341
+ let report = generate_report (
342
+ & analysis,
343
+ & rules,
344
+ "test-owner/test-repo" ,
345
+ "main" ,
346
+ "feature-branch" ,
347
+ ) ;
348
+
349
+ // Verify the report contains expected sections
350
+ assert ! ( report. contains( "## Clippy Allow Annotation Report" ) ) ;
351
+ assert ! ( report. contains( "### Summary by Rule" ) ) ;
352
+ assert ! ( report. contains( "### Annotation Counts by File" ) ) ;
353
+ assert ! ( report. contains( "### Annotation Stats by Crate" ) ) ;
354
+ assert ! ( report. contains( "### About This Report" ) ) ;
355
+
356
+ // Verify the report contains repository and branch information
357
+ assert ! ( report. contains( "test-owner/test-repo" ) ) ;
358
+ assert ! ( report. contains( "main" ) ) ;
359
+ assert ! ( report. contains( "feature-branch" ) ) ;
360
+ }
361
+
362
+ #[ test]
363
+ fn test_generate_report_rule_summary ( ) {
364
+ let analysis = create_analysis_result ( ) ;
365
+ let rules = vec ! [
366
+ "clippy::unwrap_used" . to_owned( ) ,
367
+ "clippy::match_bool" . to_owned( ) ,
368
+ "clippy::unused_imports" . to_owned( ) ,
369
+ ] ;
370
+
371
+ let report = generate_report (
372
+ & analysis,
373
+ & rules,
374
+ "test-owner/test-repo" ,
375
+ "main" ,
376
+ "feature-branch" ,
377
+ ) ;
378
+
379
+ // Verify rule summary contains all rules
380
+ assert ! ( report. contains( "clippy::unwrap_used" ) ) ;
381
+ assert ! ( report. contains( "clippy::match_bool" ) ) ;
382
+ assert ! ( report. contains( "clippy::unused_imports" ) ) ;
383
+
384
+ // Verify counts and changes
385
+ assert ! ( report. contains( "5" ) ) ; // Base count for unwrap_used
386
+ assert ! ( report. contains( "3" ) ) ; // Head count for unwrap_used
387
+ assert ! ( report. contains( "-2" ) ) ; // Change for unwrap_used
388
+
389
+ assert ! ( report. contains( "3" ) ) ; // Base count for match_bool
390
+ assert ! ( report. contains( "4" ) ) ; // Head count for match_bool
391
+ assert ! ( report. contains( "+1" ) ) ; // Change for match_bool
392
+
393
+ assert ! ( report. contains( "10" ) ) ; // Base count for unused_imports
394
+ assert ! ( report. contains( "5" ) ) ; // Head count for unused_imports
395
+ assert ! ( report. contains( "-5" ) ) ; // Change for unused_imports
396
+ }
397
+
398
+ #[ test]
399
+ fn test_generate_report_file_section ( ) {
400
+ let analysis = create_analysis_result ( ) ;
401
+ let rules = vec ! [
402
+ "clippy::unwrap_used" . to_owned( ) ,
403
+ "clippy::match_bool" . to_owned( ) ,
404
+ "clippy::unused_imports" . to_owned( ) ,
405
+ ] ;
406
+
407
+ let report = generate_report (
408
+ & analysis,
409
+ & rules,
410
+ "test-owner/test-repo" ,
411
+ "main" ,
412
+ "feature-branch" ,
413
+ ) ;
414
+
415
+ // Verify file section contains the changed files
416
+ assert ! ( report. contains( "src/file1.rs" ) ) ;
417
+ assert ! ( report. contains( "src/file2.rs" ) ) ;
418
+
419
+ // Verify file counts for file1.rs
420
+ // In the base branch, file1.rs has 3 annotations (2 unwrap_used, 1 match_bool)
421
+ // In the head branch, file1.rs has 3 annotations (1 unwrap_used, 2 match_bool)
422
+ let file1_pattern = r"`src/file1\.rs`\s*\|\s*3\s*\|\s*3\s*\|\s*No change" ;
423
+ assert ! (
424
+ report. contains( "| `src/file1.rs` | 3 | 3 |" )
425
+ || regex:: Regex :: new( file1_pattern) . unwrap( ) . is_match( & report) ,
426
+ "File1 count information not found in report"
427
+ ) ;
428
+
429
+ // Verify file counts for file2.rs
430
+ // In the base branch, file2.rs has 2 annotations (1 unwrap_used, 1 unused_imports)
431
+ // In the head branch, file2.rs has 1 annotation (1 unused_imports)
432
+ let file2_pattern = r"`src/file2\.rs`\s*\|\s*2\s*\|\s*1\s*\|\s*.*-1" ;
433
+ assert ! (
434
+ report. contains( "| `src/file2.rs` | 2 | 1 |" )
435
+ || regex:: Regex :: new( file2_pattern) . unwrap( ) . is_match( & report) ,
436
+ "File2 count information not found in report"
437
+ ) ;
438
+
439
+ // Make sure the change column has the correct indicators
440
+ assert ! (
441
+ report. contains( "No change" ) || report. contains( "(0%)" ) ,
442
+ "No change indicator missing for file1"
443
+ ) ;
444
+ assert ! (
445
+ report. contains( "✅ -1" ) ,
446
+ "Decrease indicator missing for file2"
447
+ ) ;
448
+ }
449
+
450
+ #[ test]
451
+ fn test_generate_report_crate_section ( ) {
452
+ let analysis = create_analysis_result ( ) ;
453
+ let rules = vec ! [
454
+ "clippy::unwrap_used" . to_owned( ) ,
455
+ "clippy::match_bool" . to_owned( ) ,
456
+ "clippy::unused_imports" . to_owned( ) ,
457
+ ] ;
458
+
459
+ let report = generate_report (
460
+ & analysis,
461
+ & rules,
462
+ "test-owner/test-repo" ,
463
+ "main" ,
464
+ "feature-branch" ,
465
+ ) ;
466
+
467
+ // Verify crate section contains the crates
468
+ assert ! ( report. contains( "`crate1`" ) ) ;
469
+ assert ! ( report. contains( "`crate2`" ) ) ;
470
+
471
+ // Verify crate counts
472
+ // Base count for crate1: 8, Head count: 5
473
+ assert ! ( report. contains( "8" ) ) ;
474
+ assert ! ( report. contains( "5" ) ) ;
475
+ assert ! ( report. contains( "-3" ) ) ; // Change
476
+
477
+ // Base count for crate2: 10, Head count: 12
478
+ assert ! ( report. contains( "10" ) ) ;
479
+ assert ! ( report. contains( "12" ) ) ;
480
+ assert ! ( report. contains( "+2" ) ) ; // Change
481
+ }
482
+
483
+ #[ test]
484
+ fn test_generate_report_empty_changed_files ( ) {
485
+ let mut analysis = create_analysis_result ( ) ;
486
+ analysis. changed_files . clear ( ) ;
487
+
488
+ let rules = vec ! [
489
+ "clippy::unwrap_used" . to_owned( ) ,
490
+ "clippy::match_bool" . to_owned( ) ,
491
+ "clippy::unused_imports" . to_owned( ) ,
492
+ ] ;
493
+
494
+ let report = generate_report (
495
+ & analysis,
496
+ & rules,
497
+ "test-owner/test-repo" ,
498
+ "main" ,
499
+ "feature-branch" ,
500
+ ) ;
501
+
502
+ // Verify that the file-level section is not included when there are no changed files
503
+ assert ! ( !report. contains( "### Annotation Counts by File" ) ) ;
504
+
505
+ // But other sections should still be present
506
+ assert ! ( report. contains( "### Summary by Rule" ) ) ;
507
+ assert ! ( report. contains( "### Annotation Stats by Crate" ) ) ;
508
+ }
509
+
510
+ #[ test]
511
+ fn test_generate_report_formatting ( ) {
512
+ let analysis = create_analysis_result ( ) ;
513
+ let rules = vec ! [
514
+ "clippy::unwrap_used" . to_owned( ) ,
515
+ "clippy::match_bool" . to_owned( ) ,
516
+ "clippy::unused_imports" . to_owned( ) ,
517
+ ] ;
518
+
519
+ let report = generate_report (
520
+ & analysis,
521
+ & rules,
522
+ "test-owner/test-repo" ,
523
+ "main" ,
524
+ "feature-branch" ,
525
+ ) ;
526
+
527
+ // Verify positive changes are formatted with ⚠️
528
+ assert ! ( report. contains( "⚠️ +1" ) ) ;
529
+
530
+ // Verify negative changes are formatted with ✅
531
+ assert ! ( report. contains( "✅ -2" ) ) ;
532
+
533
+ // Verify total row exists
534
+ assert ! ( report. contains( "**Total**" ) ) ;
535
+ }
536
+
537
+ #[ test]
538
+ fn test_generate_report_with_origin_prefix ( ) {
539
+ let analysis = create_analysis_result ( ) ;
540
+ let rules = vec ! [
541
+ "clippy::unwrap_used" . to_owned( ) ,
542
+ "clippy::match_bool" . to_owned( ) ,
543
+ "clippy::unused_imports" . to_owned( ) ,
544
+ ] ;
545
+
546
+ // Test with origin/ prefix in base branch
547
+ let report = generate_report (
548
+ & analysis,
549
+ & rules,
550
+ "test-owner/test-repo" ,
551
+ "origin/main" ,
552
+ "feature-branch" ,
553
+ ) ;
554
+
555
+ // Verify the origin/ prefix is removed in the link URL
556
+ assert ! ( report. contains( "https://github.com/test-owner/test-repo/tree/main" ) ) ;
557
+ assert ! ( report. contains( "origin/main" ) ) ;
558
+ }
559
+
560
+ #[ test]
561
+ fn test_generate_report_new_annotations ( ) {
562
+ // Create an analysis where annotations are added in the head branch
563
+ let mut analysis = create_analysis_result ( ) ;
564
+
565
+ // Add a new rule that only appears in the head counts
566
+ let new_rule = Rc :: new ( "clippy::new_rule" . to_owned ( ) ) ;
567
+ analysis. head_counts . insert ( new_rule. clone ( ) , 2 ) ;
568
+
569
+ let rules = vec ! [
570
+ "clippy::unwrap_used" . to_owned( ) ,
571
+ "clippy::match_bool" . to_owned( ) ,
572
+ "clippy::unused_imports" . to_owned( ) ,
573
+ "clippy::new_rule" . to_owned( ) ,
574
+ ] ;
575
+
576
+ let report = generate_report (
577
+ & analysis,
578
+ & rules,
579
+ "test-owner/test-repo" ,
580
+ "main" ,
581
+ "feature-branch" ,
582
+ ) ;
583
+
584
+ // Verify that new rule appears with appropriate formatting
585
+ assert ! ( report. contains( "clippy::new_rule" ) ) ;
586
+ assert ! ( report. contains( "0" ) ) ; // Base count should be 0
587
+ assert ! ( report. contains( "2" ) ) ; // Head count should be 2
588
+ assert ! ( report. contains( "⚠️ +2" ) ) ; // Change should be +2
589
+
590
+ // Also verify N/A for the percentage since base count is 0
591
+ assert ! ( report. contains( "N/A" ) ) ;
592
+ }
593
+ }
0 commit comments