use std::collections::BTreeMap;
use serde::Deserialize;
use crate::answer::Counts;
use crate::dialects::check_buckets;
use crate::readers::{cloc, scc};
const KNOWN_FORMAT: u32 = 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
pub enum PerLineFormat {
#[serde(rename = "linejudge-per-line")]
LinejudgePerLine,
#[serde(rename = "cloc-stages")]
ClocStages,
#[serde(rename = "scc-trace")]
SccTrace,
}
#[derive(Debug, PartialEq, Eq)]
pub struct PerLineAnswer {
pub buckets_of_lines: Vec<String>,
pub counts: Counts,
}
pub fn read_output(
format: PerLineFormat,
buckets: &[String],
lines_of_the_file: usize,
text: &str,
) -> Result<PerLineAnswer, String> {
match format {
PerLineFormat::LinejudgePerLine => read_per_line(text, buckets, lines_of_the_file),
PerLineFormat::ClocStages => cloc::read_per_line(buckets, lines_of_the_file, text),
PerLineFormat::SccTrace => scc::read_per_line(buckets, lines_of_the_file, text),
}
}
pub fn read_per_line(
text: &str,
buckets: &[String],
lines_of_the_file: usize,
) -> Result<PerLineAnswer, String> {
let raw: RawDocument =
serde_json::from_str(text).map_err(|e| format!("it does not parse: {e}"))?;
if raw.format != KNOWN_FORMAT {
return Err(format!(
"it declares format {}, and the format this reads is {KNOWN_FORMAT}",
raw.format
));
}
let verdicts = raw.per_line.into_iter().map(|verdict| (verdict.line, verdict.bucket)).collect();
build_answer(raw.lines, raw.buckets, verdicts, buckets, lines_of_the_file)
}
pub(crate) fn build_answer(
lines: u32,
totals: BTreeMap<String, u32>,
verdicts: Vec<(u32, String)>,
buckets: &[String],
lines_of_the_file: usize,
) -> Result<PerLineAnswer, String> {
check_buckets(&totals, buckets).map_err(|e| format!("it {e}"))?;
if verdicts.len() != lines_of_the_file {
return Err(format!(
"it answers {} lines, and the file has {lines_of_the_file}",
verdicts.len()
));
}
if lines as usize != lines_of_the_file {
return Err(format!("it says the file has {lines} lines, and it has {lines_of_the_file}"));
}
let mut buckets_of_lines = Vec::with_capacity(verdicts.len());
let mut summed: BTreeMap<String, u32> = BTreeMap::new();
for (index, (line, bucket)) in verdicts.into_iter().enumerate() {
let expected = index as u32 + 1;
if line != expected {
return Err(format!(
"its answer number {expected} is for line {line}, and the format numbers them \
from 1 with none missing"
));
}
if !buckets.contains(&bucket) {
return Err(format!(
"it puts line {line} in {bucket}, which is not one of its buckets: {}",
buckets.join(", ")
));
}
*summed.entry(bucket.clone()).or_default() += 1;
buckets_of_lines.push(bucket);
}
for (name, total) in &totals {
let counted = summed.get(name).copied().unwrap_or_default();
if counted != *total {
return Err(format!(
"it counts {total} {name} for the file and {counted} of its lines say {name}"
));
}
}
Ok(PerLineAnswer { buckets_of_lines, counts: Counts { lines, buckets: totals } })
}
#[derive(Deserialize)]
struct RawDocument {
format: u32,
lines: u32,
buckets: BTreeMap<String, u32>,
per_line: Vec<RawVerdict>,
}
#[derive(Deserialize)]
struct RawVerdict {
line: u32,
bucket: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_document_comes_back_as_one_bucket_per_line_and_the_totals_it_printed() {
let read = read_per_line(&a_document(), &buckets(), 3).unwrap();
assert_eq!(read.buckets_of_lines, ["code", "comments", "code"]);
assert_eq!(read.counts.lines, 3);
assert_eq!(read.counts.buckets["comments"], 1);
}
#[test]
fn a_field_the_reader_does_not_know_is_passed_over_instead_of_refused() {
let with_extras = a_document().replace(
"{ \"line\": 2, \"bucket\": \"comments\" }",
"{ \"line\": 2, \"bucket\": \"comments\", \"class\": \"words_in_comment\", \
\"spans\": [[0, 4, \"comment\"]] }",
);
assert_eq!(read_per_line(&with_extras, &buckets(), 3).unwrap().buckets_of_lines[1], "comments");
}
#[test]
fn a_verdict_for_every_line_is_the_promise_and_a_document_that_breaks_it_is_named() {
let refused = |text: &str, lines: usize| {
read_per_line(text, &buckets(), lines).err().unwrap_or_else(|| panic!("{text} was read"))
};
assert!(refused(&a_document(), 4).contains("it answers 3 lines, and the file has 4"));
assert!(refused("nothing json about it", 3).contains("does not parse"));
assert!(refused(&a_document().replace("\"format\": 1", "\"format\": 2"), 3)
.contains("it declares format 2"));
let renumbered = a_document().replace("\"line\": 2", "\"line\": 4");
assert!(refused(&renumbered, 3).contains("its answer number 2 is for line 4"));
let miscounted = a_document().replace("\"lines\": 3", "\"lines\": 7");
assert!(refused(&miscounted, 3).contains("it says the file has 7 lines"));
}
#[test]
fn totals_that_do_not_add_up_to_the_lines_under_them_are_refused() {
let text = a_document().replace("\"code\": 2", "\"code\": 3");
let refused = read_per_line(&text, &buckets(), 3).unwrap_err();
assert!(refused.contains("it counts 3 code for the file and 2 of its lines say code"), "{refused}");
}
#[test]
fn a_bucket_this_way_of_counting_does_not_have_is_refused_wherever_it_appears() {
let renamed = a_document().replace("\"bucket\": \"comments\"", "\"bucket\": \"documentation\"");
let refused = read_per_line(&renamed, &buckets(), 3).unwrap_err();
assert!(refused.contains("it puts line 2 in documentation"), "{refused}");
let extra_total = a_document().replace("\"extra\": 0", "\"extra\": 0, \"docs\": 0");
let refused = read_per_line(&extra_total, &buckets(), 3).unwrap_err();
assert!(refused.contains("has a bucket named docs"), "{refused}");
}
#[test]
fn each_format_is_read_by_its_own_reader_through_the_dispatch() {
let scc = "TRACE 12:00: a.c line 1 ended with state: 1: counted as code\n\
[{\"Name\":\"C\",\"Lines\":1,\"Code\":1,\"Comment\":0,\"Blank\":0}]\n";
let read = read_output(PerLineFormat::SccTrace, &three(), 1, scc).unwrap();
assert_eq!(read.buckets_of_lines, ["code"]);
let cloc = "->a.c Original file:\n 1 | int x;\n\
->a.c Blank lines removed:\n 1 | int x;\n\
{\"SUM\": {\"blank\": 0, \"comment\": 0, \"code\": 1}}\n";
let read = read_output(PerLineFormat::ClocStages, &three(), 1, cloc).unwrap();
assert_eq!(read.buckets_of_lines, ["code"]);
}
fn three() -> Vec<String> {
["code", "comments", "blanks"].map(String::from).to_vec()
}
fn a_document() -> String {
"{\n \"format\": 1,\n \"counter\": \"mezura\",\n \"lines\": 3,\n \
\"buckets\": { \"code\": 2, \"comments\": 1, \"extra\": 0 },\n \"per_line\": [\n \
{ \"line\": 1, \"bucket\": \"code\" },\n \
{ \"line\": 2, \"bucket\": \"comments\" },\n \
{ \"line\": 3, \"bucket\": \"code\" }\n ]\n}"
.to_string()
}
fn buckets() -> Vec<String> {
["code", "comments", "extra"].map(String::from).to_vec()
}
}