use super::*;
use crate::snippets::session::SessionSpec;
use crate::snippets::types::{Language, SnippetMetadata, SourceOrigin};
use crate::snippets::validators::SnippetValidator;
use std::collections::BTreeMap;
use std::path::Path;
use std::sync::Arc;
struct CountingValidator {
language: Language,
invocations: Arc<Mutex<usize>>,
}
impl SnippetValidator for CountingValidator {
fn language(&self) -> Language {
self.language
}
fn is_available(&self) -> bool {
true
}
fn validate(
&self,
_snippet: &Snippet,
_level: ValidationLevel,
_timeout_secs: u64,
) -> Result<(SnippetStatus, Option<String>)> {
*self.invocations.lock().expect("invocations") += 1;
Ok((SnippetStatus::Pass, None))
}
fn validate_in_session(
&self,
snippet: &Snippet,
level: ValidationLevel,
timeout_secs: u64,
_session: Option<&crate::snippets::session::ValidationSession>,
) -> Result<(SnippetStatus, Option<String>)> {
self.validate(snippet, level, timeout_secs)
}
fn max_level(&self) -> ValidationLevel {
ValidationLevel::Run
}
}
fn snippet(language: Language) -> Snippet {
Snippet {
id: None,
path: "example.md".into(),
language,
title: None,
code: "example snippet body".into(),
start_line: 1,
block_index: 0,
annotation: None,
metadata: SnippetMetadata::default(),
source_origin: SourceOrigin {
path: "example.md".into(),
line: 1,
block_index: 0,
},
}
}
fn sh_path(path: &Path) -> String {
path.display().to_string().replace('\\', "/")
}
fn touch_hook(marker: &Path) -> String {
let script = format!("touch {}", sh_path(marker));
if cfg!(windows) {
format!("sh -c '{script}'")
} else {
script
}
}
fn python_session(working_directory: &Path, marker: &Path) -> SessionSpec {
SessionSpec {
language: Language::Python,
working_directory: working_directory.to_path_buf(),
manifest: None,
before: vec![touch_hook(marker)],
env: BTreeMap::new(),
include_paths: Vec::new(),
rust_features: Vec::new(),
rust_dependencies: BTreeMap::new(),
}
}
#[test]
fn a_fully_cached_run_does_not_rerun_the_sessions_before_hook() {
let working_directory = tempfile::tempdir().expect("working directory");
let cache_directory = tempfile::tempdir().expect("cache directory");
let marker = working_directory.path().join("before-hook-ran");
let mut registry = ValidatorRegistry::new();
let invocations = Arc::new(Mutex::new(0_usize));
registry.register(Box::new(CountingValidator {
language: Language::Python,
invocations: Arc::clone(&invocations),
}));
let mut sessions = HashMap::new();
sessions.insert("python".to_string(), python_session(working_directory.path(), &marker));
let config = RunnerConfig {
level: ValidationLevel::Syntax,
parallelism: 1,
cache_dir: Some(cache_directory.path().to_path_buf()),
changed_only: true,
sessions,
..RunnerConfig::default()
};
let snippets = vec![snippet(Language::Python)];
let first = run_validation(&snippets, ®istry, &config).expect("first run completes");
assert_eq!(
first.passed, 1,
"the uncached first pass must validate and pass the snippet"
);
assert!(marker.exists(), "the before hook must run on the first, uncached pass");
assert_eq!(*invocations.lock().expect("invocations"), 1);
std::fs::remove_file(&marker).expect("remove marker between runs");
let second = run_validation(&snippets, ®istry, &config).expect("second run completes");
assert_eq!(
second.passed, 1,
"the cached second pass must still report the same passing result"
);
assert!(
!marker.exists(),
"a fully cached run must not re-run the session's before hook"
);
assert_eq!(
*invocations.lock().expect("invocations"),
1,
"a fully cached run must not invoke the validator a second time either"
);
}
#[test]
fn a_session_with_any_uncached_snippet_still_runs_its_before_hook() {
let working_directory = tempfile::tempdir().expect("working directory");
let cache_directory = tempfile::tempdir().expect("cache directory");
let marker = working_directory.path().join("before-hook-ran");
let mut registry = ValidatorRegistry::new();
let invocations = Arc::new(Mutex::new(0_usize));
registry.register(Box::new(CountingValidator {
language: Language::Python,
invocations: Arc::clone(&invocations),
}));
let mut sessions = HashMap::new();
sessions.insert("python".to_string(), python_session(working_directory.path(), &marker));
let config = RunnerConfig {
level: ValidationLevel::Syntax,
parallelism: 1,
cache_dir: Some(cache_directory.path().to_path_buf()),
changed_only: true,
sessions,
..RunnerConfig::default()
};
let cached_snippet = snippet(Language::Python);
let first = run_validation(std::slice::from_ref(&cached_snippet), ®istry, &config).expect("first run completes");
assert_eq!(first.passed, 1);
std::fs::remove_file(&marker).expect("remove marker between runs");
assert_eq!(*invocations.lock().expect("invocations"), 1);
let mut new_snippet = snippet(Language::Python);
new_snippet.code = "a body never seen before, so it cannot be a cache hit".into();
let second = run_validation(&[cached_snippet, new_snippet], ®istry, &config).expect("second run completes");
assert_eq!(
second.passed, 2,
"both the cached and the uncached snippet must still pass"
);
assert!(
marker.exists(),
"a session with any uncached snippet must still run its before hook"
);
assert_eq!(
*invocations.lock().expect("invocations"),
2,
"only the genuinely uncached snippet should have reached the validator a second time"
);
}