use either::Either;
use petgraph::{
graph::{DiGraph, NodeIndex},
visit::DfsPostOrder,
Direction,
};
use std::{
collections::{btree_map::Entry, BTreeMap, HashMap},
fs::{create_dir_all, read, write},
iter::repeat,
path::{Component, Path, PathBuf},
str::FromStr,
};
pub mod error;
pub(crate) mod html;
use error::{Error, Result};
use html::{
CurrentView, DirectoryPage, FilePage, FunctionListing, Head, HtmlFunctionInfo, HtmlLineInfo,
HtmlSummaryInfo, Listing, Page, Summary,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TestNameRecordEntry(String);
impl std::fmt::Display for TestNameRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TN:{}", self.0)
}
}
impl FromStr for TestNameRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Ok(Self(
s.trim()
.strip_prefix("TN:")
.ok_or_else(|| Error::InvalidTestNameRecordEntry {
record: s.to_string(),
})?
.to_string(),
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceFileRecordEntry(PathBuf);
impl std::fmt::Display for SourceFileRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SF:{}", self.0.to_string_lossy())
}
}
impl Default for SourceFileRecordEntry {
fn default() -> Self {
Self(PathBuf::new())
}
}
impl FromStr for SourceFileRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Ok(Self(
s.trim()
.strip_prefix("SF:")
.ok_or_else(|| Error::InvalidSourceFileRecordEntry {
record: s.to_string(),
})?
.to_string()
.parse()?,
))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionRecordEntry(usize);
impl Default for VersionRecordEntry {
fn default() -> Self {
Self(1)
}
}
impl std::fmt::Display for VersionRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "VER:{}", self.0)
}
}
impl FromStr for VersionRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
s.trim()
.strip_prefix("VER:")
.ok_or_else(|| Error::InvalidVersionRecordEntry {
record: s.to_string(),
})
.and_then(|version| {
version
.parse()
.map_err(|_| Error::InvalidVersionRecordEntry {
record: s.to_string(),
})
.map(Self)
})
}
}
#[derive(Debug, Clone, Eq)]
pub struct FunctionRecordEntry {
pub start_line: usize,
pub end_line: Option<usize>,
pub name: String,
}
impl std::fmt::Display for FunctionRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.end_line {
Some(end_line) => write!(f, "FN:{},{},{}", self.start_line, end_line, self.name),
None => write!(f, "FN:{},{}", self.start_line, self.name),
}
}
}
impl std::cmp::PartialEq for FunctionRecordEntry {
fn eq(&self, other: &Self) -> bool {
self.start_line == other.start_line && self.name == other.name
}
}
impl std::cmp::PartialOrd for FunctionRecordEntry {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.start_line.partial_cmp(&other.start_line)
}
}
impl FromStr for FunctionRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut parts = s
.trim()
.strip_prefix("FN:")
.ok_or_else(|| Error::InvalidFunctionRecordEntry {
record: s.to_string(),
})?
.split(',');
let start_line = parts
.next()
.ok_or_else(|| Error::InvalidFunctionRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidFunctionRecordEntry {
record: s.to_string(),
})?;
let end_line = parts
.next()
.map(|end_line| {
end_line
.parse()
.map_err(|_| Error::InvalidFunctionRecordEntry {
record: s.to_string(),
})
})
.transpose()?;
let name = parts
.next()
.ok_or_else(|| Error::InvalidFunctionRecordEntry {
record: s.to_string(),
})?
.to_string();
Ok(Self {
start_line,
end_line,
name,
})
}
}
#[derive(Debug, Clone, Eq)]
pub struct FunctionDataRecordEntry {
pub hits: usize,
pub name: String,
}
impl std::fmt::Display for FunctionDataRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FNDA:{},{}", self.hits, self.name)
}
}
impl std::cmp::PartialEq for FunctionDataRecordEntry {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl FromStr for FunctionDataRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut parts = s
.trim()
.strip_prefix("FNDA:")
.ok_or_else(|| Error::InvalidFunctionDataRecordEntry {
record: s.to_string(),
})?
.split(',');
let hits = parts
.next()
.ok_or_else(|| Error::InvalidFunctionDataRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidFunctionDataRecordEntry {
record: s.to_string(),
})?;
let name = parts
.next()
.ok_or_else(|| Error::InvalidFunctionDataRecordEntry {
record: s.to_string(),
})?
.to_string();
Ok(Self { hits, name })
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FunctionsFoundRecordEntry(usize);
impl std::fmt::Display for FunctionsFoundRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FNF:{}", self.0)
}
}
impl FromStr for FunctionsFoundRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let count = s
.trim()
.strip_prefix("FNF:")
.ok_or_else(|| Error::InvalidFunctionsFoundRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidFunctionsFoundRecordEntry {
record: s.to_string(),
})?;
Ok(Self(count))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FunctionsHitRecordEntry(usize);
impl std::fmt::Display for FunctionsHitRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FNH:{}", self.0)
}
}
impl FromStr for FunctionsHitRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let count = s
.trim()
.strip_prefix("FNH:")
.ok_or_else(|| Error::InvalidFunctionsHitRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidFunctionsHitRecordEntry {
record: s.to_string(),
})?;
Ok(Self(count))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BranchDataRecordEntry {
pub line_number: usize,
pub exception: bool,
pub block_id: usize,
pub branch: Either<usize, String>,
pub taken: Option<usize>,
}
impl std::fmt::Display for BranchDataRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.branch {
Either::Left(branch) => format!(
"BRDA:{},{}{},{},{}",
self.line_number,
if self.exception { "e" } else { "" },
self.block_id,
branch,
self.taken.map(|t| t.to_string()).unwrap_or("-".to_string())
),
Either::Right(branch) => format!(
"BRDA:{},{}{},{},{}",
self.line_number,
if self.exception { "e" } else { "" },
self.block_id,
branch,
self.taken.map(|t| t.to_string()).unwrap_or("-".to_string())
),
}
.fmt(f)
}
}
impl FromStr for BranchDataRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut parts = s
.trim()
.strip_prefix("BRDA:")
.ok_or_else(|| Error::InvalidBranchDataRecordEntry {
record: s.to_string(),
})?
.split(',');
let line_number = parts
.next()
.ok_or_else(|| Error::InvalidBranchDataRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidBranchDataRecordEntry {
record: s.to_string(),
})?;
let (exception, block_id) = parts
.next()
.map(|exception_and_block_id| {
if let Some(exception_and_block_id) = exception_and_block_id.strip_prefix('e') {
exception_and_block_id
.parse::<usize>()
.map(|block_id| (true, block_id))
} else {
exception_and_block_id
.parse::<usize>()
.map(|block_id| (false, block_id))
}
})
.transpose()?
.ok_or_else(|| Error::InvalidBranchDataRecordEntry {
record: s.to_string(),
})?;
let mut branch_and_taken_parts = parts.collect::<Vec<_>>();
let taken = branch_and_taken_parts
.pop()
.and_then(|last| {
if last == "-" {
None
} else {
Some(
last.parse()
.map_err(|_| Error::InvalidBranchDataRecordEntry {
record: s.to_string(),
}),
)
}
})
.transpose()?;
let branch = branch_and_taken_parts
.join(",")
.parse::<usize>()
.map(Either::Left)
.unwrap_or(Either::Right(branch_and_taken_parts.join(",")));
Ok(Self {
line_number,
exception,
block_id,
branch,
taken,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BranchesFoundRecordEntry(usize);
impl std::fmt::Display for BranchesFoundRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BRF:{}", self.0)
}
}
impl FromStr for BranchesFoundRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let count = s
.trim()
.strip_prefix("BRF:")
.ok_or_else(|| Error::InvalidBranchesFoundRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidBranchesFoundRecordEntry {
record: s.to_string(),
})?;
Ok(Self(count))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BranchesHitRecordEntry(usize);
impl std::fmt::Display for BranchesHitRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "BRH:{}", self.0)
}
}
impl FromStr for BranchesHitRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let count = s
.trim()
.strip_prefix("BRH:")
.ok_or_else(|| Error::InvalidBranchesHitRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidBranchesHitRecordEntry {
record: s.to_string(),
})?;
Ok(Self(count))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineRecordEntry {
pub line_number: usize,
pub hit_count: usize,
pub checksum: Option<String>,
}
impl std::fmt::Display for LineRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.checksum {
Some(checksum) => {
write!(f, "DA:{},{},{}", self.line_number, self.hit_count, checksum)
}
None => write!(f, "DA:{},{}", self.line_number, self.hit_count),
}
}
}
impl FromStr for LineRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut parts = s
.trim()
.strip_prefix("DA:")
.ok_or_else(|| Error::InvalidLineDataRecordEntry {
record: s.to_string(),
})?
.split(',');
let line_number = parts
.next()
.ok_or_else(|| Error::InvalidLineDataRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidLineDataRecordEntry {
record: s.to_string(),
})?;
let hit_count = parts
.next()
.ok_or_else(|| Error::InvalidLineDataRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidLineDataRecordEntry {
record: s.to_string(),
})?;
let checksum = parts.next().map(|checksum| checksum.to_string());
Ok(Self {
line_number,
hit_count,
checksum,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LinesFoundRecordEntry(usize);
impl std::fmt::Display for LinesFoundRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "LF:{}", self.0)
}
}
impl FromStr for LinesFoundRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let count = s
.trim()
.strip_prefix("LF:")
.ok_or_else(|| Error::InvalidLinesFoundRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidLinesFoundRecordEntry {
record: s.to_string(),
})?;
Ok(Self(count))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LinesHitRecordEntry(usize);
impl std::fmt::Display for LinesHitRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "LH:{}", self.0)
}
}
impl FromStr for LinesHitRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let count = s
.trim()
.strip_prefix("LH:")
.ok_or_else(|| Error::InvalidLinesHitRecordEntry {
record: s.to_string(),
})?
.parse()
.map_err(|_| Error::InvalidLinesHitRecordEntry {
record: s.to_string(),
})?;
Ok(Self(count))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EndOfRecordEntry;
impl std::fmt::Display for EndOfRecordEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "end_of_record")
}
}
impl FromStr for EndOfRecordEntry {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
if s.trim() == "end_of_record" {
Ok(Self)
} else {
Err(Error::InvalidEndOfRecordEntry {
record: s.to_string(),
})
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Record {
pub test_name: TestNameRecordEntry,
pub source_file: SourceFileRecordEntry,
pub version: Option<VersionRecordEntry>,
pub functions: BTreeMap<usize, FunctionRecordEntry>,
pub function_data: HashMap<String, FunctionDataRecordEntry>,
pub functions_found: Option<FunctionsFoundRecordEntry>,
pub functions_hit: Option<FunctionsHitRecordEntry>,
pub lines: BTreeMap<usize, LineRecordEntry>,
pub lines_found: Option<LinesFoundRecordEntry>,
pub lines_hit: Option<LinesHitRecordEntry>,
pub end_of_record: EndOfRecordEntry,
}
impl Record {
pub fn new<P>(path: P) -> Self
where
P: AsRef<Path>,
{
Self {
source_file: SourceFileRecordEntry(path.as_ref().to_path_buf()),
functions: BTreeMap::new(),
function_data: HashMap::new(),
lines: BTreeMap::new(),
..Default::default()
}
}
pub fn add_function_if_not_exists<S>(
&mut self,
start_line: usize,
end_line: Option<usize>,
name: S,
) -> bool
where
S: AsRef<str>,
{
match self.functions.entry(start_line) {
Entry::Occupied(_) => false,
Entry::Vacant(entry) => {
entry.insert(FunctionRecordEntry {
start_line,
end_line,
name: name.as_ref().to_string(),
});
if self.functions_found.is_none() {
self.functions_found = Some(FunctionsFoundRecordEntry(0));
}
let Some(functions_found) = self.functions_found.as_mut() else {
unreachable!("functions_found must be present");
};
functions_found.0 += 1;
self.function_data
.entry(name.as_ref().to_string())
.or_insert_with(|| FunctionDataRecordEntry {
hits: 0,
name: name.as_ref().to_string(),
});
true
}
}
}
pub fn increment_function_data<S>(&mut self, name: S)
where
S: AsRef<str>,
{
let entry = self
.function_data
.entry(name.as_ref().to_string())
.or_insert_with(|| {
if self.functions_found.is_none() {
self.functions_found = Some(FunctionsFoundRecordEntry(0));
}
let Some(functions_found) = self.functions_found.as_mut() else {
unreachable!("functions_found must be present");
};
functions_found.0 += 1;
FunctionDataRecordEntry {
hits: 0,
name: name.as_ref().to_string(),
}
});
if entry.hits == 0 {
if self.functions_hit.is_none() {
self.functions_hit = Some(FunctionsHitRecordEntry(0));
}
let Some(functions_hit) = self.functions_hit.as_mut() else {
unreachable!("functions_hit must be present");
};
functions_hit.0 += 1;
}
entry.hits += 1;
}
pub fn add_line_if_not_exists(&mut self, line_number: usize) -> bool {
match self.lines.entry(line_number) {
Entry::Occupied(_) => false,
Entry::Vacant(entry) => {
entry.insert(LineRecordEntry {
line_number,
hit_count: 0,
checksum: None,
});
if self.lines_found.is_none() {
self.lines_found = Some(LinesFoundRecordEntry(0));
}
let Some(lines_found) = self.lines_found.as_mut() else {
unreachable!("lines_found must be present");
};
lines_found.0 += 1;
true
}
}
}
pub fn increment_line(&mut self, line_number: usize) {
let entry = self.lines.entry(line_number).or_insert_with(|| {
if self.lines_found.is_none() {
self.lines_found = Some(LinesFoundRecordEntry(0));
}
let Some(lines_found) = self.lines_found.as_mut() else {
unreachable!("lines_found must be present");
};
lines_found.0 += 1;
LineRecordEntry {
line_number,
hit_count: 0,
checksum: None,
}
});
if entry.hit_count == 0 {
if self.lines_hit.is_none() {
self.lines_hit = Some(LinesHitRecordEntry(0));
}
let Some(lines_hit) = self.lines_hit.as_mut() else {
unreachable!("lines_hit must be present");
};
lines_hit.0 += 1
}
entry.hit_count += 1;
}
}
impl std::fmt::Display for Record {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{}", self.test_name)?;
writeln!(f, "{}", self.source_file)?;
if let Some(version) = self.version.as_ref() {
writeln!(f, "{}", version)?;
}
for function in self.functions.values() {
writeln!(f, "{}", function)?;
}
for function_data in self.function_data.values() {
writeln!(f, "{}", function_data)?;
}
if let Some(functions_found) = self.functions_found.as_ref() {
writeln!(f, "{}", functions_found)?;
}
if let Some(functions_hit) = self.functions_hit.as_ref() {
writeln!(f, "{}", functions_hit)?;
}
for line in self.lines.values() {
writeln!(f, "{}", line)?;
}
if let Some(branches_found) = self.lines_found.as_ref() {
writeln!(f, "{}", branches_found)?;
}
if let Some(branches_hit) = self.lines_hit.as_ref() {
writeln!(f, "{}", branches_hit)?;
}
writeln!(f, "{}", self.end_of_record)?;
Ok(())
}
}
impl Record {
pub fn source_filename(&self) -> String {
self.source_file
.0
.file_name()
.map(|fname| fname.to_string_lossy().to_string())
.unwrap_or("<unknown>".to_string())
}
pub(crate) fn summary(&self, top_level: PathBuf, parent: Option<PathBuf>) -> HtmlSummaryInfo {
HtmlSummaryInfo {
is_dir: false,
top_level,
parent,
filename: Some(self.source_filename()),
total_lines: self.lines_found.clone().unwrap_or_default().0,
hit_lines: self.lines_hit.clone().unwrap_or_default().0,
total_functions: self.functions_found.clone().unwrap_or_default().0,
hit_functions: self.functions_hit.clone().unwrap_or_default().0,
}
}
pub(crate) fn lines(&self) -> Result<Vec<HtmlLineInfo>> {
let contents_raw = read(self.source_file.0.as_path())?;
let contents = String::from_utf8_lossy(&contents_raw);
let lines = contents
.lines()
.enumerate()
.map(|(i, line)| {
let hit_count = self.lines.get(&(i + 1)).map(|l| l.hit_count);
let leading_spaces = line.chars().take_while(|c| c.is_whitespace()).count();
let trimmed = line.trim().to_string();
HtmlLineInfo {
hit_count,
leading_spaces,
line: trimmed,
}
})
.collect::<Vec<_>>();
Ok(lines)
}
pub(crate) fn functions(&self) -> Vec<HtmlFunctionInfo> {
let mut functions = self
.functions
.values()
.map(|f| HtmlFunctionInfo {
hit_count: self.function_data.get(&f.name).map(|d| d.hits),
name: f.name.as_str().to_string(),
})
.collect::<Vec<_>>();
functions.sort_by(|a, b| a.name.cmp(&b.name));
functions
}
}
impl FromStr for Record {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let input_lines = s.lines().collect::<Vec<_>>();
let test_name = input_lines
.first()
.ok_or_else(|| Error::InvalidRecordEntry {
record: s.to_string(),
})?
.parse()
.unwrap_or_default();
let source_file = input_lines
.get(1)
.ok_or_else(|| Error::InvalidRecordEntry {
record: s.to_string(),
})?
.parse()
.unwrap_or_default();
let mut version = None;
let mut functions = BTreeMap::new();
let mut function_data = HashMap::new();
let mut functions_found = None;
let mut functions_hit = None;
let mut lines = BTreeMap::new();
let mut lines_found = None;
let mut lines_hit = None;
let end_of_record = EndOfRecordEntry;
input_lines.iter().skip(2).for_each(|line| {
if let Ok(parsed_version) = line.parse::<VersionRecordEntry>() {
version = Some(parsed_version);
} else if let Ok(parsed_function) = line.parse::<FunctionRecordEntry>() {
functions.insert(parsed_function.start_line, parsed_function);
} else if let Ok(parsed_function_data) = line.parse::<FunctionDataRecordEntry>() {
function_data.insert(parsed_function_data.name.clone(), parsed_function_data);
} else if let Ok(parsed_functions_found) = line.parse::<FunctionsFoundRecordEntry>() {
functions_found = Some(parsed_functions_found);
} else if let Ok(parsed_functions_hit) = line.parse::<FunctionsHitRecordEntry>() {
functions_hit = Some(parsed_functions_hit);
} else if let Ok(parsed_line) = line.parse::<LineRecordEntry>() {
lines.insert(parsed_line.line_number, parsed_line);
} else if let Ok(parsed_lines_found) = line.parse::<LinesFoundRecordEntry>() {
lines_found = Some(parsed_lines_found);
} else if let Ok(parsed_lines_hit) = line.parse::<LinesHitRecordEntry>() {
lines_hit = Some(parsed_lines_hit);
}
});
Ok(Self {
test_name,
source_file,
version,
functions,
function_data,
functions_found,
functions_hit,
lines,
lines_found,
lines_hit,
end_of_record,
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Records(HashMap<PathBuf, Record>);
impl Records {
pub fn get_or_insert_mut<P>(&mut self, path: P) -> &mut Record
where
P: AsRef<Path>,
{
self.0
.entry(path.as_ref().to_path_buf())
.or_insert_with(|| Record::new(path))
}
pub fn get(&self, path: &Path) -> Option<&Record> {
self.0.get(path)
}
}
impl std::fmt::Display for Records {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for record in self.0.values() {
write!(f, "{}", record)?;
}
Ok(())
}
}
impl FromStr for Records {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut records = HashMap::new();
s.split("end_of_record\n")
.filter(|record| !record.is_empty())
.try_for_each(|record| {
let record = record.to_string() + "end_of_record";
let parsed_record = record.parse::<Record>()?;
records.insert(parsed_record.source_file.0.clone(), parsed_record);
Ok::<_, Error>(())
})?;
Ok(Self(records))
}
}
struct GraphNode {
pub path: PathBuf,
pub summary: Option<HtmlSummaryInfo>,
}
impl GraphNode {
pub fn new(path: PathBuf, summary: Option<HtmlSummaryInfo>) -> Self {
Self { path, summary }
}
}
impl std::fmt::Display for GraphNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.path.to_string_lossy())
}
}
struct GraphEdge {}
impl std::fmt::Display for GraphEdge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "")
}
}
impl Records {
pub fn to_html<P>(&self, output_directory: P) -> Result<()>
where
P: AsRef<Path>,
{
let mut graph = DiGraph::<GraphNode, GraphEdge>::new();
let mut node_ids = HashMap::<PathBuf, NodeIndex>::new();
let entries = self
.0
.values()
.map(|record| {
let absolute_source_path = record.source_file.0.canonicalize()?;
let mut output_path = output_directory
.as_ref()
.components()
.chain(
absolute_source_path
.components()
.filter(|c| matches!(c, Component::Normal(_))),
)
.collect::<PathBuf>();
output_path.set_file_name(
output_path
.file_name()
.map(|fname| fname.to_string_lossy().to_string())
.unwrap_or_default()
+ ".html",
);
if let std::collections::hash_map::Entry::Vacant(entry) =
node_ids.entry(output_path.clone())
{
entry.insert(graph.add_node(GraphNode::new(output_path.clone(), None)));
}
let mut path = output_path.as_path();
while let Some(parent) = path.parent() {
if let std::collections::hash_map::Entry::Vacant(entry) =
node_ids.entry(parent.to_path_buf())
{
entry.insert(graph.add_node(GraphNode::new(parent.to_path_buf(), None)));
}
if graph
.find_edge(
*node_ids.get(parent).ok_or_else(|| Error::NodeNotFound {
path: parent.to_path_buf(),
})?,
*node_ids.get(path).ok_or_else(|| Error::NodeNotFound {
path: path.to_path_buf(),
})?,
)
.is_none()
{
graph.add_edge(
*node_ids.get(parent).ok_or_else(|| Error::NodeNotFound {
path: parent.to_path_buf(),
})?,
*node_ids.get(path).ok_or_else(|| Error::NodeNotFound {
path: path.to_path_buf(),
})?,
GraphEdge {},
);
}
path = parent;
if !path.is_dir() {
create_dir_all(path)?;
}
if path == output_directory.as_ref() {
break;
}
}
Ok((output_path, record))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.collect::<HashMap<_, _>>();
let root = node_ids
.get(output_directory.as_ref())
.ok_or_else(|| Error::NodeNotFound {
path: output_directory.as_ref().to_path_buf(),
})?;
let mut traversal = DfsPostOrder::new(&graph, *root);
while let Some(node) = traversal.next(&graph) {
let path = graph
.node_weight(node)
.ok_or_else(|| Error::WeightNotFound { index: node })?
.path
.clone();
if let Some(record) = entries.get(path.as_path()) {
let depth = path
.components()
.count()
.saturating_sub(output_directory.as_ref().components().count())
.saturating_sub(1);
let summary = record.summary(
repeat("..")
.take(depth)
.collect::<PathBuf>()
.join("index.html"),
path.parent().map(|p| p.join("index.html")),
);
graph
.node_weight_mut(node)
.ok_or_else(|| Error::WeightNotFound { index: node })?
.summary = Some(summary.clone());
let lines = record.lines()?;
let functions = record.functions();
let page = Page {
head: Head {},
current_view: CurrentView {
summary: summary.clone(),
},
summary: Summary { summary },
main: FilePage {
listing: Listing { lines },
function_listing: FunctionListing { functions },
},
};
write(&path, page.to_string())?;
} else {
let depth = path
.components()
.count()
.saturating_sub(output_directory.as_ref().components().count());
let (top_level, parent) = if path == output_directory.as_ref() {
(PathBuf::from("index.html"), None)
} else {
(
repeat("..")
.take(depth)
.collect::<PathBuf>()
.join("index.html"),
path.parent().map(|p| p.join("index.html")),
)
};
let (total_lines, hit_lines, total_functions, hit_functions) = graph
.neighbors_directed(node, Direction::Outgoing)
.try_fold(
(0, 0, 0, 0),
|(total_lines, hit_lines, total_functions, hit_functions), neighbor| {
let summary = graph
.node_weight(neighbor)
.ok_or_else(|| Error::WeightNotFound { index: neighbor })?
.summary
.as_ref()
.ok_or_else(|| Error::NoSummaryInfo)?;
Ok::<(usize, usize, usize, usize), Error>((
total_lines + summary.total_lines,
hit_lines + summary.hit_lines,
total_functions + summary.total_functions,
hit_functions + summary.hit_functions,
))
},
)?;
let summary = HtmlSummaryInfo {
is_dir: true,
top_level,
parent,
filename: path
.file_name()
.map(|fname| fname.to_string_lossy().to_string()),
total_lines,
hit_lines,
total_functions,
hit_functions,
};
let page = Page {
head: Head {},
current_view: CurrentView {
summary: summary.clone(),
},
summary: Summary {
summary: summary.clone(),
},
main: DirectoryPage {
summaries: graph
.neighbors_directed(node, Direction::Outgoing)
.filter_map(|neighbor| {
graph
.node_weight(neighbor)
.ok_or_else(|| Error::WeightNotFound { index: neighbor })
.ok()
.and_then(|weight| weight.summary.as_ref().cloned())
})
.collect(),
},
};
write(path.join("index.html"), page.to_string())?;
graph
.node_weight_mut(node)
.ok_or_else(|| Error::WeightNotFound { index: node })?
.summary = Some(summary);
}
}
write(
output_directory.as_ref().join("index.info"),
self.to_string(),
)?;
Ok(())
}
}
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod test {
use std::path::PathBuf;
use std::str::FromStr;
use super::Records;
#[test]
fn test_records() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut records = Records::default();
let record_test =
records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test.c"));
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test2 =
records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test2.c"));
record_test2.add_function_if_not_exists(1, Some(3), "x");
record_test2.increment_function_data("x");
record_test2.add_line_if_not_exists(1);
record_test2.add_line_if_not_exists(2);
record_test2.add_line_if_not_exists(3);
record_test2.increment_line(1);
record_test2.increment_line(2);
record_test2.increment_line(3);
}
#[test]
fn test_records_to_html() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut records = Records::default();
let record_test =
records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test.c"));
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test = records.get_or_insert_mut(
manifest_dir
.join("tests")
.join("rsrc")
.join("subdir1")
.join("test.c"),
);
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test = records.get_or_insert_mut(
manifest_dir
.join("tests")
.join("rsrc")
.join("subdir2")
.join("test-subdir2.c"),
);
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test2 =
records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test2.c"));
record_test2.add_function_if_not_exists(1, Some(3), "x");
record_test2.increment_function_data("x");
record_test2.add_line_if_not_exists(1);
record_test2.add_line_if_not_exists(2);
record_test2.add_line_if_not_exists(3);
record_test2.increment_line(1);
record_test2.increment_line(2);
record_test2.increment_line(3);
records
.to_html(manifest_dir.join("tests").join("rsrc").join("html"))
.unwrap();
}
#[test]
fn test_records_to_lcov() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut records = Records::default();
let record_test =
records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test.c"));
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test = records.get_or_insert_mut(
manifest_dir
.join("tests")
.join("rsrc")
.join("subdir1")
.join("test.c"),
);
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test = records.get_or_insert_mut(
manifest_dir
.join("tests")
.join("rsrc")
.join("subdir2")
.join("test-subdir2.c"),
);
record_test.add_function_if_not_exists(4, Some(16), "main");
record_test.increment_function_data("main");
record_test.add_line_if_not_exists(4);
record_test.add_line_if_not_exists(5);
record_test.add_line_if_not_exists(7);
record_test.add_line_if_not_exists(9);
record_test.add_line_if_not_exists(11);
record_test.add_line_if_not_exists(12);
record_test.add_line_if_not_exists(14);
record_test.increment_line(4);
record_test.increment_line(5);
record_test.increment_line(7);
record_test.increment_line(9);
record_test.increment_line(11);
record_test.increment_line(14);
let record_test2 =
records.get_or_insert_mut(manifest_dir.join("tests").join("rsrc").join("test2.c"));
record_test2.add_function_if_not_exists(1, Some(3), "x");
record_test2.increment_function_data("x");
record_test2.add_line_if_not_exists(1);
record_test2.add_line_if_not_exists(2);
record_test2.add_line_if_not_exists(3);
record_test2.increment_line(1);
record_test2.increment_line(2);
record_test2.increment_line(3);
let to_lcov = records.to_string();
let from_lcov = Records::from_str(&to_lcov).unwrap();
assert_eq!(records, from_lcov);
}
}