Skip to content

Commit 4e14d5a

Browse files
committed
test(cli): check separately what a C++ answer has to get right
- A scan that loses the file's name, the compiler's answer about it, or the offsets that answer carries looks identical from the outside: a full, successful, semantic run that reports nothing a compiler contributed. Ask the three questions apart so a failure says which one went.
1 parent 9de89ca commit 4e14d5a

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

crates/codehelion-cli/tests/semantic_cpp/core_semantics.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,119 @@ fn assert_sequence_family(
435435
);
436436
}
437437
}
438+
439+
/// The compiler's answer and the fragments cut from the same file have to be
440+
/// about the same bytes of the same file, and three separate things have to
441+
/// line up for that: how the analysis names the file, whether it says anything
442+
/// about it, and whether what it says points where the scan read.
443+
///
444+
/// Each is checked separately because a scan that loses any one of them looks
445+
/// identical from the outside — a full, successful, semantic run that reports
446+
/// nothing a compiler contributed. Which of the three went is the difference
447+
/// between a spelling fault, a compiler that answered about nothing, and
448+
/// offsets measured against a different copy of the file.
449+
#[test]
450+
fn a_cpp_answer_is_about_the_bytes_the_scan_read() {
451+
require_clang_helper();
452+
let directory = tempfile::tempdir().expect("temp dir");
453+
let root = codehelion_fixtures::copy_cpp("overload-resolution", directory.path())
454+
.expect("plant fixture");
455+
let report = scan(&root);
456+
let run_id = report["run"]["run_id"]
457+
.as_i64()
458+
.or_else(|| reports(&report).first()?["run"]["run_id"].as_i64())
459+
.expect("the scan records a run id");
460+
461+
let store = Store::open(&root.join(".codehelion/audit.db")).expect("open audit database");
462+
let source = root.join("src").join("range_loop.cpp");
463+
let absolute = codehelion_core::paths::canonical(&source).expect("the planted source resolves");
464+
let text = std::fs::read_to_string(&source).expect("the planted source is readable");
465+
466+
let irs: Vec<_> = store
467+
.run_compiler_units(run_id)
468+
.expect("read compiler rows")
469+
.into_iter()
470+
.filter_map(|unit| match unit.outcome {
471+
CompilerOutcome::Analyzed(ir) => Some(ir),
472+
CompilerOutcome::Unavailable { .. } => None,
473+
})
474+
.collect();
475+
assert!(!irs.is_empty(), "no unit of this tree was analyzed");
476+
477+
// How a reader holding the file asks about it, written by the same rule the
478+
// helper wrote its anchors with. Everything below is filed under this name.
479+
let named: Vec<String> = irs.iter().map(|ir| ir.spelling(&absolute)).collect();
480+
let anchored: Vec<&str> = irs
481+
.iter()
482+
.flat_map(|ir| {
483+
ir.calls
484+
.iter()
485+
.map(|call| call.anchor.expansion.file.as_str())
486+
})
487+
.collect();
488+
assert!(
489+
named.iter().any(|name| anchored.contains(&name.as_str())),
490+
"no answer is filed under the name a reader looks this file up by.\n\
491+
looked up as: {named:?}\n\
492+
answers are filed under: {:?}\n\
493+
anchored at: {:?}",
494+
anchored
495+
.iter()
496+
.copied()
497+
.collect::<std::collections::BTreeSet<_>>(),
498+
irs.iter().map(|ir| &ir.anchored_at).collect::<Vec<_>>()
499+
);
500+
501+
let spelling = named
502+
.iter()
503+
.find(|name| anchored.contains(&name.as_str()))
504+
.expect("one of the answers is about this file");
505+
let ir = irs
506+
.iter()
507+
.find(|ir| {
508+
ir.calls
509+
.iter()
510+
.any(|call| call.anchor.expansion.file == *spelling)
511+
})
512+
.expect("the answer about this file");
513+
514+
// What it says about it. A standard call is the smallest thing every rule
515+
// over this fixture is built out of.
516+
let standard: Vec<_> = ir
517+
.calls
518+
.iter()
519+
.filter(|call| call.anchor.expansion.file == *spelling)
520+
.filter(|call| call.api_name.as_deref() == Some("std::push_back"))
521+
.collect();
522+
assert!(
523+
!standard.is_empty(),
524+
"the compiler resolved no standard call in a file written out of them: {:?}",
525+
ir.calls
526+
.iter()
527+
.filter(|call| call.anchor.expansion.file == *spelling)
528+
.filter_map(|call| call.api_name.as_deref())
529+
.collect::<std::collections::BTreeSet<_>>()
530+
);
531+
assert!(
532+
ir.semantic_constructs
533+
.iter()
534+
.any(|construct| construct.anchor.expansion.file == *spelling),
535+
"the compiler reported no construct in a file of range loops"
536+
);
537+
538+
// And where. An offset is only an offset into some copy of the file, so it
539+
// is checked against the copy the scan read rather than assumed to be the
540+
// same one.
541+
for call in standard {
542+
let range = &call.anchor.expansion;
543+
let start = usize::try_from(range.start_byte).expect("an offset within this file");
544+
let end = usize::try_from(range.end_byte).expect("an offset within this file");
545+
let written = text
546+
.get(start..end)
547+
.unwrap_or_else(|| panic!("{start}..{end} is outside the {} bytes read", text.len()));
548+
assert!(
549+
written.contains("push_back"),
550+
"the offsets point at {written:?}, not at the call they were reported for"
551+
);
552+
}
553+
}

0 commit comments

Comments
 (0)