1use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23
24#[derive(Debug, Clone)]
26pub struct CoverageConfig {
27 pub call_count: bool,
29 pub detailed: bool,
31 pub allow_triggered_updates: bool,
33}
34
35impl Default for CoverageConfig {
36 fn default() -> Self {
37 Self {
38 call_count: true,
39 detailed: true,
40 allow_triggered_updates: false,
41 }
42 }
43}
44
45impl CoverageConfig {
46 #[must_use]
48 pub fn new() -> Self {
49 Self::default()
50 }
51
52 #[must_use]
54 pub const fn with_call_count(mut self, enabled: bool) -> Self {
55 self.call_count = enabled;
56 self
57 }
58
59 #[must_use]
61 pub const fn with_detailed(mut self, enabled: bool) -> Self {
62 self.detailed = enabled;
63 self
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct CoverageRange {
70 pub start_offset: u32,
72 pub end_offset: u32,
74 pub count: u32,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct FunctionCoverage {
81 pub function_name: String,
83 pub ranges: Vec<CoverageRange>,
85 pub is_block_coverage: bool,
87}
88
89impl FunctionCoverage {
90 #[must_use]
92 pub fn was_executed(&self) -> bool {
93 self.ranges.iter().any(|r| r.count > 0)
94 }
95
96 #[must_use]
98 pub fn total_count(&self) -> u32 {
99 self.ranges.iter().map(|r| r.count).sum()
100 }
101
102 #[must_use]
104 pub fn byte_range(&self) -> Option<(u32, u32)> {
105 if self.ranges.is_empty() {
106 return None;
107 }
108 let start = self.ranges.iter().map(|r| r.start_offset).min()?;
109 let end = self.ranges.iter().map(|r| r.end_offset).max()?;
110 Some((start, end))
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ScriptCoverage {
117 pub script_id: String,
119 pub url: String,
121 pub functions: Vec<FunctionCoverage>,
123}
124
125impl ScriptCoverage {
126 #[must_use]
128 pub fn functions_executed(&self) -> usize {
129 self.functions.iter().filter(|f| f.was_executed()).count()
130 }
131
132 #[must_use]
134 pub fn functions_total(&self) -> usize {
135 self.functions.len()
136 }
137
138 #[must_use]
140 pub fn coverage_percent(&self) -> f64 {
141 if self.functions.is_empty() {
142 return 100.0;
143 }
144 (self.functions_executed() as f64 / self.functions_total() as f64) * 100.0
145 }
146
147 #[must_use]
149 pub fn is_wasm(&self) -> bool {
150 std::path::Path::new(&self.url)
151 .extension()
152 .is_some_and(|ext| ext.eq_ignore_ascii_case("wasm"))
153 || self.url.contains("wasm")
154 }
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
159pub struct CoverageReport {
160 pub scripts: Vec<ScriptCoverage>,
162 pub timestamp_ms: u64,
164}
165
166impl CoverageReport {
167 #[must_use]
169 pub fn new() -> Self {
170 Self::default()
171 }
172
173 pub fn add_script(&mut self, script: ScriptCoverage) {
175 self.scripts.push(script);
176 }
177
178 #[must_use]
180 pub fn functions_covered(&self) -> usize {
181 self.scripts.iter().map(|s| s.functions_executed()).sum()
182 }
183
184 #[must_use]
186 pub fn functions_total(&self) -> usize {
187 self.scripts.iter().map(|s| s.functions_total()).sum()
188 }
189
190 #[must_use]
192 pub fn coverage_percent(&self) -> f64 {
193 let total = self.functions_total();
194 if total == 0 {
195 return 100.0;
196 }
197 (self.functions_covered() as f64 / total as f64) * 100.0
198 }
199
200 #[must_use]
202 pub fn wasm_coverage(&self) -> WasmCoverage {
203 let wasm_scripts: Vec<_> = self.scripts.iter().filter(|s| s.is_wasm()).collect();
204
205 let functions_covered = wasm_scripts.iter().map(|s| s.functions_executed()).sum();
206 let functions_total = wasm_scripts.iter().map(|s| s.functions_total()).sum();
207
208 WasmCoverage {
209 functions_covered,
210 functions_total,
211 scripts: wasm_scripts.into_iter().cloned().collect(),
212 }
213 }
214
215 #[must_use]
217 pub fn js_coverage(&self) -> JsCoverage {
218 let js_scripts: Vec<_> = self.scripts.iter().filter(|s| !s.is_wasm()).collect();
219
220 let functions_covered = js_scripts.iter().map(|s| s.functions_executed()).sum();
221 let functions_total = js_scripts.iter().map(|s| s.functions_total()).sum();
222
223 JsCoverage {
224 functions_covered,
225 functions_total,
226 scripts: js_scripts.into_iter().cloned().collect(),
227 }
228 }
229
230 #[must_use]
232 pub fn filter_by_url(&self, pattern: &str) -> Self {
233 Self {
234 scripts: self
235 .scripts
236 .iter()
237 .filter(|s| s.url.contains(pattern))
238 .cloned()
239 .collect(),
240 timestamp_ms: self.timestamp_ms,
241 }
242 }
243
244 #[must_use]
246 pub fn summary(&self) -> String {
247 let mut s = String::new();
248 s.push_str(&format!(
249 "Coverage: {:.1}% ({}/{})\n",
250 self.coverage_percent(),
251 self.functions_covered(),
252 self.functions_total()
253 ));
254
255 for script in &self.scripts {
256 let icon = if script.is_wasm() { "🦀" } else { "📜" };
257 s.push_str(&format!(
258 " {} {} - {:.1}% ({}/{})\n",
259 icon,
260 script.url,
261 script.coverage_percent(),
262 script.functions_executed(),
263 script.functions_total()
264 ));
265 }
266
267 s
268 }
269
270 #[must_use]
272 pub fn uncovered_functions(&self) -> Vec<(&str, &str)> {
273 let mut result = Vec::new();
274 for script in &self.scripts {
275 for func in &script.functions {
276 if !func.was_executed() && !func.function_name.is_empty() {
277 result.push((script.url.as_str(), func.function_name.as_str()));
278 }
279 }
280 }
281 result
282 }
283
284 #[must_use]
286 pub fn covered_functions(&self) -> Vec<CoveredFunction> {
287 let mut result = Vec::new();
288 for script in &self.scripts {
289 for func in &script.functions {
290 if func.was_executed() {
291 result.push(CoveredFunction {
292 script_url: script.url.clone(),
293 function_name: func.function_name.clone(),
294 call_count: func.total_count(),
295 });
296 }
297 }
298 }
299 result
300 }
301}
302
303#[derive(Debug, Clone)]
305pub struct CoveredFunction {
306 pub script_url: String,
308 pub function_name: String,
310 pub call_count: u32,
312}
313
314#[derive(Debug, Clone)]
316pub struct WasmCoverage {
317 pub functions_covered: usize,
319 pub functions_total: usize,
321 pub scripts: Vec<ScriptCoverage>,
323}
324
325impl WasmCoverage {
326 #[must_use]
328 pub fn coverage_percent(&self) -> f64 {
329 if self.functions_total == 0 {
330 return 100.0;
331 }
332 (self.functions_covered as f64 / self.functions_total as f64) * 100.0
333 }
334}
335
336#[derive(Debug, Clone)]
338pub struct JsCoverage {
339 pub functions_covered: usize,
341 pub functions_total: usize,
343 pub scripts: Vec<ScriptCoverage>,
345}
346
347impl JsCoverage {
348 #[must_use]
350 pub fn coverage_percent(&self) -> f64 {
351 if self.functions_total == 0 {
352 return 100.0;
353 }
354 (self.functions_covered as f64 / self.functions_total as f64) * 100.0
355 }
356}
357
358#[derive(Debug, Clone)]
360pub struct SourceMapEntry {
361 pub wasm_offset: u32,
363 pub source_file: String,
365 pub line: u32,
367 pub column: u32,
369}
370
371#[derive(Debug, Clone, Default)]
373pub struct WasmSourceMap {
374 pub entries: Vec<SourceMapEntry>,
376 pub sources: HashMap<String, Vec<String>>,
378}
379
380impl WasmSourceMap {
381 #[must_use]
383 pub fn new() -> Self {
384 Self::default()
385 }
386
387 #[must_use]
389 pub fn lookup(&self, offset: u32) -> Option<&SourceMapEntry> {
390 self.entries
392 .iter()
393 .filter(|e| e.wasm_offset <= offset)
394 .max_by_key(|e| e.wasm_offset)
395 }
396
397 pub fn map_coverage(&self, coverage: &CoverageReport) -> LineCoverage {
399 let mut line_coverage = LineCoverage::new();
400
401 for script in &coverage.scripts {
402 if !script.is_wasm() {
403 continue;
404 }
405
406 for func in &script.functions {
407 for range in &func.ranges {
408 if let Some(start_entry) = self.lookup(range.start_offset) {
410 line_coverage.mark_covered(
411 &start_entry.source_file,
412 start_entry.line,
413 range.count,
414 );
415 }
416 if let Some(end_entry) = self.lookup(range.end_offset) {
417 line_coverage.mark_covered(
418 &end_entry.source_file,
419 end_entry.line,
420 range.count,
421 );
422 }
423 }
424 }
425 }
426
427 line_coverage
428 }
429}
430
431#[derive(Debug, Clone, Default)]
433pub struct LineCoverage {
434 pub files: HashMap<String, HashMap<u32, u32>>,
436}
437
438impl LineCoverage {
439 #[must_use]
441 pub fn new() -> Self {
442 Self::default()
443 }
444
445 pub fn mark_covered(&mut self, file: &str, line: u32, count: u32) {
447 let file_coverage = self.files.entry(file.to_string()).or_default();
448 let current = file_coverage.entry(line).or_insert(0);
449 *current = (*current).saturating_add(count);
450 }
451
452 #[must_use]
454 pub fn is_covered(&self, file: &str, line: u32) -> bool {
455 self.files
456 .get(file)
457 .and_then(|f| f.get(&line))
458 .is_some_and(|&count| count > 0)
459 }
460
461 #[must_use]
463 pub fn get_count(&self, file: &str, line: u32) -> u32 {
464 self.files
465 .get(file)
466 .and_then(|f| f.get(&line))
467 .copied()
468 .unwrap_or(0)
469 }
470
471 #[must_use]
473 pub fn covered_lines(&self, file: &str) -> Vec<u32> {
474 self.files
475 .get(file)
476 .map(|f| f.keys().copied().collect())
477 .unwrap_or_default()
478 }
479}
480
481#[cfg(test)]
486#[allow(clippy::unwrap_used, clippy::expect_used)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_coverage_config_default() {
492 let config = CoverageConfig::default();
493 assert!(config.call_count);
494 assert!(config.detailed);
495 assert!(!config.allow_triggered_updates);
496 }
497
498 #[test]
499 fn test_coverage_config_builder() {
500 let config = CoverageConfig::new()
501 .with_call_count(false)
502 .with_detailed(true);
503 assert!(!config.call_count);
504 assert!(config.detailed);
505 }
506
507 #[test]
508 fn test_coverage_range() {
509 let range = CoverageRange {
510 start_offset: 0,
511 end_offset: 100,
512 count: 5,
513 };
514 assert_eq!(range.count, 5);
515 }
516
517 #[test]
518 fn test_function_coverage_executed() {
519 let func = FunctionCoverage {
520 function_name: "test_fn".to_string(),
521 ranges: vec![CoverageRange {
522 start_offset: 0,
523 end_offset: 50,
524 count: 3,
525 }],
526 is_block_coverage: false,
527 };
528 assert!(func.was_executed());
529 assert_eq!(func.total_count(), 3);
530 }
531
532 #[test]
533 fn test_function_coverage_not_executed() {
534 let func = FunctionCoverage {
535 function_name: "unused_fn".to_string(),
536 ranges: vec![CoverageRange {
537 start_offset: 0,
538 end_offset: 50,
539 count: 0,
540 }],
541 is_block_coverage: false,
542 };
543 assert!(!func.was_executed());
544 assert_eq!(func.total_count(), 0);
545 }
546
547 #[test]
548 fn test_function_byte_range() {
549 let func = FunctionCoverage {
550 function_name: "test".to_string(),
551 ranges: vec![
552 CoverageRange {
553 start_offset: 10,
554 end_offset: 30,
555 count: 1,
556 },
557 CoverageRange {
558 start_offset: 50,
559 end_offset: 100,
560 count: 1,
561 },
562 ],
563 is_block_coverage: false,
564 };
565 assert_eq!(func.byte_range(), Some((10, 100)));
566 }
567
568 #[test]
569 fn test_script_coverage() {
570 let script = ScriptCoverage {
571 script_id: "1".to_string(),
572 url: "http://localhost/test.js".to_string(),
573 functions: vec![
574 FunctionCoverage {
575 function_name: "covered".to_string(),
576 ranges: vec![CoverageRange {
577 start_offset: 0,
578 end_offset: 50,
579 count: 1,
580 }],
581 is_block_coverage: false,
582 },
583 FunctionCoverage {
584 function_name: "uncovered".to_string(),
585 ranges: vec![CoverageRange {
586 start_offset: 50,
587 end_offset: 100,
588 count: 0,
589 }],
590 is_block_coverage: false,
591 },
592 ],
593 };
594 assert_eq!(script.functions_executed(), 1);
595 assert_eq!(script.functions_total(), 2);
596 assert!((script.coverage_percent() - 50.0).abs() < 0.01);
597 assert!(!script.is_wasm());
598 }
599
600 #[test]
601 fn test_script_is_wasm() {
602 let wasm_script = ScriptCoverage {
603 script_id: "1".to_string(),
604 url: "http://localhost/app.wasm".to_string(),
605 functions: vec![],
606 };
607 assert!(wasm_script.is_wasm());
608
609 let js_script = ScriptCoverage {
610 script_id: "2".to_string(),
611 url: "http://localhost/app.js".to_string(),
612 functions: vec![],
613 };
614 assert!(!js_script.is_wasm());
615 }
616
617 #[test]
618 fn test_coverage_report_summary() {
619 let mut report = CoverageReport::new();
620 report.add_script(ScriptCoverage {
621 script_id: "1".to_string(),
622 url: "http://localhost/app.wasm".to_string(),
623 functions: vec![FunctionCoverage {
624 function_name: "main".to_string(),
625 ranges: vec![CoverageRange {
626 start_offset: 0,
627 end_offset: 100,
628 count: 1,
629 }],
630 is_block_coverage: false,
631 }],
632 });
633
634 assert_eq!(report.functions_covered(), 1);
635 assert_eq!(report.functions_total(), 1);
636 assert!((report.coverage_percent() - 100.0).abs() < 0.01);
637 }
638
639 #[test]
640 fn test_coverage_report_wasm_only() {
641 let mut report = CoverageReport::new();
642 report.add_script(ScriptCoverage {
643 script_id: "1".to_string(),
644 url: "http://localhost/app.wasm".to_string(),
645 functions: vec![FunctionCoverage {
646 function_name: "wasm_fn".to_string(),
647 ranges: vec![CoverageRange {
648 start_offset: 0,
649 end_offset: 100,
650 count: 1,
651 }],
652 is_block_coverage: false,
653 }],
654 });
655 report.add_script(ScriptCoverage {
656 script_id: "2".to_string(),
657 url: "http://localhost/app.js".to_string(),
658 functions: vec![FunctionCoverage {
659 function_name: "js_fn".to_string(),
660 ranges: vec![CoverageRange {
661 start_offset: 0,
662 end_offset: 50,
663 count: 1,
664 }],
665 is_block_coverage: false,
666 }],
667 });
668
669 let wasm = report.wasm_coverage();
670 assert_eq!(wasm.functions_covered, 1);
671 assert_eq!(wasm.functions_total, 1);
672
673 let js = report.js_coverage();
674 assert_eq!(js.functions_covered, 1);
675 assert_eq!(js.functions_total, 1);
676 }
677
678 #[test]
679 fn test_coverage_report_filter() {
680 let mut report = CoverageReport::new();
681 report.add_script(ScriptCoverage {
682 script_id: "1".to_string(),
683 url: "http://localhost/myapp.wasm".to_string(),
684 functions: vec![],
685 });
686 report.add_script(ScriptCoverage {
687 script_id: "2".to_string(),
688 url: "http://localhost/vendor.js".to_string(),
689 functions: vec![],
690 });
691
692 let filtered = report.filter_by_url("myapp");
693 assert_eq!(filtered.scripts.len(), 1);
694 assert!(filtered.scripts[0].url.contains("myapp"));
695 }
696
697 #[test]
698 fn test_uncovered_functions() {
699 let mut report = CoverageReport::new();
700 report.add_script(ScriptCoverage {
701 script_id: "1".to_string(),
702 url: "test.js".to_string(),
703 functions: vec![
704 FunctionCoverage {
705 function_name: "covered".to_string(),
706 ranges: vec![CoverageRange {
707 start_offset: 0,
708 end_offset: 50,
709 count: 1,
710 }],
711 is_block_coverage: false,
712 },
713 FunctionCoverage {
714 function_name: "uncovered".to_string(),
715 ranges: vec![CoverageRange {
716 start_offset: 50,
717 end_offset: 100,
718 count: 0,
719 }],
720 is_block_coverage: false,
721 },
722 ],
723 });
724
725 let uncovered = report.uncovered_functions();
726 assert_eq!(uncovered.len(), 1);
727 assert_eq!(uncovered[0].1, "uncovered");
728 }
729
730 #[test]
731 fn test_covered_functions() {
732 let mut report = CoverageReport::new();
733 report.add_script(ScriptCoverage {
734 script_id: "1".to_string(),
735 url: "test.js".to_string(),
736 functions: vec![FunctionCoverage {
737 function_name: "my_fn".to_string(),
738 ranges: vec![CoverageRange {
739 start_offset: 0,
740 end_offset: 50,
741 count: 5,
742 }],
743 is_block_coverage: false,
744 }],
745 });
746
747 let covered = report.covered_functions();
748 assert_eq!(covered.len(), 1);
749 assert_eq!(covered[0].function_name, "my_fn");
750 assert_eq!(covered[0].call_count, 5);
751 }
752
753 #[test]
754 fn test_line_coverage() {
755 let mut lc = LineCoverage::new();
756 lc.mark_covered("src/lib.rs", 10, 1);
757 lc.mark_covered("src/lib.rs", 10, 2);
758 lc.mark_covered("src/lib.rs", 20, 1);
759
760 assert!(lc.is_covered("src/lib.rs", 10));
761 assert!(lc.is_covered("src/lib.rs", 20));
762 assert!(!lc.is_covered("src/lib.rs", 30));
763 assert_eq!(lc.get_count("src/lib.rs", 10), 3);
764 }
765
766 #[test]
767 fn test_wasm_source_map_lookup() {
768 let mut sm = WasmSourceMap::new();
769 sm.entries.push(SourceMapEntry {
770 wasm_offset: 0,
771 source_file: "src/lib.rs".to_string(),
772 line: 1,
773 column: 0,
774 });
775 sm.entries.push(SourceMapEntry {
776 wasm_offset: 100,
777 source_file: "src/lib.rs".to_string(),
778 line: 10,
779 column: 0,
780 });
781
782 let entry = sm.lookup(50).unwrap();
783 assert_eq!(entry.line, 1);
784
785 let entry = sm.lookup(150).unwrap();
786 assert_eq!(entry.line, 10);
787 }
788
789 #[test]
794 fn test_function_byte_range_empty() {
795 let func = FunctionCoverage {
796 function_name: "empty".to_string(),
797 ranges: vec![],
798 is_block_coverage: false,
799 };
800 assert!(func.byte_range().is_none());
801 }
802
803 #[test]
804 fn test_function_byte_range_single_range() {
805 let func = FunctionCoverage {
806 function_name: "single".to_string(),
807 ranges: vec![CoverageRange {
808 start_offset: 10,
809 end_offset: 50,
810 count: 1,
811 }],
812 is_block_coverage: false,
813 };
814 assert_eq!(func.byte_range(), Some((10, 50)));
815 }
816
817 #[test]
818 fn test_script_coverage_percent_empty_functions() {
819 let script = ScriptCoverage {
820 script_id: "1".to_string(),
821 url: "test.js".to_string(),
822 functions: vec![],
823 };
824 assert!((script.coverage_percent() - 100.0).abs() < 0.01);
826 }
827
828 #[test]
829 fn test_coverage_report_coverage_percent_zero_total() {
830 let report = CoverageReport::new();
831 assert!((report.coverage_percent() - 100.0).abs() < 0.01);
833 }
834
835 #[test]
836 fn test_wasm_coverage_percent_zero_total() {
837 let wasm = WasmCoverage {
838 functions_covered: 0,
839 functions_total: 0,
840 scripts: vec![],
841 };
842 assert!((wasm.coverage_percent() - 100.0).abs() < 0.01);
844 }
845
846 #[test]
847 fn test_js_coverage_percent_zero_total() {
848 let js = JsCoverage {
849 functions_covered: 0,
850 functions_total: 0,
851 scripts: vec![],
852 };
853 assert!((js.coverage_percent() - 100.0).abs() < 0.01);
855 }
856
857 #[test]
858 fn test_wasm_coverage_percent_partial() {
859 let wasm = WasmCoverage {
860 functions_covered: 3,
861 functions_total: 10,
862 scripts: vec![],
863 };
864 assert!((wasm.coverage_percent() - 30.0).abs() < 0.01);
865 }
866
867 #[test]
868 fn test_js_coverage_percent_partial() {
869 let js = JsCoverage {
870 functions_covered: 7,
871 functions_total: 10,
872 scripts: vec![],
873 };
874 assert!((js.coverage_percent() - 70.0).abs() < 0.01);
875 }
876
877 #[test]
878 fn test_line_coverage_covered_lines() {
879 let mut lc = LineCoverage::new();
880 lc.mark_covered("src/lib.rs", 10, 1);
881 lc.mark_covered("src/lib.rs", 20, 2);
882 lc.mark_covered("src/lib.rs", 30, 3);
883
884 let lines = lc.covered_lines("src/lib.rs");
885 assert_eq!(lines.len(), 3);
886 assert!(lines.contains(&10));
887 assert!(lines.contains(&20));
888 assert!(lines.contains(&30));
889 }
890
891 #[test]
892 fn test_line_coverage_covered_lines_nonexistent_file() {
893 let lc = LineCoverage::new();
894 let lines = lc.covered_lines("nonexistent.rs");
895 assert!(lines.is_empty());
896 }
897
898 #[test]
899 fn test_line_coverage_get_count_nonexistent() {
900 let lc = LineCoverage::new();
901 assert_eq!(lc.get_count("src/lib.rs", 10), 0);
902 }
903
904 #[test]
905 fn test_line_coverage_is_covered_false() {
906 let lc = LineCoverage::new();
907 assert!(!lc.is_covered("src/lib.rs", 10));
908 }
909
910 #[test]
911 fn test_coverage_report_summary_format() {
912 let mut report = CoverageReport::new();
913 report.add_script(ScriptCoverage {
914 script_id: "1".to_string(),
915 url: "http://localhost/app.wasm".to_string(),
916 functions: vec![
917 FunctionCoverage {
918 function_name: "covered".to_string(),
919 ranges: vec![CoverageRange {
920 start_offset: 0,
921 end_offset: 50,
922 count: 1,
923 }],
924 is_block_coverage: false,
925 },
926 FunctionCoverage {
927 function_name: "uncovered".to_string(),
928 ranges: vec![CoverageRange {
929 start_offset: 50,
930 end_offset: 100,
931 count: 0,
932 }],
933 is_block_coverage: false,
934 },
935 ],
936 });
937
938 let summary = report.summary();
939 assert!(summary.contains("Coverage:"));
940 assert!(summary.contains("50.0%"));
941 assert!(summary.contains("1/2"));
942 }
943
944 #[test]
945 fn test_wasm_source_map_map_coverage() {
946 let mut sm = WasmSourceMap::new();
947 sm.entries.push(SourceMapEntry {
948 wasm_offset: 0,
949 source_file: "src/lib.rs".to_string(),
950 line: 1,
951 column: 0,
952 });
953 sm.entries.push(SourceMapEntry {
954 wasm_offset: 100,
955 source_file: "src/lib.rs".to_string(),
956 line: 10,
957 column: 0,
958 });
959
960 let mut report = CoverageReport::new();
961 report.add_script(ScriptCoverage {
962 script_id: "1".to_string(),
963 url: "http://localhost/app.wasm".to_string(),
964 functions: vec![FunctionCoverage {
965 function_name: "test_fn".to_string(),
966 ranges: vec![CoverageRange {
967 start_offset: 50,
968 end_offset: 150,
969 count: 3,
970 }],
971 is_block_coverage: false,
972 }],
973 });
974
975 let line_coverage = sm.map_coverage(&report);
976 assert!(line_coverage.is_covered("src/lib.rs", 1));
978 assert!(line_coverage.is_covered("src/lib.rs", 10));
979 }
980
981 #[test]
982 fn test_wasm_source_map_map_coverage_skips_js() {
983 let sm = WasmSourceMap::new();
984
985 let mut report = CoverageReport::new();
986 report.add_script(ScriptCoverage {
987 script_id: "1".to_string(),
988 url: "http://localhost/app.js".to_string(), functions: vec![FunctionCoverage {
990 function_name: "js_fn".to_string(),
991 ranges: vec![CoverageRange {
992 start_offset: 0,
993 end_offset: 100,
994 count: 1,
995 }],
996 is_block_coverage: false,
997 }],
998 });
999
1000 let line_coverage = sm.map_coverage(&report);
1001 assert!(line_coverage.files.is_empty());
1003 }
1004
1005 #[test]
1006 fn test_wasm_source_map_lookup_no_match() {
1007 let sm = WasmSourceMap::new();
1008 assert!(sm.lookup(50).is_none());
1010 }
1011
1012 #[test]
1013 fn test_wasm_source_map_lookup_exact_match() {
1014 let mut sm = WasmSourceMap::new();
1015 sm.entries.push(SourceMapEntry {
1016 wasm_offset: 100,
1017 source_file: "src/lib.rs".to_string(),
1018 line: 10,
1019 column: 5,
1020 });
1021
1022 let entry = sm.lookup(100).unwrap();
1023 assert_eq!(entry.wasm_offset, 100);
1024 assert_eq!(entry.line, 10);
1025 }
1026
1027 #[test]
1028 fn test_script_is_wasm_case_insensitive() {
1029 let wasm_script = ScriptCoverage {
1030 script_id: "1".to_string(),
1031 url: "http://localhost/app.WASM".to_string(),
1032 functions: vec![],
1033 };
1034 assert!(wasm_script.is_wasm());
1035 }
1036
1037 #[test]
1038 fn test_script_is_wasm_url_contains() {
1039 let wasm_script = ScriptCoverage {
1040 script_id: "1".to_string(),
1041 url: "http://localhost/wasm/module".to_string(),
1042 functions: vec![],
1043 };
1044 assert!(wasm_script.is_wasm());
1045 }
1046
1047 #[test]
1048 fn test_coverage_config_new() {
1049 let config = CoverageConfig::new();
1050 assert!(config.call_count);
1051 assert!(config.detailed);
1052 assert!(!config.allow_triggered_updates);
1053 }
1054
1055 #[test]
1056 fn test_coverage_report_timestamp() {
1057 let mut report = CoverageReport::new();
1058 report.timestamp_ms = 1234567890;
1059 assert_eq!(report.timestamp_ms, 1234567890);
1060 }
1061
1062 #[test]
1063 fn test_coverage_report_filter_by_url_no_match() {
1064 let mut report = CoverageReport::new();
1065 report.add_script(ScriptCoverage {
1066 script_id: "1".to_string(),
1067 url: "http://localhost/app.js".to_string(),
1068 functions: vec![],
1069 });
1070
1071 let filtered = report.filter_by_url("nonexistent");
1072 assert!(filtered.scripts.is_empty());
1073 }
1074
1075 #[test]
1076 fn test_uncovered_functions_anonymous() {
1077 let mut report = CoverageReport::new();
1078 report.add_script(ScriptCoverage {
1079 script_id: "1".to_string(),
1080 url: "test.js".to_string(),
1081 functions: vec![
1082 FunctionCoverage {
1083 function_name: String::new(), ranges: vec![CoverageRange {
1085 start_offset: 0,
1086 end_offset: 50,
1087 count: 0, }],
1089 is_block_coverage: false,
1090 },
1091 FunctionCoverage {
1092 function_name: "named".to_string(),
1093 ranges: vec![CoverageRange {
1094 start_offset: 50,
1095 end_offset: 100,
1096 count: 0, }],
1098 is_block_coverage: false,
1099 },
1100 ],
1101 });
1102
1103 let uncovered = report.uncovered_functions();
1104 assert_eq!(uncovered.len(), 1);
1106 assert_eq!(uncovered[0].1, "named");
1107 }
1108
1109 #[test]
1110 fn test_covered_function_struct() {
1111 let func = CoveredFunction {
1112 script_url: "test.js".to_string(),
1113 function_name: "my_func".to_string(),
1114 call_count: 5,
1115 };
1116 assert_eq!(func.script_url, "test.js");
1117 assert_eq!(func.function_name, "my_func");
1118 assert_eq!(func.call_count, 5);
1119 }
1120
1121 #[test]
1122 fn test_source_map_entry_fields() {
1123 let entry = SourceMapEntry {
1124 wasm_offset: 100,
1125 source_file: "src/main.rs".to_string(),
1126 line: 42,
1127 column: 8,
1128 };
1129 assert_eq!(entry.wasm_offset, 100);
1130 assert_eq!(entry.source_file, "src/main.rs");
1131 assert_eq!(entry.line, 42);
1132 assert_eq!(entry.column, 8);
1133 }
1134
1135 #[test]
1136 fn test_line_coverage_mark_covered_saturating() {
1137 let mut lc = LineCoverage::new();
1138 lc.mark_covered("src/lib.rs", 10, u32::MAX);
1140 lc.mark_covered("src/lib.rs", 10, 1);
1141 assert_eq!(lc.get_count("src/lib.rs", 10), u32::MAX);
1142 }
1143
1144 #[test]
1145 fn test_function_coverage_total_count_multiple_ranges() {
1146 let func = FunctionCoverage {
1147 function_name: "multi".to_string(),
1148 ranges: vec![
1149 CoverageRange {
1150 start_offset: 0,
1151 end_offset: 10,
1152 count: 5,
1153 },
1154 CoverageRange {
1155 start_offset: 10,
1156 end_offset: 20,
1157 count: 3,
1158 },
1159 CoverageRange {
1160 start_offset: 20,
1161 end_offset: 30,
1162 count: 2,
1163 },
1164 ],
1165 is_block_coverage: true,
1166 };
1167 assert_eq!(func.total_count(), 10);
1168 assert!(func.is_block_coverage);
1169 }
1170
1171 #[test]
1172 fn test_wasm_source_map_sources_field() {
1173 let mut sm = WasmSourceMap::new();
1174 sm.sources
1175 .insert("src/lib.rs".to_string(), vec!["fn main() {}".to_string()]);
1176
1177 assert!(sm.sources.contains_key("src/lib.rs"));
1178 assert_eq!(sm.sources["src/lib.rs"].len(), 1);
1179 }
1180
1181 #[test]
1182 fn test_coverage_report_serialize_deserialize() {
1183 let mut report = CoverageReport::new();
1184 report.timestamp_ms = 1000;
1185 report.add_script(ScriptCoverage {
1186 script_id: "1".to_string(),
1187 url: "test.js".to_string(),
1188 functions: vec![FunctionCoverage {
1189 function_name: "test".to_string(),
1190 ranges: vec![CoverageRange {
1191 start_offset: 0,
1192 end_offset: 50,
1193 count: 1,
1194 }],
1195 is_block_coverage: false,
1196 }],
1197 });
1198
1199 let json = serde_json::to_string(&report).unwrap();
1200 let deserialized: CoverageReport = serde_json::from_str(&json).unwrap();
1201
1202 assert_eq!(deserialized.timestamp_ms, 1000);
1203 assert_eq!(deserialized.scripts.len(), 1);
1204 assert_eq!(deserialized.functions_covered(), 1);
1205 }
1206
1207 #[test]
1208 fn test_line_coverage_default() {
1209 let lc = LineCoverage::default();
1210 assert!(lc.files.is_empty());
1211 }
1212
1213 #[test]
1214 fn test_wasm_source_map_default() {
1215 let sm = WasmSourceMap::default();
1216 assert!(sm.entries.is_empty());
1217 assert!(sm.sources.is_empty());
1218 }
1219}