lcov2/
lib.rs

1//! A library for generating and writing LCOV files, and converting them to HTML format.
2//!
3//! # Examples
4//!
5//! ## Writing an LCOV file
6//!
7//! This example will write an info file with a single record for a test file with two lines
8//! and a single function. (imagine main() has a lot of empty space!)
9//!
10//! ```rust,ignore
11//! use lcov::{Records, Record};
12//! use std::fs::write;
13//!
14//! let mut records = Records::default();
15//! let test_record = records.get_or_insert_mut(Path::new("/path/to/test.c"));
16//! test_record.add_function_if_not_exists(1, 32, "main");
17//! test_record.increment_function_data("main");
18//! test_record.add_line_if_not_exists(1);
19//! test_record.add_line_if_not_exists(32);
20//! test_record.increment_line(1);
21//! test_record.increment_line(32);
22//! write("test.info", records.to_string()).unwrap();
23//! ```
24//!
25//! ## Reading an LCOV file
26//!
27//! This example will read an info file.
28//!
29//! ```rust,ignore
30//! use lcov::Records;
31//! use std::fs::read_to_string;
32//!
33//! let contents = read_to_string("test.info").unwrap();
34//! let records = Records::from_str(&contents).unwrap();
35//! ```
36//!
37//! ## Generating an HTML report
38//!
39//! This example will generate an HTML report from the records in a format that looks
40//! very much like LCOV style (not an exact match).
41//!
42//! ```rust,ignore
43//! use lcov::Records;
44//! use std::fs::{read_to_string, write};
45//!
46//! let contents = read_to_string("test.info").unwrap();
47//! let records = Records::from_str(&contents).unwrap();
48//! write("test.html", records.to_html().unwrap()).unwrap();
49//! ```
50
51use 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// NOTE: Definitions derived from https://github.com/linux-test-project/lcov/blob/d465f73117ac3b66e9f6d172346ae18fcfaf0f69/lib/lcovutil.pm#L6983
75
76#[derive(Debug, Clone, Default, PartialEq, Eq)]
77/// If available, a tracefile begins with the testname
78pub 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)]
102/// For each source file referenced in the .gcda file, there is a section containing
103/// filename and coverage data
104pub 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)]
135/// An optional source code version ID
136///
137/// If present, the version ID is compared before file entries are merged (see lcov
138/// --add-tracefile ), and before the 'source detail' view is generated by genhtml. See
139/// the --version-script callback_script documentation and the sample usage in the lcov
140/// regression test examples.
141pub 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)]
176/// A list of line numbers for each function name found in the source file
177pub struct FunctionRecordEntry {
178    /// Line number of function start
179    pub start_line: usize,
180    /// Line number of function end
181    pub end_line: Option<usize>,
182    /// Function name
183    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        // Entries are ordered by start line
204        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)]
253/// A list of execution counts for each instrumented function
254pub struct FunctionDataRecordEntry {
255    /// The number of times the function was hit
256    pub hits: usize,
257    /// The name of the function
258    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)]
304/// The number of functions found
305pub 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)]
332/// The number of functions hit
333pub 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)]
360/// Branch coverage information
361///
362/// `block_id` and `branch` uniquely identify the particular edge
363pub struct BranchDataRecordEntry {
364    /// The line number of the branch
365    pub line_number: usize,
366    /// Whether the branch is an exception or related to exception handling
367    pub exception: bool,
368    /// The block ID of the branch
369    pub block_id: usize,
370    /// The branch number or expression associated with the branch
371    pub branch: Either<usize, String>,
372    /// Whether the branch was taken and how many times
373    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)]
473/// The number of branches found
474pub 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)]
501/// The number of branches hit
502pub 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)]
529/// Execution count for an instrumented line (i.e. a line which resulted in executable
530/// code -- there may not be a record for every source line)
531pub struct LineRecordEntry {
532    /// The line number
533    pub line_number: usize,
534    /// The execution count
535    pub hit_count: usize,
536    /// MD5 hash of line saved as base64, typically not used
537    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)]
590/// Number of instrumented lines
591pub 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)]
618/// Number of lines with a non-zero execution count
619pub 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)]
646/// The end of the record section
647pub 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)]
670/// A single record section, which contains information about a single source file for a
671/// single test
672pub struct Record {
673    /// The name of the test
674    pub test_name: TestNameRecordEntry,
675    /// The source file
676    pub source_file: SourceFileRecordEntry,
677    /// The version of the source file
678    pub version: Option<VersionRecordEntry>,
679    // Functions ordered by start line
680    pub functions: BTreeMap<usize, FunctionRecordEntry>,
681    // Function datas are unique
682    pub function_data: HashMap<String, FunctionDataRecordEntry>,
683    pub functions_found: Option<FunctionsFoundRecordEntry>,
684    pub functions_hit: Option<FunctionsHitRecordEntry>,
685    // Lines are ordered by line and unique
686    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    /// Get the source filename for this record
878    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)]
1001/// A collection of record sections read from a file
1002pub struct Records(HashMap<PathBuf, Record>);
1003
1004impl Records {
1005    /// Get a mutable reference to a record by path
1006    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    /// Get a reference to a record by path
1016    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    /// Output LCOV records to HTML format, like a mini genhtml, in the specified output
1076    /// directory. Sub-directories are created to reflect the directory structure of the
1077    /// source files.
1078    pub fn to_html<P>(&self, output_directory: P) -> Result<()>
1079    where
1080        P: AsRef<Path>,
1081    {
1082        // Build a tree out of the output paths
1083        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            // Calculate the depth of this path from the output directory
1175            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                // This is a file node
1182                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                    // This is the root node
1214                    (PathBuf::from("index.html"), None)
1215                } else {
1216                    // This is a directory node
1217                    (
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        // Output the records to the index.info file
1289        write(
1290            output_directory.as_ref().join("index.info"),
1291            self.to_string(),
1292        )?;
1293
1294        // NOTE: Left for easy debugging of the directory graph
1295        // let dot = petgraph::dot::Dot::new(&graph);
1296        // write(
1297        //     output_directory.as_ref().join("graph.dot"),
1298        //     format!("{}", dot),
1299        // )?;
1300
1301        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}