1use either::Either;
52use petgraph::{
53 graph::{DiGraph, NodeIndex},
54 visit::DfsPostOrder,
55 Direction,
56};
57use std::{
58 collections::{btree_map::Entry, BTreeMap, HashMap},
59 fs::{create_dir_all, read, write},
60 iter::repeat,
61 path::{Component, Path, PathBuf},
62 str::FromStr,
63};
64
65pub mod error;
66pub(crate) mod html;
67
68use error::{Error, Result};
69use html::{
70 CurrentView, DirectoryPage, FilePage, FunctionListing, Head, HtmlFunctionInfo, HtmlLineInfo,
71 HtmlSummaryInfo, Listing, Page, Summary,
72};
73
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub struct TestNameRecordEntry(String);
79
80impl std::fmt::Display for TestNameRecordEntry {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "TN:{}", self.0)
83 }
84}
85
86impl FromStr for TestNameRecordEntry {
87 type Err = Error;
88
89 fn from_str(s: &str) -> Result<Self> {
90 Ok(Self(
91 s.trim()
92 .strip_prefix("TN:")
93 .ok_or_else(|| Error::InvalidTestNameRecordEntry {
94 record: s.to_string(),
95 })?
96 .to_string(),
97 ))
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct SourceFileRecordEntry(PathBuf);
105
106impl std::fmt::Display for SourceFileRecordEntry {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "SF:{}", self.0.to_string_lossy())
109 }
110}
111
112impl Default for SourceFileRecordEntry {
113 fn default() -> Self {
114 Self(PathBuf::new())
115 }
116}
117
118impl FromStr for SourceFileRecordEntry {
119 type Err = Error;
120
121 fn from_str(s: &str) -> Result<Self> {
122 Ok(Self(
123 s.trim()
124 .strip_prefix("SF:")
125 .ok_or_else(|| Error::InvalidSourceFileRecordEntry {
126 record: s.to_string(),
127 })?
128 .to_string()
129 .parse()?,
130 ))
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct VersionRecordEntry(usize);
142
143impl Default for VersionRecordEntry {
144 fn default() -> Self {
145 Self(1)
146 }
147}
148
149impl std::fmt::Display for VersionRecordEntry {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 write!(f, "VER:{}", self.0)
152 }
153}
154
155impl FromStr for VersionRecordEntry {
156 type Err = Error;
157
158 fn from_str(s: &str) -> Result<Self> {
159 s.trim()
160 .strip_prefix("VER:")
161 .ok_or_else(|| Error::InvalidVersionRecordEntry {
162 record: s.to_string(),
163 })
164 .and_then(|version| {
165 version
166 .parse()
167 .map_err(|_| Error::InvalidVersionRecordEntry {
168 record: s.to_string(),
169 })
170 .map(Self)
171 })
172 }
173}
174
175#[derive(Debug, Clone, Eq)]
176pub struct FunctionRecordEntry {
178 pub start_line: usize,
180 pub end_line: Option<usize>,
182 pub name: String,
184}
185
186impl std::fmt::Display for FunctionRecordEntry {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 match self.end_line {
189 Some(end_line) => write!(f, "FN:{},{},{}", self.start_line, end_line, self.name),
190 None => write!(f, "FN:{},{}", self.start_line, self.name),
191 }
192 }
193}
194
195impl std::cmp::PartialEq for FunctionRecordEntry {
196 fn eq(&self, other: &Self) -> bool {
197 self.start_line == other.start_line && self.name == other.name
198 }
199}
200
201impl std::cmp::PartialOrd for FunctionRecordEntry {
202 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
203 self.start_line.partial_cmp(&other.start_line)
205 }
206}
207
208impl FromStr for FunctionRecordEntry {
209 type Err = Error;
210
211 fn from_str(s: &str) -> Result<Self> {
212 let mut parts = s
213 .trim()
214 .strip_prefix("FN:")
215 .ok_or_else(|| Error::InvalidFunctionRecordEntry {
216 record: s.to_string(),
217 })?
218 .split(',');
219 let start_line = parts
220 .next()
221 .ok_or_else(|| Error::InvalidFunctionRecordEntry {
222 record: s.to_string(),
223 })?
224 .parse()
225 .map_err(|_| Error::InvalidFunctionRecordEntry {
226 record: s.to_string(),
227 })?;
228 let end_line = parts
229 .next()
230 .map(|end_line| {
231 end_line
232 .parse()
233 .map_err(|_| Error::InvalidFunctionRecordEntry {
234 record: s.to_string(),
235 })
236 })
237 .transpose()?;
238 let name = parts
239 .next()
240 .ok_or_else(|| Error::InvalidFunctionRecordEntry {
241 record: s.to_string(),
242 })?
243 .to_string();
244 Ok(Self {
245 start_line,
246 end_line,
247 name,
248 })
249 }
250}
251
252#[derive(Debug, Clone, Eq)]
253pub struct FunctionDataRecordEntry {
255 pub hits: usize,
257 pub name: String,
259}
260
261impl std::fmt::Display for FunctionDataRecordEntry {
262 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263 write!(f, "FNDA:{},{}", self.hits, self.name)
264 }
265}
266
267impl std::cmp::PartialEq for FunctionDataRecordEntry {
268 fn eq(&self, other: &Self) -> bool {
269 self.name == other.name
270 }
271}
272
273impl FromStr for FunctionDataRecordEntry {
274 type Err = Error;
275
276 fn from_str(s: &str) -> Result<Self> {
277 let mut parts = s
278 .trim()
279 .strip_prefix("FNDA:")
280 .ok_or_else(|| Error::InvalidFunctionDataRecordEntry {
281 record: s.to_string(),
282 })?
283 .split(',');
284 let hits = parts
285 .next()
286 .ok_or_else(|| Error::InvalidFunctionDataRecordEntry {
287 record: s.to_string(),
288 })?
289 .parse()
290 .map_err(|_| Error::InvalidFunctionDataRecordEntry {
291 record: s.to_string(),
292 })?;
293 let name = parts
294 .next()
295 .ok_or_else(|| Error::InvalidFunctionDataRecordEntry {
296 record: s.to_string(),
297 })?
298 .to_string();
299 Ok(Self { hits, name })
300 }
301}
302
303#[derive(Debug, Clone, Default, PartialEq, Eq)]
304pub struct FunctionsFoundRecordEntry(usize);
306
307impl std::fmt::Display for FunctionsFoundRecordEntry {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 write!(f, "FNF:{}", self.0)
310 }
311}
312
313impl FromStr for FunctionsFoundRecordEntry {
314 type Err = Error;
315
316 fn from_str(s: &str) -> Result<Self> {
317 let count = s
318 .trim()
319 .strip_prefix("FNF:")
320 .ok_or_else(|| Error::InvalidFunctionsFoundRecordEntry {
321 record: s.to_string(),
322 })?
323 .parse()
324 .map_err(|_| Error::InvalidFunctionsFoundRecordEntry {
325 record: s.to_string(),
326 })?;
327 Ok(Self(count))
328 }
329}
330
331#[derive(Debug, Clone, Default, PartialEq, Eq)]
332pub struct FunctionsHitRecordEntry(usize);
334
335impl std::fmt::Display for FunctionsHitRecordEntry {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 write!(f, "FNH:{}", self.0)
338 }
339}
340
341impl FromStr for FunctionsHitRecordEntry {
342 type Err = Error;
343
344 fn from_str(s: &str) -> Result<Self> {
345 let count = s
346 .trim()
347 .strip_prefix("FNH:")
348 .ok_or_else(|| Error::InvalidFunctionsHitRecordEntry {
349 record: s.to_string(),
350 })?
351 .parse()
352 .map_err(|_| Error::InvalidFunctionsHitRecordEntry {
353 record: s.to_string(),
354 })?;
355 Ok(Self(count))
356 }
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct BranchDataRecordEntry {
364 pub line_number: usize,
366 pub exception: bool,
368 pub block_id: usize,
370 pub branch: Either<usize, String>,
372 pub taken: Option<usize>,
374}
375
376impl std::fmt::Display for BranchDataRecordEntry {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 match &self.branch {
379 Either::Left(branch) => format!(
380 "BRDA:{},{}{},{},{}",
381 self.line_number,
382 if self.exception { "e" } else { "" },
383 self.block_id,
384 branch,
385 self.taken.map(|t| t.to_string()).unwrap_or("-".to_string())
386 ),
387 Either::Right(branch) => format!(
388 "BRDA:{},{}{},{},{}",
389 self.line_number,
390 if self.exception { "e" } else { "" },
391 self.block_id,
392 branch,
393 self.taken.map(|t| t.to_string()).unwrap_or("-".to_string())
394 ),
395 }
396 .fmt(f)
397 }
398}
399
400impl FromStr for BranchDataRecordEntry {
401 type Err = Error;
402
403 fn from_str(s: &str) -> Result<Self> {
404 let mut parts = s
405 .trim()
406 .strip_prefix("BRDA:")
407 .ok_or_else(|| Error::InvalidBranchDataRecordEntry {
408 record: s.to_string(),
409 })?
410 .split(',');
411 let line_number = parts
412 .next()
413 .ok_or_else(|| Error::InvalidBranchDataRecordEntry {
414 record: s.to_string(),
415 })?
416 .parse()
417 .map_err(|_| Error::InvalidBranchDataRecordEntry {
418 record: s.to_string(),
419 })?;
420 let (exception, block_id) = parts
421 .next()
422 .map(|exception_and_block_id| {
423 if let Some(exception_and_block_id) = exception_and_block_id.strip_prefix('e') {
424 exception_and_block_id
425 .parse::<usize>()
426 .map(|block_id| (true, block_id))
427 } else {
428 exception_and_block_id
429 .parse::<usize>()
430 .map(|block_id| (false, block_id))
431 }
432 })
433 .transpose()?
434 .ok_or_else(|| Error::InvalidBranchDataRecordEntry {
435 record: s.to_string(),
436 })?;
437
438 let mut branch_and_taken_parts = parts.collect::<Vec<_>>();
439
440 let taken = branch_and_taken_parts
441 .pop()
442 .and_then(|last| {
443 if last == "-" {
444 None
445 } else {
446 Some(
447 last.parse()
448 .map_err(|_| Error::InvalidBranchDataRecordEntry {
449 record: s.to_string(),
450 }),
451 )
452 }
453 })
454 .transpose()?;
455
456 let branch = branch_and_taken_parts
457 .join(",")
458 .parse::<usize>()
459 .map(Either::Left)
460 .unwrap_or(Either::Right(branch_and_taken_parts.join(",")));
461
462 Ok(Self {
463 line_number,
464 exception,
465 block_id,
466 branch,
467 taken,
468 })
469 }
470}
471
472#[derive(Debug, Clone, Default, PartialEq, Eq)]
473pub struct BranchesFoundRecordEntry(usize);
475
476impl std::fmt::Display for BranchesFoundRecordEntry {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 write!(f, "BRF:{}", self.0)
479 }
480}
481
482impl FromStr for BranchesFoundRecordEntry {
483 type Err = Error;
484
485 fn from_str(s: &str) -> Result<Self> {
486 let count = s
487 .trim()
488 .strip_prefix("BRF:")
489 .ok_or_else(|| Error::InvalidBranchesFoundRecordEntry {
490 record: s.to_string(),
491 })?
492 .parse()
493 .map_err(|_| Error::InvalidBranchesFoundRecordEntry {
494 record: s.to_string(),
495 })?;
496 Ok(Self(count))
497 }
498}
499
500#[derive(Debug, Clone, Default, PartialEq, Eq)]
501pub struct BranchesHitRecordEntry(usize);
503
504impl std::fmt::Display for BranchesHitRecordEntry {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 write!(f, "BRH:{}", self.0)
507 }
508}
509
510impl FromStr for BranchesHitRecordEntry {
511 type Err = Error;
512
513 fn from_str(s: &str) -> Result<Self> {
514 let count = s
515 .trim()
516 .strip_prefix("BRH:")
517 .ok_or_else(|| Error::InvalidBranchesHitRecordEntry {
518 record: s.to_string(),
519 })?
520 .parse()
521 .map_err(|_| Error::InvalidBranchesHitRecordEntry {
522 record: s.to_string(),
523 })?;
524 Ok(Self(count))
525 }
526}
527
528#[derive(Debug, Clone, PartialEq, Eq)]
529pub struct LineRecordEntry {
532 pub line_number: usize,
534 pub hit_count: usize,
536 pub checksum: Option<String>,
538}
539
540impl std::fmt::Display for LineRecordEntry {
541 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542 match &self.checksum {
543 Some(checksum) => {
544 write!(f, "DA:{},{},{}", self.line_number, self.hit_count, checksum)
545 }
546 None => write!(f, "DA:{},{}", self.line_number, self.hit_count),
547 }
548 }
549}
550
551impl FromStr for LineRecordEntry {
552 type Err = Error;
553
554 fn from_str(s: &str) -> Result<Self> {
555 let mut parts = s
556 .trim()
557 .strip_prefix("DA:")
558 .ok_or_else(|| Error::InvalidLineDataRecordEntry {
559 record: s.to_string(),
560 })?
561 .split(',');
562 let line_number = parts
563 .next()
564 .ok_or_else(|| Error::InvalidLineDataRecordEntry {
565 record: s.to_string(),
566 })?
567 .parse()
568 .map_err(|_| Error::InvalidLineDataRecordEntry {
569 record: s.to_string(),
570 })?;
571 let hit_count = parts
572 .next()
573 .ok_or_else(|| Error::InvalidLineDataRecordEntry {
574 record: s.to_string(),
575 })?
576 .parse()
577 .map_err(|_| Error::InvalidLineDataRecordEntry {
578 record: s.to_string(),
579 })?;
580 let checksum = parts.next().map(|checksum| checksum.to_string());
581 Ok(Self {
582 line_number,
583 hit_count,
584 checksum,
585 })
586 }
587}
588
589#[derive(Debug, Clone, Default, PartialEq, Eq)]
590pub struct LinesFoundRecordEntry(usize);
592
593impl std::fmt::Display for LinesFoundRecordEntry {
594 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595 write!(f, "LF:{}", self.0)
596 }
597}
598
599impl FromStr for LinesFoundRecordEntry {
600 type Err = Error;
601
602 fn from_str(s: &str) -> Result<Self> {
603 let count = s
604 .trim()
605 .strip_prefix("LF:")
606 .ok_or_else(|| Error::InvalidLinesFoundRecordEntry {
607 record: s.to_string(),
608 })?
609 .parse()
610 .map_err(|_| Error::InvalidLinesFoundRecordEntry {
611 record: s.to_string(),
612 })?;
613 Ok(Self(count))
614 }
615}
616
617#[derive(Debug, Clone, Default, PartialEq, Eq)]
618pub struct LinesHitRecordEntry(usize);
620
621impl std::fmt::Display for LinesHitRecordEntry {
622 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623 write!(f, "LH:{}", self.0)
624 }
625}
626
627impl FromStr for LinesHitRecordEntry {
628 type Err = Error;
629
630 fn from_str(s: &str) -> Result<Self> {
631 let count = s
632 .trim()
633 .strip_prefix("LH:")
634 .ok_or_else(|| Error::InvalidLinesHitRecordEntry {
635 record: s.to_string(),
636 })?
637 .parse()
638 .map_err(|_| Error::InvalidLinesHitRecordEntry {
639 record: s.to_string(),
640 })?;
641 Ok(Self(count))
642 }
643}
644
645#[derive(Debug, Clone, Default, PartialEq, Eq)]
646pub struct EndOfRecordEntry;
648
649impl std::fmt::Display for EndOfRecordEntry {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 write!(f, "end_of_record")
652 }
653}
654
655impl FromStr for EndOfRecordEntry {
656 type Err = Error;
657
658 fn from_str(s: &str) -> Result<Self> {
659 if s.trim() == "end_of_record" {
660 Ok(Self)
661 } else {
662 Err(Error::InvalidEndOfRecordEntry {
663 record: s.to_string(),
664 })
665 }
666 }
667}
668
669#[derive(Debug, Clone, Default, PartialEq, Eq)]
670pub struct Record {
673 pub test_name: TestNameRecordEntry,
675 pub source_file: SourceFileRecordEntry,
677 pub version: Option<VersionRecordEntry>,
679 pub functions: BTreeMap<usize, FunctionRecordEntry>,
681 pub function_data: HashMap<String, FunctionDataRecordEntry>,
683 pub functions_found: Option<FunctionsFoundRecordEntry>,
684 pub functions_hit: Option<FunctionsHitRecordEntry>,
685 pub lines: BTreeMap<usize, LineRecordEntry>,
687 pub lines_found: Option<LinesFoundRecordEntry>,
688 pub lines_hit: Option<LinesHitRecordEntry>,
689 pub end_of_record: EndOfRecordEntry,
690}
691
692impl Record {
693 pub fn new<P>(path: P) -> Self
694 where
695 P: AsRef<Path>,
696 {
697 Self {
698 source_file: SourceFileRecordEntry(path.as_ref().to_path_buf()),
699 functions: BTreeMap::new(),
700 function_data: HashMap::new(),
701 lines: BTreeMap::new(),
702 ..Default::default()
703 }
704 }
705
706 pub fn add_function_if_not_exists<S>(
707 &mut self,
708 start_line: usize,
709 end_line: Option<usize>,
710 name: S,
711 ) -> bool
712 where
713 S: AsRef<str>,
714 {
715 match self.functions.entry(start_line) {
716 Entry::Occupied(_) => false,
717 Entry::Vacant(entry) => {
718 entry.insert(FunctionRecordEntry {
719 start_line,
720 end_line,
721 name: name.as_ref().to_string(),
722 });
723
724 if self.functions_found.is_none() {
725 self.functions_found = Some(FunctionsFoundRecordEntry(0));
726 }
727
728 let Some(functions_found) = self.functions_found.as_mut() else {
729 unreachable!("functions_found must be present");
730 };
731
732 functions_found.0 += 1;
733
734 self.function_data
735 .entry(name.as_ref().to_string())
736 .or_insert_with(|| FunctionDataRecordEntry {
737 hits: 0,
738 name: name.as_ref().to_string(),
739 });
740 true
741 }
742 }
743 }
744
745 pub fn increment_function_data<S>(&mut self, name: S)
746 where
747 S: AsRef<str>,
748 {
749 let entry = self
750 .function_data
751 .entry(name.as_ref().to_string())
752 .or_insert_with(|| {
753 if self.functions_found.is_none() {
754 self.functions_found = Some(FunctionsFoundRecordEntry(0));
755 }
756
757 let Some(functions_found) = self.functions_found.as_mut() else {
758 unreachable!("functions_found must be present");
759 };
760
761 functions_found.0 += 1;
762
763 FunctionDataRecordEntry {
764 hits: 0,
765 name: name.as_ref().to_string(),
766 }
767 });
768
769 if entry.hits == 0 {
770 if self.functions_hit.is_none() {
771 self.functions_hit = Some(FunctionsHitRecordEntry(0));
772 }
773
774 let Some(functions_hit) = self.functions_hit.as_mut() else {
775 unreachable!("functions_hit must be present");
776 };
777
778 functions_hit.0 += 1;
779 }
780
781 entry.hits += 1;
782 }
783
784 pub fn add_line_if_not_exists(&mut self, line_number: usize) -> bool {
785 match self.lines.entry(line_number) {
786 Entry::Occupied(_) => false,
787 Entry::Vacant(entry) => {
788 entry.insert(LineRecordEntry {
789 line_number,
790 hit_count: 0,
791 checksum: None,
792 });
793 if self.lines_found.is_none() {
794 self.lines_found = Some(LinesFoundRecordEntry(0));
795 }
796
797 let Some(lines_found) = self.lines_found.as_mut() else {
798 unreachable!("lines_found must be present");
799 };
800
801 lines_found.0 += 1;
802
803 true
804 }
805 }
806 }
807
808 pub fn increment_line(&mut self, line_number: usize) {
809 let entry = self.lines.entry(line_number).or_insert_with(|| {
810 if self.lines_found.is_none() {
811 self.lines_found = Some(LinesFoundRecordEntry(0));
812 }
813
814 let Some(lines_found) = self.lines_found.as_mut() else {
815 unreachable!("lines_found must be present");
816 };
817
818 lines_found.0 += 1;
819
820 LineRecordEntry {
821 line_number,
822 hit_count: 0,
823 checksum: None,
824 }
825 });
826
827 if entry.hit_count == 0 {
828 if self.lines_hit.is_none() {
829 self.lines_hit = Some(LinesHitRecordEntry(0));
830 }
831
832 let Some(lines_hit) = self.lines_hit.as_mut() else {
833 unreachable!("lines_hit must be present");
834 };
835
836 lines_hit.0 += 1
837 }
838
839 entry.hit_count += 1;
840 }
841}
842
843impl std::fmt::Display for Record {
844 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
845 writeln!(f, "{}", self.test_name)?;
846 writeln!(f, "{}", self.source_file)?;
847 if let Some(version) = self.version.as_ref() {
848 writeln!(f, "{}", version)?;
849 }
850 for function in self.functions.values() {
851 writeln!(f, "{}", function)?;
852 }
853 for function_data in self.function_data.values() {
854 writeln!(f, "{}", function_data)?;
855 }
856 if let Some(functions_found) = self.functions_found.as_ref() {
857 writeln!(f, "{}", functions_found)?;
858 }
859 if let Some(functions_hit) = self.functions_hit.as_ref() {
860 writeln!(f, "{}", functions_hit)?;
861 }
862 for line in self.lines.values() {
863 writeln!(f, "{}", line)?;
864 }
865 if let Some(branches_found) = self.lines_found.as_ref() {
866 writeln!(f, "{}", branches_found)?;
867 }
868 if let Some(branches_hit) = self.lines_hit.as_ref() {
869 writeln!(f, "{}", branches_hit)?;
870 }
871 writeln!(f, "{}", self.end_of_record)?;
872 Ok(())
873 }
874}
875
876impl Record {
877 pub fn source_filename(&self) -> String {
879 self.source_file
880 .0
881 .file_name()
882 .map(|fname| fname.to_string_lossy().to_string())
883 .unwrap_or("<unknown>".to_string())
884 }
885
886 pub(crate) fn summary(&self, top_level: PathBuf, parent: Option<PathBuf>) -> HtmlSummaryInfo {
887 HtmlSummaryInfo {
888 is_dir: false,
889 top_level,
890 parent,
891 filename: Some(self.source_filename()),
892 total_lines: self.lines_found.clone().unwrap_or_default().0,
893 hit_lines: self.lines_hit.clone().unwrap_or_default().0,
894 total_functions: self.functions_found.clone().unwrap_or_default().0,
895 hit_functions: self.functions_hit.clone().unwrap_or_default().0,
896 }
897 }
898
899 pub(crate) fn lines(&self) -> Result<Vec<HtmlLineInfo>> {
900 let contents_raw = read(self.source_file.0.as_path())?;
901 let contents = String::from_utf8_lossy(&contents_raw);
902 let lines = contents
903 .lines()
904 .enumerate()
905 .map(|(i, line)| {
906 let hit_count = self.lines.get(&(i + 1)).map(|l| l.hit_count);
907 let leading_spaces = line.chars().take_while(|c| c.is_whitespace()).count();
908 let trimmed = line.trim().to_string();
909 HtmlLineInfo {
910 hit_count,
911 leading_spaces,
912 line: trimmed,
913 }
914 })
915 .collect::<Vec<_>>();
916 Ok(lines)
917 }
918
919 pub(crate) fn functions(&self) -> Vec<HtmlFunctionInfo> {
920 let mut functions = self
921 .functions
922 .values()
923 .map(|f| HtmlFunctionInfo {
924 hit_count: self.function_data.get(&f.name).map(|d| d.hits),
925 name: f.name.as_str().to_string(),
926 })
927 .collect::<Vec<_>>();
928 functions.sort_by(|a, b| a.name.cmp(&b.name));
929 functions
930 }
931}
932
933impl FromStr for Record {
934 type Err = Error;
935
936 fn from_str(s: &str) -> Result<Self> {
937 let input_lines = s.lines().collect::<Vec<_>>();
938 let test_name = input_lines
939 .first()
940 .ok_or_else(|| Error::InvalidRecordEntry {
941 record: s.to_string(),
942 })?
943 .parse()
944 .unwrap_or_default();
945
946 let source_file = input_lines
947 .get(1)
948 .ok_or_else(|| Error::InvalidRecordEntry {
949 record: s.to_string(),
950 })?
951 .parse()
952 .unwrap_or_default();
953
954 let mut version = None;
955 let mut functions = BTreeMap::new();
956 let mut function_data = HashMap::new();
957 let mut functions_found = None;
958 let mut functions_hit = None;
959 let mut lines = BTreeMap::new();
960 let mut lines_found = None;
961 let mut lines_hit = None;
962 let end_of_record = EndOfRecordEntry;
963
964 input_lines.iter().skip(2).for_each(|line| {
965 if let Ok(parsed_version) = line.parse::<VersionRecordEntry>() {
966 version = Some(parsed_version);
967 } else if let Ok(parsed_function) = line.parse::<FunctionRecordEntry>() {
968 functions.insert(parsed_function.start_line, parsed_function);
969 } else if let Ok(parsed_function_data) = line.parse::<FunctionDataRecordEntry>() {
970 function_data.insert(parsed_function_data.name.clone(), parsed_function_data);
971 } else if let Ok(parsed_functions_found) = line.parse::<FunctionsFoundRecordEntry>() {
972 functions_found = Some(parsed_functions_found);
973 } else if let Ok(parsed_functions_hit) = line.parse::<FunctionsHitRecordEntry>() {
974 functions_hit = Some(parsed_functions_hit);
975 } else if let Ok(parsed_line) = line.parse::<LineRecordEntry>() {
976 lines.insert(parsed_line.line_number, parsed_line);
977 } else if let Ok(parsed_lines_found) = line.parse::<LinesFoundRecordEntry>() {
978 lines_found = Some(parsed_lines_found);
979 } else if let Ok(parsed_lines_hit) = line.parse::<LinesHitRecordEntry>() {
980 lines_hit = Some(parsed_lines_hit);
981 }
982 });
983
984 Ok(Self {
985 test_name,
986 source_file,
987 version,
988 functions,
989 function_data,
990 functions_found,
991 functions_hit,
992 lines,
993 lines_found,
994 lines_hit,
995 end_of_record,
996 })
997 }
998}
999
1000#[derive(Debug, Clone, Default, PartialEq, Eq)]
1001pub struct Records(HashMap<PathBuf, Record>);
1003
1004impl Records {
1005 pub fn get_or_insert_mut<P>(&mut self, path: P) -> &mut Record
1007 where
1008 P: AsRef<Path>,
1009 {
1010 self.0
1011 .entry(path.as_ref().to_path_buf())
1012 .or_insert_with(|| Record::new(path))
1013 }
1014
1015 pub fn get(&self, path: &Path) -> Option<&Record> {
1017 self.0.get(path)
1018 }
1019}
1020
1021impl std::fmt::Display for Records {
1022 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1023 for record in self.0.values() {
1024 write!(f, "{}", record)?;
1025 }
1026 Ok(())
1027 }
1028}
1029
1030impl FromStr for Records {
1031 type Err = Error;
1032
1033 fn from_str(s: &str) -> Result<Self> {
1034 let mut records = HashMap::new();
1035
1036 s.split("end_of_record\n")
1037 .filter(|record| !record.is_empty())
1038 .try_for_each(|record| {
1039 let record = record.to_string() + "end_of_record";
1040 let parsed_record = record.parse::<Record>()?;
1041 records.insert(parsed_record.source_file.0.clone(), parsed_record);
1042 Ok::<_, Error>(())
1043 })?;
1044
1045 Ok(Self(records))
1046 }
1047}
1048
1049struct GraphNode {
1050 pub path: PathBuf,
1051 pub summary: Option<HtmlSummaryInfo>,
1052}
1053
1054impl GraphNode {
1055 pub fn new(path: PathBuf, summary: Option<HtmlSummaryInfo>) -> Self {
1056 Self { path, summary }
1057 }
1058}
1059
1060impl std::fmt::Display for GraphNode {
1061 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1062 write!(f, "{}", self.path.to_string_lossy())
1063 }
1064}
1065
1066struct GraphEdge {}
1067
1068impl std::fmt::Display for GraphEdge {
1069 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1070 write!(f, "")
1071 }
1072}
1073
1074impl Records {
1075 pub fn to_html<P>(&self, output_directory: P) -> Result<()>
1079 where
1080 P: AsRef<Path>,
1081 {
1082 let mut graph = DiGraph::<GraphNode, GraphEdge>::new();
1084 let mut node_ids = HashMap::<PathBuf, NodeIndex>::new();
1085
1086 let entries = self
1087 .0
1088 .values()
1089 .map(|record| {
1090 let absolute_source_path = record.source_file.0.canonicalize()?;
1091 let mut output_path = output_directory
1092 .as_ref()
1093 .components()
1094 .chain(
1095 absolute_source_path
1096 .components()
1097 .filter(|c| matches!(c, Component::Normal(_))),
1098 )
1099 .collect::<PathBuf>();
1100 output_path.set_file_name(
1101 output_path
1102 .file_name()
1103 .map(|fname| fname.to_string_lossy().to_string())
1104 .unwrap_or_default()
1105 + ".html",
1106 );
1107 if let std::collections::hash_map::Entry::Vacant(entry) =
1108 node_ids.entry(output_path.clone())
1109 {
1110 entry.insert(graph.add_node(GraphNode::new(output_path.clone(), None)));
1111 }
1112
1113 let mut path = output_path.as_path();
1114 while let Some(parent) = path.parent() {
1115 if let std::collections::hash_map::Entry::Vacant(entry) =
1116 node_ids.entry(parent.to_path_buf())
1117 {
1118 entry.insert(graph.add_node(GraphNode::new(parent.to_path_buf(), None)));
1119 }
1120
1121 if graph
1122 .find_edge(
1123 *node_ids.get(parent).ok_or_else(|| Error::NodeNotFound {
1124 path: parent.to_path_buf(),
1125 })?,
1126 *node_ids.get(path).ok_or_else(|| Error::NodeNotFound {
1127 path: path.to_path_buf(),
1128 })?,
1129 )
1130 .is_none()
1131 {
1132 graph.add_edge(
1133 *node_ids.get(parent).ok_or_else(|| Error::NodeNotFound {
1134 path: parent.to_path_buf(),
1135 })?,
1136 *node_ids.get(path).ok_or_else(|| Error::NodeNotFound {
1137 path: path.to_path_buf(),
1138 })?,
1139 GraphEdge {},
1140 );
1141 }
1142
1143 path = parent;
1144
1145 if !path.is_dir() {
1146 create_dir_all(path)?;
1147 }
1148
1149 if path == output_directory.as_ref() {
1150 break;
1151 }
1152 }
1153
1154 Ok((output_path, record))
1155 })
1156 .collect::<Result<Vec<_>>>()?
1157 .into_iter()
1158 .collect::<HashMap<_, _>>();
1159
1160 let root = node_ids
1161 .get(output_directory.as_ref())
1162 .ok_or_else(|| Error::NodeNotFound {
1163 path: output_directory.as_ref().to_path_buf(),
1164 })?;
1165
1166 let mut traversal = DfsPostOrder::new(&graph, *root);
1167
1168 while let Some(node) = traversal.next(&graph) {
1169 let path = graph
1170 .node_weight(node)
1171 .ok_or_else(|| Error::WeightNotFound { index: node })?
1172 .path
1173 .clone();
1174 if let Some(record) = entries.get(path.as_path()) {
1176 let depth = path
1177 .components()
1178 .count()
1179 .saturating_sub(output_directory.as_ref().components().count())
1180 .saturating_sub(1);
1181 let summary = record.summary(
1183 repeat("..")
1184 .take(depth)
1185 .collect::<PathBuf>()
1186 .join("index.html"),
1187 path.parent().map(|p| p.join("index.html")),
1188 );
1189 graph
1190 .node_weight_mut(node)
1191 .ok_or_else(|| Error::WeightNotFound { index: node })?
1192 .summary = Some(summary.clone());
1193 let lines = record.lines()?;
1194 let functions = record.functions();
1195 let page = Page {
1196 head: Head {},
1197 current_view: CurrentView {
1198 summary: summary.clone(),
1199 },
1200 summary: Summary { summary },
1201 main: FilePage {
1202 listing: Listing { lines },
1203 function_listing: FunctionListing { functions },
1204 },
1205 };
1206 write(&path, page.to_string())?;
1207 } else {
1208 let depth = path
1209 .components()
1210 .count()
1211 .saturating_sub(output_directory.as_ref().components().count());
1212 let (top_level, parent) = if path == output_directory.as_ref() {
1213 (PathBuf::from("index.html"), None)
1215 } else {
1216 (
1218 repeat("..")
1219 .take(depth)
1220 .collect::<PathBuf>()
1221 .join("index.html"),
1222 path.parent().map(|p| p.join("index.html")),
1223 )
1224 };
1225 let (total_lines, hit_lines, total_functions, hit_functions) = graph
1226 .neighbors_directed(node, Direction::Outgoing)
1227 .try_fold(
1228 (0, 0, 0, 0),
1229 |(total_lines, hit_lines, total_functions, hit_functions), neighbor| {
1230 let summary = graph
1231 .node_weight(neighbor)
1232 .ok_or_else(|| Error::WeightNotFound { index: neighbor })?
1233 .summary
1234 .as_ref()
1235 .ok_or_else(|| Error::NoSummaryInfo)?;
1236 Ok::<(usize, usize, usize, usize), Error>((
1237 total_lines + summary.total_lines,
1238 hit_lines + summary.hit_lines,
1239 total_functions + summary.total_functions,
1240 hit_functions + summary.hit_functions,
1241 ))
1242 },
1243 )?;
1244
1245 let summary = HtmlSummaryInfo {
1246 is_dir: true,
1247 top_level,
1248 parent,
1249 filename: path
1250 .file_name()
1251 .map(|fname| fname.to_string_lossy().to_string()),
1252 total_lines,
1253 hit_lines,
1254 total_functions,
1255 hit_functions,
1256 };
1257
1258 let page = Page {
1259 head: Head {},
1260 current_view: CurrentView {
1261 summary: summary.clone(),
1262 },
1263 summary: Summary {
1264 summary: summary.clone(),
1265 },
1266 main: DirectoryPage {
1267 summaries: graph
1268 .neighbors_directed(node, Direction::Outgoing)
1269 .filter_map(|neighbor| {
1270 graph
1271 .node_weight(neighbor)
1272 .ok_or_else(|| Error::WeightNotFound { index: neighbor })
1273 .ok()
1274 .and_then(|weight| weight.summary.as_ref().cloned())
1275 })
1276 .collect(),
1277 },
1278 };
1279 write(path.join("index.html"), page.to_string())?;
1280
1281 graph
1282 .node_weight_mut(node)
1283 .ok_or_else(|| Error::WeightNotFound { index: node })?
1284 .summary = Some(summary);
1285 }
1286 }
1287
1288 write(
1290 output_directory.as_ref().join("index.info"),
1291 self.to_string(),
1292 )?;
1293
1294 Ok(())
1302 }
1303}
1304
1305#[allow(clippy::unwrap_used)]
1306#[cfg(test)]
1307mod test {
1308 use std::path::PathBuf;
1309 use std::str::FromStr;
1310
1311 use super::Records;
1312
1313 #[test]
1314 fn test_records() {
1315 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1316 let mut records = Records::default();
1317 let record_test =
1318 records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test.c"));
1319 record_test.add_function_if_not_exists(4, Some(16), "main");
1320 record_test.increment_function_data("main");
1321 record_test.add_line_if_not_exists(4);
1322 record_test.add_line_if_not_exists(5);
1323 record_test.add_line_if_not_exists(7);
1324 record_test.add_line_if_not_exists(9);
1325 record_test.add_line_if_not_exists(11);
1326 record_test.add_line_if_not_exists(12);
1327 record_test.add_line_if_not_exists(14);
1328 record_test.increment_line(4);
1329 record_test.increment_line(5);
1330 record_test.increment_line(7);
1331 record_test.increment_line(9);
1332 record_test.increment_line(11);
1333 record_test.increment_line(14);
1334 let record_test2 =
1335 records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test2.c"));
1336 record_test2.add_function_if_not_exists(1, Some(3), "x");
1337 record_test2.increment_function_data("x");
1338 record_test2.add_line_if_not_exists(1);
1339 record_test2.add_line_if_not_exists(2);
1340 record_test2.add_line_if_not_exists(3);
1341 record_test2.increment_line(1);
1342 record_test2.increment_line(2);
1343 record_test2.increment_line(3);
1344 }
1345
1346 #[test]
1347 fn test_records_to_html() {
1348 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1349 let mut records = Records::default();
1350
1351 let record_test =
1352 records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test.c"));
1353 record_test.add_function_if_not_exists(4, Some(16), "main");
1354 record_test.increment_function_data("main");
1355 record_test.add_line_if_not_exists(4);
1356 record_test.add_line_if_not_exists(5);
1357 record_test.add_line_if_not_exists(7);
1358 record_test.add_line_if_not_exists(9);
1359 record_test.add_line_if_not_exists(11);
1360 record_test.add_line_if_not_exists(12);
1361 record_test.add_line_if_not_exists(14);
1362 record_test.increment_line(4);
1363 record_test.increment_line(5);
1364 record_test.increment_line(7);
1365 record_test.increment_line(9);
1366 record_test.increment_line(11);
1367 record_test.increment_line(14);
1368
1369 let record_test = records.get_or_insert_mut(
1370 manifest_dir
1371 .join("tests")
1372 .join("rsrc")
1373 .join("subdir1")
1374 .join("test.c"),
1375 );
1376 record_test.add_function_if_not_exists(4, Some(16), "main");
1377 record_test.increment_function_data("main");
1378 record_test.add_line_if_not_exists(4);
1379 record_test.add_line_if_not_exists(5);
1380 record_test.add_line_if_not_exists(7);
1381 record_test.add_line_if_not_exists(9);
1382 record_test.add_line_if_not_exists(11);
1383 record_test.add_line_if_not_exists(12);
1384 record_test.add_line_if_not_exists(14);
1385 record_test.increment_line(4);
1386 record_test.increment_line(5);
1387 record_test.increment_line(7);
1388 record_test.increment_line(9);
1389 record_test.increment_line(11);
1390 record_test.increment_line(14);
1391
1392 let record_test = records.get_or_insert_mut(
1393 manifest_dir
1394 .join("tests")
1395 .join("rsrc")
1396 .join("subdir2")
1397 .join("test-subdir2.c"),
1398 );
1399 record_test.add_function_if_not_exists(4, Some(16), "main");
1400 record_test.increment_function_data("main");
1401 record_test.add_line_if_not_exists(4);
1402 record_test.add_line_if_not_exists(5);
1403 record_test.add_line_if_not_exists(7);
1404 record_test.add_line_if_not_exists(9);
1405 record_test.add_line_if_not_exists(11);
1406 record_test.add_line_if_not_exists(12);
1407 record_test.add_line_if_not_exists(14);
1408 record_test.increment_line(4);
1409 record_test.increment_line(5);
1410 record_test.increment_line(7);
1411 record_test.increment_line(9);
1412 record_test.increment_line(11);
1413 record_test.increment_line(14);
1414
1415 let record_test2 =
1416 records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test2.c"));
1417 record_test2.add_function_if_not_exists(1, Some(3), "x");
1418 record_test2.increment_function_data("x");
1419 record_test2.add_line_if_not_exists(1);
1420 record_test2.add_line_if_not_exists(2);
1421 record_test2.add_line_if_not_exists(3);
1422 record_test2.increment_line(1);
1423 record_test2.increment_line(2);
1424 record_test2.increment_line(3);
1425
1426 records
1427 .to_html(manifest_dir.join("tests").join("rsrc").join("html"))
1428 .unwrap();
1429 }
1430
1431 #[test]
1432 fn test_records_to_lcov() {
1433 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1434 let mut records = Records::default();
1435
1436 let record_test =
1437 records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test.c"));
1438 record_test.add_function_if_not_exists(4, Some(16), "main");
1439 record_test.increment_function_data("main");
1440 record_test.add_line_if_not_exists(4);
1441 record_test.add_line_if_not_exists(5);
1442 record_test.add_line_if_not_exists(7);
1443 record_test.add_line_if_not_exists(9);
1444 record_test.add_line_if_not_exists(11);
1445 record_test.add_line_if_not_exists(12);
1446 record_test.add_line_if_not_exists(14);
1447 record_test.increment_line(4);
1448 record_test.increment_line(5);
1449 record_test.increment_line(7);
1450 record_test.increment_line(9);
1451 record_test.increment_line(11);
1452 record_test.increment_line(14);
1453
1454 let record_test = records.get_or_insert_mut(
1455 manifest_dir
1456 .join("tests")
1457 .join("rsrc")
1458 .join("subdir1")
1459 .join("test.c"),
1460 );
1461 record_test.add_function_if_not_exists(4, Some(16), "main");
1462 record_test.increment_function_data("main");
1463 record_test.add_line_if_not_exists(4);
1464 record_test.add_line_if_not_exists(5);
1465 record_test.add_line_if_not_exists(7);
1466 record_test.add_line_if_not_exists(9);
1467 record_test.add_line_if_not_exists(11);
1468 record_test.add_line_if_not_exists(12);
1469 record_test.add_line_if_not_exists(14);
1470 record_test.increment_line(4);
1471 record_test.increment_line(5);
1472 record_test.increment_line(7);
1473 record_test.increment_line(9);
1474 record_test.increment_line(11);
1475 record_test.increment_line(14);
1476
1477 let record_test = records.get_or_insert_mut(
1478 manifest_dir
1479 .join("tests")
1480 .join("rsrc")
1481 .join("subdir2")
1482 .join("test-subdir2.c"),
1483 );
1484 record_test.add_function_if_not_exists(4, Some(16), "main");
1485 record_test.increment_function_data("main");
1486 record_test.add_line_if_not_exists(4);
1487 record_test.add_line_if_not_exists(5);
1488 record_test.add_line_if_not_exists(7);
1489 record_test.add_line_if_not_exists(9);
1490 record_test.add_line_if_not_exists(11);
1491 record_test.add_line_if_not_exists(12);
1492 record_test.add_line_if_not_exists(14);
1493 record_test.increment_line(4);
1494 record_test.increment_line(5);
1495 record_test.increment_line(7);
1496 record_test.increment_line(9);
1497 record_test.increment_line(11);
1498 record_test.increment_line(14);
1499
1500 let record_test2 =
1501 records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test2.c"));
1502 record_test2.add_function_if_not_exists(1, Some(3), "x");
1503 record_test2.increment_function_data("x");
1504 record_test2.add_line_if_not_exists(1);
1505 record_test2.add_line_if_not_exists(2);
1506 record_test2.add_line_if_not_exists(3);
1507 record_test2.increment_line(1);
1508 record_test2.increment_line(2);
1509 record_test2.increment_line(3);
1510
1511 let to_lcov = records.to_string();
1512 let from_lcov = Records::from_str(&to_lcov).unwrap();
1513 assert_eq!(records, from_lcov);
1514 }
1515}