use super::*;
use crate::output::offenders::Severity;
use std::path::PathBuf;
fn rec(path: &str, metric: &str, value: f64, limit: f64) -> OffenderRecord {
OffenderRecord {
path: PathBuf::from(path),
function: Some("f".into()),
start_line: 42,
end_line: 50,
start_col: Some(5),
metric: metric.into(),
value,
limit,
severity: Severity::Warning,
}
}
fn render(offenders: &[OffenderRecord]) -> String {
let mut buf = Vec::new();
write_checkstyle(offenders, &mut buf).expect("writing to Vec is infallible");
String::from_utf8(buf).expect("output is UTF-8")
}
#[test]
fn empty_emits_self_closing_root() {
insta::assert_snapshot!(render(&[]), @r###"
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3"/>
"###);
}
#[test]
fn single_offender_round_trips() {
let offenders = vec![rec("src/foo.rs", "cyclomatic", 17.0, 15.0)];
insta::assert_snapshot!(render(&offenders), @r###"
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="src/foo.rs">
<error line="42" column="5" severity="warning" message="cyclomatic 17 exceeds limit 15" source="big-code-analysis.cyclomatic"/>
</file>
</checkstyle>
"###);
}
#[test]
fn multiple_files_grouped_alphabetically() {
let offenders = vec![
rec("src/zeta.rs", "cyclomatic", 20.0, 15.0),
rec("src/alpha.rs", "loc.lloc", 250.0, 100.0),
rec("src/alpha.rs", "halstead.volume", 1234.5, 1000.0),
];
insta::assert_snapshot!(render(&offenders), @r###"
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="src/alpha.rs">
<error line="42" column="5" severity="warning" message="loc.lloc 250 exceeds limit 100" source="big-code-analysis.loc.lloc"/>
<error line="42" column="5" severity="warning" message="halstead.volume 1234.5 exceeds limit 1000" source="big-code-analysis.halstead.volume"/>
</file>
<file name="src/zeta.rs">
<error line="42" column="5" severity="warning" message="cyclomatic 20 exceeds limit 15" source="big-code-analysis.cyclomatic"/>
</file>
</checkstyle>
"###);
}
#[test]
fn error_severity_renders_as_error() {
let mut r = rec("a.rs", "cyclomatic", 99.0, 15.0);
r.severity = Severity::Error;
let out = render(&[r]);
assert!(out.contains(r#"severity="error""#), "{out}");
}
#[test]
fn missing_column_omits_attribute() {
let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
r.start_col = None;
let out = render(&[r]);
assert!(!out.contains("column="), "{out}");
assert!(out.contains(r#"line="42""#), "{out}");
}
#[test]
fn xml_special_chars_in_path_and_metric_are_escaped() {
let r = OffenderRecord {
path: PathBuf::from(r#"src/<a&b>"c'd.rs"#),
function: None,
start_line: 1,
end_line: 1,
start_col: None,
metric: r#"weird"&<metric>"#.into(),
value: 1.0,
limit: 0.0,
severity: Severity::Warning,
};
let out = render(&[r]);
assert!(
out.contains(r#"name="src/<a&b>"c'd.rs""#),
"{out}"
);
assert!(
out.contains(r#"source="big-code-analysis.weird"&<metric>""#),
"{out}"
);
}
#[test]
fn start_line_zero_is_clamped_to_one() {
let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
r.start_line = 0;
let out = render(&[r]);
assert!(out.contains(r#"line="1""#), "{out}");
}
#[test]
fn start_column_zero_is_clamped_to_one() {
let mut r = rec("a.rs", "cyclomatic", 17.0, 15.0);
r.start_col = Some(0);
let out = render(&[r]);
assert!(out.contains(r#"column="1""#), "{out}");
}
#[test]
fn control_characters_in_message_replaced() {
let r = OffenderRecord {
path: PathBuf::from("a.rs"),
function: None,
start_line: 1,
end_line: 1,
start_col: None,
metric: "weird\u{0001}name".into(),
value: 1.0,
limit: 0.0,
severity: Severity::Warning,
};
let out = render(&[r]);
assert!(out.contains("weird?name"), "{out}");
}
#[test]
fn whitespace_in_attribute_round_trips_via_numeric_refs() {
use quick_xml::events::Event;
use quick_xml::reader::Reader;
let r = OffenderRecord {
path: PathBuf::from("src/weird\npath\twith\rwhitespace.rs"),
function: None,
start_line: 1,
end_line: 1,
start_col: None,
metric: "cyclomatic".into(),
value: 1.0,
limit: 0.0,
severity: Severity::Warning,
};
let out = render(&[r]);
assert!(out.contains("
"), "missing 
 (LF) in {out}");
assert!(out.contains("	"), "missing 	 (TAB) in {out}");
assert!(out.contains("
"), "missing 
 (CR) in {out}");
let name_open = out.find("name=\"").expect("name attribute present");
let after_open = &out[name_open + b"name=\"".len()..];
let name_close = after_open.find('"').expect("name attribute closed");
let attr_lit = &after_open[..name_close];
assert!(
!attr_lit.contains('\n') && !attr_lit.contains('\t') && !attr_lit.contains('\r'),
"raw whitespace leaked into attribute literal: {attr_lit:?}"
);
let mut reader = Reader::from_str(&out);
let mut buf = Vec::new();
let mut roundtripped: Option<String> = None;
loop {
match reader.read_event_into(&mut buf).expect("well-formed XML") {
Event::Start(start) | Event::Empty(start) if start.name().as_ref() == b"file" => {
for attr in start.attributes().with_checks(false).flatten() {
if attr.key.as_ref() == b"name" {
roundtripped = Some(
attr.unescape_value()
.expect("attribute value decodes")
.into_owned(),
);
}
}
}
Event::Eof => break,
_ => {}
}
buf.clear();
}
let roundtripped = roundtripped.expect("found <file name=...>");
assert_eq!(roundtripped, "src/weird\npath\twith\rwhitespace.rs");
}
#[test]
fn xml_char_illegal_noncharacters_replaced_and_roundtrips() {
use quick_xml::events::Event;
use quick_xml::reader::Reader;
let r = OffenderRecord {
path: PathBuf::from("src/bad\u{FFFE}\u{FFFF}path.rs"),
function: None,
start_line: 1,
end_line: 1,
start_col: None,
metric: "weird\u{1FFFE}\u{10FFFF}metric".into(),
value: 1.0,
limit: 0.0,
severity: Severity::Warning,
};
let out = render(&[r]);
for nc in ['\u{FFFE}', '\u{FFFF}', '\u{1FFFE}', '\u{10FFFF}'] {
assert!(
!out.contains(nc),
"non-character {:04X} leaked into output: {out:?}",
nc as u32,
);
}
assert!(out.contains("src/bad??path.rs"), "{out}");
assert!(out.contains("weird??metric"), "{out}");
let mut reader = Reader::from_str(&out);
let mut buf = Vec::new();
while !matches!(
reader.read_event_into(&mut buf).expect("well-formed XML"),
Event::Eof
) {
buf.clear();
}
}
#[test]
fn predefined_entities_still_escape_after_whitespace_fix() {
let r = OffenderRecord {
path: PathBuf::from("a&b<c>d\"e'f.rs"),
function: None,
start_line: 1,
end_line: 1,
start_col: None,
metric: "cyclomatic".into(),
value: 1.0,
limit: 0.0,
severity: Severity::Warning,
};
let out = render(&[r]);
assert!(
out.contains(r#"name="a&b<c>d"e'f.rs""#),
"predefined-entity escapes regressed: {out}"
);
}