use super::{Resolver, validate};
use crate::manifest::Manifest;
fn parse(body: &str) -> Manifest {
serde_yaml_ng::from_str(body).expect("manifest parses")
}
fn rule(manifest: &Manifest) -> &crate::manifest::Command {
manifest
.commands
.iter()
.find(|rule| rule.name == "sh")
.expect("rule declared")
}
fn rust_project(source: &str, pattern: &str) -> (tempfile::TempDir, Manifest) {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("lib.rs"), source).expect("write source");
let manifest = parse(&format!(
"probes:\n api:\n file: lib.rs\n ast: '{pattern}'\ncommands:\n - name: sh\n inputs: [api]\n"
));
(dir, manifest)
}
fn digest_of(dir: &tempfile::TempDir, manifest: &Manifest) -> String {
Resolver::new(manifest, dir.path())
.for_rule(rule(manifest))
.expect("probe resolves")
.get("api")
.cloned()
.expect("probe `api` resolved")
}
fn failure_of(dir: &tempfile::TempDir, manifest: &Manifest) -> crate::error::Error {
Resolver::new(manifest, dir.path())
.for_rule(rule(manifest))
.expect_err("probe fails")
}
fn shape_refusal(body: &str) -> String {
let manifest = parse(body);
let failed = validate(&manifest.probes, &manifest.scopes, &manifest.probe_shell)
.expect_err("the shape is refused");
failed.to_string()
}
#[cfg(feature = "lang-rust")]
#[test]
fn an_ast_probe_reads_a_file_without_spawning_anything() {
let (dir, manifest) = rust_project("pub fn one() {}\n", "pub fn $N() {}");
assert_eq!(digest_of(&dir, &manifest).len(), 64, "a blake3 hex digest");
}
#[cfg(feature = "lang-rust")]
#[test]
fn only_the_matched_slice_moves_the_digest() {
let pattern = "pub fn $N() {}";
let (before, manifest) =
rust_project("// first\npub fn one() {}\nfn hidden() { 1; }\n", pattern);
let (after, _) = rust_project(
"// second thoughts\npub fn one() {}\nfn hidden() { 2; }\n",
pattern,
);
assert_eq!(
digest_of(&before, &manifest),
digest_of(&after, &manifest),
"a reworded comment and a changed private body are not this probe's input"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn a_change_inside_the_matched_slice_moves_the_digest() {
let pattern = "pub fn $N() {}";
let (before, manifest) = rust_project("pub fn one() {}\n", pattern);
let (after, _) = rust_project("pub fn renamed() {}\n", pattern);
assert_ne!(digest_of(&before, &manifest), digest_of(&after, &manifest));
}
#[cfg(feature = "lang-rust")]
#[test]
fn a_pattern_matching_nothing_is_a_hard_error() {
let (dir, manifest) = rust_project("fn private() {}\n", "pub fn $N() {}");
let message = failure_of(&dir, &manifest).to_string();
assert!(message.contains("api"), "names the probe: {message}");
assert!(
message.contains("allow_empty"),
"names the opt-in: {message}"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn allow_empty_opts_into_a_pattern_that_matched_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("lib.rs"), "fn private() {}\n").expect("write source");
let manifest = parse(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $N() {}'\n allow_empty: true\ncommands:\n - name: sh\n inputs: [api]\n",
);
assert_eq!(digest_of(&dir, &manifest).len(), 64);
}
#[test]
fn a_run_probe_without_lang_names_the_missing_key() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = parse(
"probes:\n api:\n run: printf 'pub fn one() {}'\n ast: 'pub fn $N() {}'\ncommands:\n - name: sh\n inputs: [api]\n",
);
let message = failure_of(&dir, &manifest).to_string();
assert!(message.contains("lang:"), "names the key: {message}");
}
#[cfg(feature = "lang-rust")]
#[test]
fn a_run_probe_with_lang_matches_over_stdout() {
let dir = tempfile::tempdir().expect("tempdir");
let manifest = parse(
"probes:\n api:\n run: printf 'pub fn one() {}'\n ast: 'pub fn $N() {}'\n lang: rust\ncommands:\n - name: sh\n inputs: [api]\n",
);
assert_eq!(digest_of(&dir, &manifest).len(), 64);
}
#[test]
fn a_probe_declaring_both_selectors_is_refused() {
let message =
shape_refusal("probes:\n api:\n file: lib.rs\n json: '.a'\n ast: 'fn $N() {}'\n");
assert!(message.contains("one selector"), "{message}");
}
#[test]
fn lang_without_ast_is_refused() {
let message =
shape_refusal("probes:\n api:\n file: data.json\n json: '.a'\n lang: rust\n");
assert!(message.contains("without `ast:`"), "{message}");
}
#[test]
fn a_file_with_no_selector_offers_both() {
let message = shape_refusal("probes:\n api:\n file: lib.rs\n");
assert!(message.contains("`json:` or `ast:`"), "{message}");
}
#[test]
fn an_unknown_language_is_refused_at_resolve_time() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("lib.rs"), "pub fn one() {}\n").expect("write source");
let manifest = parse(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $N() {}'\n lang: cobol\ncommands:\n - name: sh\n inputs: [api]\n",
);
let message = failure_of(&dir, &manifest).to_string();
assert!(message.contains("cobol"), "names the language: {message}");
assert!(message.contains("this build parses"), "{message}");
}
fn captured_project(source: &str, pattern: &str, capture: &str) -> (tempfile::TempDir, Manifest) {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("lib.rs"), source).expect("write source");
let manifest = parse(&format!(
"probes:\n api:\n file: lib.rs\n ast: '{pattern}'\n capture: {capture}\ncommands:\n - name: sh\n inputs: [api]\n"
));
(dir, manifest)
}
const SIGNATURE: &str = "pub fn $NAME($$$ARGS) -> $RET { $$$BODY }";
#[cfg(feature = "lang-rust")]
#[test]
fn a_captured_probe_depends_on_the_signature_and_not_the_body() {
let (before, manifest) = captured_project(
"pub fn one(a: u8) -> u8 { a }\n",
SIGNATURE,
"[NAME, ARGS, RET]",
);
let (after, _) = captured_project(
"pub fn one(a: u8) -> u8 { a + 0 }\n",
SIGNATURE,
"[NAME, ARGS, RET]",
);
assert_eq!(
digest_of(&before, &manifest),
digest_of(&after, &manifest),
"the body was matched but not captured"
);
let (renamed, _) = captured_project(
"pub fn renamed(a: u8) -> u8 { a }\n",
SIGNATURE,
"[NAME, ARGS, RET]",
);
assert_ne!(
digest_of(&before, &manifest),
digest_of(&renamed, &manifest),
"the signature is still the input"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn the_default_is_still_the_whole_matched_node() {
let (before, manifest) = rust_project("pub fn one(a: u8) -> u8 { a }\n", SIGNATURE);
let (after, _) = rust_project("pub fn one(a: u8) -> u8 { a + 0 }\n", SIGNATURE);
assert_ne!(
digest_of(&before, &manifest),
digest_of(&after, &manifest),
"a match is a whole node unless `capture:` says otherwise"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn a_capture_the_pattern_does_not_define_names_the_probe() {
let (dir, manifest) = captured_project("pub fn one() -> u8 { 1 }\n", SIGNATURE, "[NAME, TYPO]");
let message = failure_of(&dir, &manifest).to_string();
assert!(message.contains("api"), "names the probe: {message}");
assert!(message.contains("TYPO"), "names the miss: {message}");
assert!(
message.contains("`NAME`"),
"names what is defined: {message}"
);
}
#[cfg(feature = "lang-rust")]
#[test]
fn allow_empty_does_not_waive_an_undefined_capture() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("lib.rs"), "pub fn one() -> u8 { 1 }\n").expect("write source");
let manifest = parse(&format!(
"probes:\n api:\n file: lib.rs\n ast: '{SIGNATURE}'\n capture: [TYPO]\n allow_empty: true\ncommands:\n - name: sh\n inputs: [api]\n"
));
assert!(
failure_of(&dir, &manifest).to_string().contains("TYPO"),
"an undefined capture is not an emptiness `allow_empty` can opt into"
);
}
#[test]
fn the_capture_list_rules_are_refused_at_load() {
for (body, expected) in [
(
"probes:\n api:\n file: lib.rs\n json: '.a'\n capture: [NAME]\n",
"without `ast:`",
),
(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $NAME()'\n capture: []\n",
"empty `capture:` list",
),
(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $NAME()'\n capture: ['$NAME']\n",
"without the `$`",
),
(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $NAME()'\n capture: [name]\n",
"not a metavariable name",
),
(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $NAME()'\n capture: [NAME, NAME]\n",
"twice",
),
] {
let message = shape_refusal(body);
assert!(message.contains(expected), "{expected}: {message}");
}
}
#[test]
fn a_dropped_variable_is_refused_by_name() {
let message = shape_refusal(
"probes:\n api:\n file: lib.rs\n ast: 'pub fn $NAME($_ARG)'\n capture: [_ARG]\n",
);
assert!(message.contains("not a metavariable name"), "{message}");
}