#![cfg_attr(coverage_nightly, coverage(off))]
use std::fmt::Write;
use super::super::ProjectScore;
use super::boxdraw::{box_blank, box_bottom, box_row, box_separator, box_top};
use super::ungraded::{box_entry_budget, ungraded_rows};
use crate::cli::colors as c;
#[must_use]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn format_project(project: &ProjectScore) -> String {
let mut output = String::new();
let mut line = |text: String| {
writeln!(output, "{text}").expect("Writing to String buffer cannot fail");
};
line(box_top());
line(box_row(&c::header("Project TDG Score Report")));
line(box_separator());
line(box_row(
&match (project.average_score, project.average_grade) {
(Some(score), Some(grade)) => {
let grade = grade.to_string();
format!(
"Average Score: {}/100 ({})",
c::number(&format!("{score:.1}")),
c::grade(&grade)
)
}
_ => c::dim("Average Score: not measured (no files analysed)"),
},
));
line(box_row(&format!(
"Total Files: {}",
c::number(&project.total_files.to_string())
)));
let ungraded = ungraded_rows(&project.ungraded_files, Some(box_entry_budget()));
for (i, row) in ungraded.iter().enumerate() {
line(box_row(&if i == 0 {
c::colored(c::YELLOW, row)
} else {
c::dim(row)
}));
}
let waived = project
.files
.iter()
.filter(|f| f.critical_defects_suppressed.is_some())
.count();
if waived > 0 {
line(box_row(&c::colored(
c::YELLOW,
&format!("Waived (#279): {waived} file(s) with critical defects"),
)));
}
if project.files_truncated {
let via = project
.list_filter
.as_deref()
.map(|f| format!(" ({f})"))
.unwrap_or_default();
line(box_row(&format!(
"Files Listed: {} of {}{via}",
c::number(&project.files_reported.to_string()),
c::number(&project.total_files.to_string())
)));
}
line(box_blank());
line(box_row(&c::label("Language Distribution:")));
for (language, count) in &project.language_distribution {
let percentage = percent_of(*count, project.total_files);
line(box_row(&format!(
"├─ {:12}: {} files ({percentage:4.1}%)",
language.to_string(),
c::number(&format!("{count:3}")),
)));
}
line(box_blank());
line(box_row(&c::label("Grade Distribution:")));
for (grade, count) in &project.grade_distribution {
let percentage = percent_of(*count, project.total_files);
let grade_text = grade.to_string();
line(box_row(&format!(
"├─ {}: {} files ({percentage:4.1}%)",
c::grade(&grade_text),
c::number(&format!("{count:3}"))
)));
}
line(box_bottom());
output
}
fn percent_of(count: usize, total: usize) -> f32 {
if total == 0 {
0.0
} else {
(count as f32 / total as f32) * 100.0
}
}
#[cfg(test)]
mod ungraded_disclosure_tests {
use super::*;
use crate::tdg::formatters::boxdraw::visible_width;
use crate::tdg::{ProjectScore, UngradedFile};
fn project_with(paths: &[&str]) -> ProjectScore {
let mut project = ProjectScore::aggregate(Vec::new());
for p in paths {
project.ungraded_files.push(UngradedFile {
path: (*p).to_string(),
reason: "expected `;`".to_string(),
});
}
project
}
#[test]
fn long_paths_are_named_by_their_tail_and_stay_distinct() {
let rendered = format_project(&project_with(&[
"/home/noah/src/aprender/crates/aprender-core/src/oracle/arxiv_entries.rs",
"/home/noah/src/aprender/crates/aprender-core/src/oracle/coursera_entries.rs",
]));
assert!(rendered.contains("Not Graded: 2 file(s)"), "{rendered}");
assert!(rendered.contains("arxiv_entries.rs"), "got:\n{rendered}");
assert!(rendered.contains("coursera_entries.rs"), "got:\n{rendered}");
}
#[test]
fn named_rows_stay_inside_the_frame() {
let rendered = format_project(&project_with(&[
"/home/noah/src/aprender/crates/aprender-core/src/oracle/coursera/arxiv_entries.rs",
]));
let widths: Vec<usize> = rendered
.lines()
.filter(|l| !l.is_empty())
.map(visible_width)
.collect();
for (i, w) in widths.iter().enumerate() {
assert_eq!(
*w,
widths[0],
"line {i} is {w} cols wide, frame is {} — {:?}",
widths[0],
rendered.lines().nth(i)
);
}
}
}
#[cfg(test)]
mod color_census_tests {
use super::*;
use crate::cli::colors::assert_honours_color;
use crate::tdg::language_simple::Language;
use crate::tdg::{Grade, ProjectScore, TdgScore};
use std::collections::BTreeMap;
fn project() -> ProjectScore {
ProjectScore {
average_score: Some(90.9),
average_grade: Some(Grade::A),
not_measured: Vec::new(),
total_files: 7,
files: Vec::new(),
language_distribution: BTreeMap::from([(Language::Rust, 7)]),
grade_distribution: BTreeMap::from([(Grade::A, 5), (Grade::F, 2)]),
..ProjectScore::aggregate(Vec::new())
}
}
#[test]
fn project_table_honours_color() {
assert_honours_color("tdg::formatters::format_project", || {
format_project(&project())
});
}
#[test]
fn empty_project_table_honours_color() {
assert_honours_color("tdg::formatters::format_project (empty)", || {
format_project(&ProjectScore::aggregate(Vec::new()))
});
}
#[test]
fn file_report_honours_color() {
assert_honours_color("tdg::formatters::format_human", || {
super::super::format_human(&TdgScore {
total: 85.5,
grade: Grade::AMinus,
language: Language::Rust,
confidence: 1.0,
file_path: Some(std::path::PathBuf::from("src/test.rs")),
..TdgScore::default()
})
});
}
#[test]
fn plain_table_still_reads_as_before() {
let _guard = crate::cli::colors::ForcedColor::off();
let rendered = format_project(&project());
assert!(
rendered.contains("Average Score: 90.9/100 (A)"),
"plain table must be unchanged, got:\n{rendered}"
);
assert!(rendered.contains("Total Files: 7"), "got:\n{rendered}");
}
#[test]
fn colored_table_stays_rectangular() {
let _guard = crate::cli::colors::ForcedColor::on();
let rendered = format_project(&project());
let widths: Vec<usize> = rendered
.lines()
.filter(|l| !l.is_empty())
.map(crate::tdg::formatters::boxdraw::visible_width)
.collect();
assert!(!widths.is_empty(), "nothing rendered");
for (i, w) in widths.iter().enumerate() {
assert_eq!(
*w,
widths[0],
"coloured line {i} is {w} cols wide, frame is {} — {:?}",
widths[0],
rendered.lines().nth(i)
);
}
}
}