use std::{
collections::BTreeSet,
env,
ffi::OsString,
fs,
io::{ErrorKind, Read},
path::{Component, Path, PathBuf},
process::{Command, ExitStatus, Stdio},
thread,
time::{Duration, Instant},
};
use serde::Deserialize;
use bamts_cli::{
args::{CliArgs, ExecutionTarget, Mode, parse_args},
driver,
};
use crate::{ErrorCode, Result, VerificationError};
pub const SCHEMA_VERSION: u32 = 1;
pub const NODE_VERSION: &str = "24.18.0";
pub const NODE_VERSION_OUTPUT: &str = "v24.18.0";
pub const MANIFEST_PATH: &str = "corpus/manifest.toml";
pub const PINNED_CASE_IDS: [&str; 20] = [
"citty",
"defu",
"destr",
"dot-prop",
"escape-string-regexp",
"hookable",
"is-plain-obj",
"mitt",
"ohash",
"p-defer",
"p-map",
"p-queue",
"pathe",
"perfect-debounce",
"rou3",
"tiny-invariant",
"tslib",
"ufo",
"valita",
"yocto-queue",
];
pub const TASK_106_SYNC_CASE_IDS: [&str; 11] = [
"defu",
"destr",
"dot-prop",
"escape-string-regexp",
"mitt",
"p-queue",
"pathe",
"rou3",
"tslib",
"ufo",
"valita",
];
pub const TASK_107_NODE_CASE_IDS: [&str; 3] = ["citty", "is-plain-obj", "ohash"];
pub const NORMALIZED_ENV: [&str; 4] = ["TZ=UTC", "LANG=C", "LC_ALL=C", "NO_COLOR=1"];
pub const COMPARE_KEYS: [&str; 2] = ["stdout", "exit_code"];
const COMMIT_LEN: usize = 40;
const MIN_TIMEOUT_MS: u64 = 1;
const MAX_TIMEOUT_MS: u64 = 60_000;
const DEFAULT_MAX_OUTPUT_BYTES: usize = 1 << 20;
const READ_CHUNK: usize = 8192;
const POLL_INTERVAL: Duration = Duration::from_millis(5);
const NODE_VERSION_TIMEOUT: Duration = Duration::from_secs(10);
const NODE_VERSION_OUTPUT_CAP: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CorpusManifest {
pub node_version: String,
pub environment: Vec<String>,
pub compare: Vec<String>,
pub projects: Vec<ManifestProject>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ManifestProject {
pub id: String,
pub repository: String,
pub commit: String,
pub spec: String,
pub entrypoint: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CaseSpec {
pub id: String,
pub repository: String,
pub commit: String,
pub license: String,
pub source_dir: String,
pub entrypoint: String,
pub node_args: Vec<String>,
pub expected_timeout_ms: u64,
pub constructs: Vec<String>,
pub source_files: Vec<String>,
}
impl CaseSpec {
pub fn timeout(&self) -> Duration {
Duration::from_millis(self.expected_timeout_ms)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Corpus {
pub manifest: CorpusManifest,
pub cases: Vec<CaseSpec>,
}
impl Corpus {
pub fn case(&self, id: &str) -> Option<&CaseSpec> {
self.cases.iter().find(|case| case.id == id)
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawManifest {
schema: u32,
node_version: String,
environment: Vec<String>,
compare: Vec<String>,
projects: Vec<RawProject>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawProject {
id: String,
repository: String,
commit: String,
spec: String,
entrypoint: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawSpec {
schema: u32,
id: String,
repository: String,
commit: String,
license: String,
source_dir: String,
entrypoint: String,
node_args: Vec<String>,
expected_timeout_ms: u64,
constructs: Vec<String>,
source_files: Vec<String>,
}
pub fn load_manifest(root: &Path) -> Result<CorpusManifest> {
let path = root.join(MANIFEST_PATH);
let raw: RawManifest = parse_toml(&path)?;
validate_manifest(&path, raw)
}
pub fn load_corpus(root: &Path) -> Result<Corpus> {
let manifest = load_manifest(root)?;
verify_exact_layout(root, &manifest)?;
let mut cases = Vec::with_capacity(manifest.projects.len());
for project in &manifest.projects {
let spec_path = root.join(&project.spec);
let raw: RawSpec = parse_toml(&spec_path)?;
let spec = validate_spec(&spec_path, raw, project)?;
verify_case_paths(root, &spec_path, &spec)?;
cases.push(spec);
}
Ok(Corpus { manifest, cases })
}
fn validate_manifest(path: &Path, raw: RawManifest) -> Result<CorpusManifest> {
require_schema(path, raw.schema)?;
if raw.node_version != NODE_VERSION {
return Err(schema_error(
path,
format!(
"manifest node_version must be `{NODE_VERSION}`, found `{}`",
raw.node_version
),
));
}
require_exact_set(
path,
"manifest environment",
&raw.environment,
&NORMALIZED_ENV,
)?;
require_exact_set(path, "manifest compare", &raw.compare, &COMPARE_KEYS)?;
if raw.projects.is_empty() {
return Err(schema_error(path, "manifest declares no projects"));
}
let mut ids = BTreeSet::new();
let mut specs = BTreeSet::new();
let mut entrypoints = BTreeSet::new();
let mut projects = Vec::with_capacity(raw.projects.len());
for raw_project in raw.projects {
validate_project(path, &raw_project)?;
if !ids.insert(raw_project.id.clone()) {
return Err(schema_error(
path,
format!("duplicate project id `{}`", raw_project.id),
));
}
if !specs.insert(raw_project.spec.clone()) {
return Err(schema_error(
path,
format!("duplicate project spec path `{}`", raw_project.spec),
));
}
if !entrypoints.insert(raw_project.entrypoint.clone()) {
return Err(schema_error(
path,
format!("duplicate project entrypoint `{}`", raw_project.entrypoint),
));
}
projects.push(ManifestProject {
id: raw_project.id,
repository: raw_project.repository,
commit: raw_project.commit,
spec: raw_project.spec,
entrypoint: raw_project.entrypoint,
});
}
Ok(CorpusManifest {
node_version: raw.node_version,
environment: raw.environment,
compare: raw.compare,
projects,
})
}
fn validate_project(path: &Path, project: &RawProject) -> Result<()> {
require_nonempty(path, "project id", &project.id)?;
require_nonempty(
path,
&format!("project `{}` repository", project.id),
&project.repository,
)?;
require_commit(path, &project.id, &project.commit)?;
require_clean_relative(
path,
&format!("project `{}` spec", project.id),
&project.spec,
)?;
require_clean_relative(
path,
&format!("project `{}` entrypoint", project.id),
&project.entrypoint,
)?;
require_ts_entrypoint(path, &project.id, &project.entrypoint)
}
fn validate_spec(path: &Path, raw: RawSpec, project: &ManifestProject) -> Result<CaseSpec> {
require_schema(path, raw.schema)?;
require_match(path, "id", &raw.id, &project.id)?;
require_match(path, "repository", &raw.repository, &project.repository)?;
require_match(path, "commit", &raw.commit, &project.commit)?;
require_match(path, "entrypoint", &raw.entrypoint, &project.entrypoint)?;
require_commit(path, &raw.id, &raw.commit)?;
require_nonempty(path, "license", &raw.license)?;
require_clean_relative(path, "source_dir", &raw.source_dir)?;
require_clean_relative(path, "entrypoint", &raw.entrypoint)?;
require_ts_entrypoint(path, &raw.id, &raw.entrypoint)?;
require_unique_nonempty(path, "constructs", &raw.constructs)?;
require_unique_nonempty(path, "source_files", &raw.source_files)?;
for source_file in &raw.source_files {
require_clean_relative(path, "source_files entry", source_file)?;
}
validate_node_args(path, &raw.node_args)?;
if !(MIN_TIMEOUT_MS..=MAX_TIMEOUT_MS).contains(&raw.expected_timeout_ms) {
return Err(schema_error(
path,
format!(
"expected_timeout_ms must be within {MIN_TIMEOUT_MS}..={MAX_TIMEOUT_MS}, found {}",
raw.expected_timeout_ms
),
));
}
Ok(CaseSpec {
id: raw.id,
repository: raw.repository,
commit: raw.commit,
license: raw.license,
source_dir: raw.source_dir,
entrypoint: raw.entrypoint,
node_args: raw.node_args,
expected_timeout_ms: raw.expected_timeout_ms,
constructs: raw.constructs,
source_files: raw.source_files,
})
}
fn verify_exact_layout(root: &Path, manifest: &CorpusManifest) -> Result<()> {
let expected_specs = manifest
.projects
.iter()
.map(|project| project.spec.clone())
.collect::<BTreeSet<_>>();
let expected_cases = manifest
.projects
.iter()
.map(|project| project.entrypoint.clone())
.collect::<BTreeSet<_>>();
require_same_paths(
root,
"corpus spec files",
&expected_specs,
&directory_entries(root, "corpus/specs")?,
)?;
require_same_paths(
root,
"corpus case entrypoints",
&expected_cases,
&directory_entries(root, "corpus/cases")?,
)
}
fn directory_entries(root: &Path, relative: &str) -> Result<BTreeSet<String>> {
let directory = root.join(relative);
let mut paths = BTreeSet::new();
let entries = fs::read_dir(&directory).map_err(|error| io_error(&directory, &error))?;
for entry in entries {
let entry = entry.map_err(|error| io_error(&directory, &error))?;
let file_type = entry
.file_type()
.map_err(|error| io_error(&entry.path(), &error))?;
if file_type.is_symlink() || !file_type.is_file() {
return Err(schema_error(
&entry.path(),
"expected a regular corpus file",
));
}
let name = entry
.file_name()
.into_string()
.map_err(|_| schema_error(&entry.path(), "corpus entry name must be valid UTF-8"))?;
paths.insert(format!("{relative}/{name}"));
}
Ok(paths)
}
fn require_same_paths(
root: &Path,
kind: &str,
expected: &BTreeSet<String>,
actual: &BTreeSet<String>,
) -> Result<()> {
if expected == actual {
return Ok(());
}
let missing = expected
.difference(actual)
.map(String::as_str)
.collect::<Vec<_>>()
.join(", ");
let extra = actual
.difference(expected)
.map(String::as_str)
.collect::<Vec<_>>()
.join(", ");
Err(VerificationError::new(
ErrorCode::SetMismatch,
format!(
"{}: {kind}: missing [{missing}]; extra [{extra}]",
root.display()
),
))
}
fn verify_case_paths(root: &Path, spec_path: &Path, spec: &CaseSpec) -> Result<()> {
require_dir(root, spec_path, &spec.source_dir)?;
require_file(root, spec_path, &spec.entrypoint)?;
for source_file in &spec.source_files {
require_file(root, spec_path, source_file)?;
}
Ok(())
}
fn validate_node_args(path: &Path, args: &[String]) -> Result<()> {
if args.iter().any(|arg| arg.is_empty()) {
return Err(schema_error(
path,
"node_args must not contain empty entries",
));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OracleLimits {
pub timeout: Duration,
pub max_output_bytes: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OracleOutcome {
pub timed_out: bool,
pub exit_code: Option<i32>,
pub signal: Option<i32>,
pub stdout: Vec<u8>,
pub stdout_truncated: bool,
pub stderr: Vec<u8>,
pub stderr_truncated: bool,
pub compile_stderr: Vec<u8>,
pub compile_stderr_truncated: bool,
}
impl OracleOutcome {
pub fn parity_key(&self) -> (&[u8], Option<i32>) {
(&self.stdout, self.exit_code)
}
pub fn is_reliable(&self) -> bool {
!self.timed_out && !self.stdout_truncated
}
pub fn parity_matches(&self, other: &OracleOutcome) -> bool {
self.is_reliable()
&& other.is_reliable()
&& self.exit_code == other.exit_code
&& self.stdout == other.stdout
}
}
#[derive(Debug, Clone)]
pub struct NodeOracle {
node: PathBuf,
root: PathBuf,
environment: Vec<(String, String)>,
max_output_bytes: usize,
}
impl NodeOracle {
pub fn new(root: &Path, node: &Path) -> Result<Self> {
verify_node_version(node)?;
Ok(Self {
node: node.to_path_buf(),
root: root.to_path_buf(),
environment: normalized_env(),
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
})
}
pub fn discover(root: &Path) -> Result<Self> {
let node = locate_node()?;
Self::new(root, &node)
}
pub fn with_max_output_bytes(mut self, cap: usize) -> Self {
self.max_output_bytes = cap;
self
}
pub fn node(&self) -> &Path {
&self.node
}
pub fn run_case(&self, spec: &CaseSpec) -> Result<OracleOutcome> {
let entrypoint_root = self.root.join(MANIFEST_PATH);
require_clean_relative(&entrypoint_root, "entrypoint", &spec.entrypoint)
.map_err(|error| corpus_stage_error(CorpusStage::Resolve, error))?;
let mut args: Vec<OsString> = Vec::with_capacity(spec.node_args.len() + 1);
for arg in &spec.node_args {
args.push(OsString::from(arg));
}
args.push(OsString::from(&spec.entrypoint));
let limits = OracleLimits {
timeout: spec.timeout(),
max_output_bytes: self.max_output_bytes,
};
run_node(&self.node, &self.root, &self.environment, &args, &limits)
.map_err(|error| corpus_stage_error(CorpusStage::Spawn, error))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionMode {
Jit,
Aot,
}
impl ExecutionMode {
pub const ALL: [Self; 2] = [Self::Jit, Self::Aot];
pub const fn as_str(self) -> &'static str {
match self {
Self::Jit => "jit",
Self::Aot => "aot",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CorpusStage {
Load,
Resolve,
Check,
Lower,
Verify,
Instantiate,
Evaluate,
Link,
Spawn,
Compare,
}
impl CorpusStage {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Load => "load",
Self::Resolve => "resolve",
Self::Check => "check",
Self::Lower => "lower",
Self::Verify => "verify",
Self::Instantiate => "instantiate",
Self::Evaluate => "evaluate",
Self::Link => "link",
Self::Spawn => "spawn",
Self::Compare => "compare",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CorpusFailure {
pub stage: CorpusStage,
pub evidence: String,
}
impl CorpusFailure {
#[must_use]
pub fn from_driver_error(error: &driver::DriverError) -> Self {
let stage = match error {
driver::DriverError::ReadSource { .. } | driver::DriverError::ProgramLoad(_) => {
CorpusStage::Load
}
driver::DriverError::UnsupportedSourceExtension { .. }
| driver::DriverError::Usage(_)
| driver::DriverError::LintConfig { .. }
| driver::DriverError::ProjectConfig { .. }
| driver::DriverError::MissingEntrypoint
| driver::DriverError::MultipleCompileInputs
| driver::DriverError::UnsupportedCompileTarget(_)
| driver::DriverError::UnsupportedOutputOption(_) => CorpusStage::Resolve,
driver::DriverError::Diagnostics { .. } => CorpusStage::Check,
driver::DriverError::Lower(error) => program_lower_stage(error),
driver::DriverError::Jit(error) => jit_stage(error),
driver::DriverError::Native(error) => native_stage(error),
driver::DriverError::Aot(error) => aot_stage(error),
driver::DriverError::CreateDirectory { .. }
| driver::DriverError::CacheArchive { .. }
| driver::DriverError::WriteObject { .. }
| driver::DriverError::ToolchainMissing { .. }
| driver::DriverError::ToolchainProbe { .. }
| driver::DriverError::ToolchainRejected { .. }
| driver::DriverError::LinkStart { .. }
| driver::DriverError::LinkFailed { .. }
| driver::DriverError::PublishExecutable { .. }
| driver::DriverError::CrossTargetLink { .. } => CorpusStage::Link,
};
Self {
stage,
evidence: driver_error_evidence(error),
}
}
}
impl std::fmt::Display for CorpusFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"stage={}: {}",
self.stage.as_str(),
self.evidence
)
}
}
const FAILURE_EVIDENCE_BYTES: usize = 512;
fn program_lower_stage(error: &bamts_compiler::program::ProgramLowerError) -> CorpusStage {
match &error.kind {
bamts_compiler::program::ProgramLowerErrorKind::Lower(error) => lower_stage(error),
bamts_compiler::program::ProgramLowerErrorKind::Link(_) => CorpusStage::Verify,
_ => CorpusStage::Lower,
}
}
fn lower_stage(error: &bamts_compiler::lower::LowerError) -> CorpusStage {
if matches!(
&error.kind,
bamts_compiler::lower::LowerErrorKind::Verify(_)
) {
CorpusStage::Verify
} else {
CorpusStage::Lower
}
}
fn jit_stage(error: &bamts_codegen::JitError) -> CorpusStage {
match error {
bamts_codegen::JitError::Lower(_) => CorpusStage::Lower,
bamts_codegen::JitError::Module(_) | bamts_codegen::JitError::UnknownHelper { .. } => {
CorpusStage::Instantiate
}
}
}
fn aot_stage(error: &bamts_codegen::AotError) -> CorpusStage {
match error {
bamts_codegen::AotError::Lower(_) => CorpusStage::Lower,
bamts_codegen::AotError::TargetLookup(_)
| bamts_codegen::AotError::TargetBuild(_)
| bamts_codegen::AotError::TargetEndianness(_)
| bamts_codegen::AotError::InvalidLoweredModule(_)
| bamts_codegen::AotError::Module(_)
| bamts_codegen::AotError::Emit(_) => CorpusStage::Instantiate,
}
}
fn native_stage(error: &bamts_runtime::NativeError) -> CorpusStage {
match error {
bamts_runtime::NativeError::Runtime(_) | bamts_runtime::NativeError::FatalTrap { .. } => {
CorpusStage::Evaluate
}
bamts_runtime::NativeError::Abi(_) | bamts_runtime::NativeError::ProgramMismatch => {
CorpusStage::Link
}
}
}
fn driver_error_evidence(error: &driver::DriverError) -> String {
match error {
driver::DriverError::Diagnostics { rendered } => {
let code = first_diagnostic_code(rendered);
match code {
Some(code) => format!("diagnostic={code}; rendered={}", bounded_text(rendered)),
None => format!("rendered={}", bounded_text(rendered)),
}
}
driver::DriverError::Native(bamts_runtime::NativeError::Runtime(error)) => format!(
"runtime function={} pc={} opcode={:?}; error={}",
error.function.get(),
error.pc.get(),
error.source.instruction,
bounded_text(&error.to_string())
),
_ => bounded_text(&error.to_string()),
}
}
fn first_diagnostic_code(rendered: &str) -> Option<&str> {
let bytes = rendered.as_bytes();
if bytes.len() < 10 {
return None;
}
for start in 0..=bytes.len() - 10 {
let candidate = &bytes[start..start + 10];
if candidate.starts_with(b"BAMTS-")
&& candidate[6].is_ascii_uppercase()
&& candidate[7..].iter().all(u8::is_ascii_digit)
{
return rendered.get(start..start + 10);
}
}
None
}
fn bounded_text(text: &str) -> String {
let mut evidence = text
.bytes()
.take(FAILURE_EVIDENCE_BYTES)
.flat_map(|byte| byte.escape_ascii())
.map(char::from)
.collect::<String>();
if text.len() > FAILURE_EVIDENCE_BYTES {
evidence.push_str("...");
}
evidence
}
#[derive(Debug, Clone)]
pub struct BamtsRunner {
root: PathBuf,
max_output_bytes: usize,
}
impl BamtsRunner {
pub fn new(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
}
}
pub fn with_max_output_bytes(mut self, cap: usize) -> Self {
self.max_output_bytes = cap;
self
}
pub fn run_case(&self, spec: &CaseSpec, mode: ExecutionMode) -> Result<OracleOutcome> {
let manifest = self.root.join(MANIFEST_PATH);
require_clean_relative(&manifest, "entrypoint", &spec.entrypoint)
.map_err(|error| corpus_stage_error(CorpusStage::Resolve, error))?;
match mode {
ExecutionMode::Jit => self.run_jit(spec),
ExecutionMode::Aot => self.run_aot(spec),
}
}
fn run_jit(&self, spec: &CaseSpec) -> Result<OracleOutcome> {
let args = cli_args(
Mode::Run,
ExecutionTarget::Jit,
self.root.join(&spec.entrypoint),
None,
)
.map_err(|error| corpus_stage_error(CorpusStage::Resolve, error))?;
let started = Instant::now();
let outcome =
driver::execute(&args).map_err(|error| cli_error(spec, ExecutionMode::Jit, error))?;
Ok(driver_outcome(
outcome,
started.elapsed() >= spec.timeout(),
self.max_output_bytes,
))
}
fn run_aot(&self, spec: &CaseSpec) -> Result<OracleOutcome> {
let artifacts = ArtifactDirectory::create(&self.root, spec)
.map_err(|error| corpus_stage_error(CorpusStage::Link, error))?;
let executable = artifacts.executable(spec);
let args = cli_args(
Mode::Compile,
ExecutionTarget::Aot,
self.root.join(&spec.entrypoint),
Some(&executable),
)
.map_err(|error| corpus_stage_error(CorpusStage::Resolve, error))?;
let compile =
driver::execute(&args).map_err(|error| cli_error(spec, ExecutionMode::Aot, error))?;
let limits = aot_execution_limits(spec, self.max_output_bytes);
let outcome = run_process(
"BamTS AOT executable",
&executable,
&self.root,
&normalized_env(),
&[],
&limits,
)
.map_err(|error| corpus_stage_error(CorpusStage::Spawn, error))?;
Ok(with_aot_compile_evidence(
outcome,
compile.stderr,
self.max_output_bytes,
))
}
}
fn aot_execution_limits(spec: &CaseSpec, max_output_bytes: usize) -> OracleLimits {
OracleLimits {
timeout: spec.timeout(),
max_output_bytes,
}
}
fn with_aot_compile_evidence(
mut runtime: OracleOutcome,
compile_stderr: Vec<u8>,
max_output_bytes: usize,
) -> OracleOutcome {
(runtime.compile_stderr, runtime.compile_stderr_truncated) =
bounded_output(compile_stderr, max_output_bytes);
runtime
}
fn cli_args(
mode: Mode,
target: ExecutionTarget,
entrypoint: PathBuf,
output: Option<&Path>,
) -> Result<CliArgs> {
let mut raw = vec![
mode.to_string(),
entrypoint.to_string_lossy().into_owned(),
"--target".to_owned(),
target.to_string(),
];
if let Some(path) = output {
raw.push("--output".to_owned());
raw.push(path.to_string_lossy().into_owned());
}
parse_args(raw).map_err(|error| {
VerificationError::new(
ErrorCode::Usage,
format!("cannot construct corpus CLI invocation: {error}"),
)
})
}
fn driver_outcome(outcome: driver::CommandOutcome, timed_out: bool, cap: usize) -> OracleOutcome {
let (stdout, stdout_truncated) = bounded_output(outcome.stdout, cap);
let (stderr, stderr_truncated) = bounded_output(outcome.stderr, cap);
OracleOutcome {
timed_out,
exit_code: Some(outcome.exit_code),
signal: None,
stdout,
stdout_truncated,
stderr,
stderr_truncated,
compile_stderr: Vec::new(),
compile_stderr_truncated: false,
}
}
fn bounded_output(mut output: Vec<u8>, cap: usize) -> (Vec<u8>, bool) {
let truncated = output.len() > cap;
output.truncate(cap);
(output, truncated)
}
struct ArtifactDirectory(PathBuf);
impl ArtifactDirectory {
fn create(root: &Path, spec: &CaseSpec) -> Result<Self> {
let path = root
.join("target/corpus-differential")
.join(&spec.id)
.join(ExecutionMode::Aot.as_str());
if path.exists() {
fs::remove_dir_all(&path).map_err(|error| io_error(&path, &error))?;
}
fs::create_dir_all(&path).map_err(|error| io_error(&path, &error))?;
Ok(Self(path))
}
fn executable(&self, spec: &CaseSpec) -> PathBuf {
self.0
.join(format!("{}{}", spec.id, env::consts::EXE_SUFFIX))
}
}
impl Drop for ArtifactDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn cli_error(
spec: &CaseSpec,
mode: ExecutionMode,
error: driver::DriverError,
) -> VerificationError {
let failure = CorpusFailure::from_driver_error(&error);
VerificationError::new(
ErrorCode::ToolFailed,
format!(
"case `{}` failed in {} mode: {failure}",
spec.id,
mode.as_str()
),
)
}
fn corpus_stage_error(stage: CorpusStage, error: VerificationError) -> VerificationError {
VerificationError::new(
error.code(),
format!(
"stage={}: {}",
stage.as_str(),
bounded_text(&error.to_string())
),
)
}
pub fn verify_node_version(node: &Path) -> Result<()> {
let limits = OracleLimits {
timeout: NODE_VERSION_TIMEOUT,
max_output_bytes: NODE_VERSION_OUTPUT_CAP,
};
let outcome = run_node(
node,
&env::temp_dir(),
&normalized_env(),
&[OsString::from("--version")],
&limits,
)?;
if outcome.timed_out {
return Err(VerificationError::new(
ErrorCode::ToolFailed,
format!("`{} --version` timed out", node.display()),
));
}
if outcome.exit_code != Some(0) {
return Err(VerificationError::new(
ErrorCode::ToolFailed,
format!(
"`{} --version` exited with {:?}",
node.display(),
outcome.exit_code
),
));
}
let reported = String::from_utf8_lossy(&outcome.stdout);
let reported = reported.trim();
if reported != NODE_VERSION_OUTPUT {
return Err(VerificationError::new(
ErrorCode::ToolFailed,
format!("Node oracle reported `{reported}`, expected `{NODE_VERSION_OUTPUT}`"),
));
}
Ok(())
}
fn run_node(
node: &Path,
cwd: &Path,
environment: &[(String, String)],
args: &[OsString],
limits: &OracleLimits,
) -> Result<OracleOutcome> {
run_process("Node oracle", node, cwd, environment, args, limits)
}
fn run_process(
label: &str,
program: &Path,
cwd: &Path,
environment: &[(String, String)],
args: &[OsString],
limits: &OracleLimits,
) -> Result<OracleOutcome> {
let mut command = Command::new(program);
command.env_clear();
for (key, value) in environment {
command.env(key, value);
}
command
.current_dir(cwd)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
let mut child = command
.spawn()
.map_err(|error| spawn_error(label, program, &error))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| pipe_error(label, "stdout"))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| pipe_error(label, "stderr"))?;
let cap = limits.max_output_bytes;
let stdout_handle = drain_stream(stdout, cap);
let stderr_handle = drain_stream(stderr, cap);
let start = Instant::now();
let mut timed_out = false;
let mut termination_error = None;
let status = loop {
if let Some(status) = child.try_wait().map_err(|error| wait_error(label, error))? {
break status;
}
if start.elapsed() >= limits.timeout {
termination_error = terminate_process_group(&mut child, label).err();
timed_out = true;
break child.wait().map_err(|error| wait_error(label, error))?;
}
thread::sleep(POLL_INTERVAL);
};
let (stdout, stdout_truncated) = stdout_handle
.join()
.map_err(|_| thread_error(label, "stdout"))?;
let (stderr, stderr_truncated) = stderr_handle
.join()
.map_err(|_| thread_error(label, "stderr"))?;
if let Some(error) = termination_error {
return Err(error);
}
Ok(OracleOutcome {
timed_out,
exit_code: status.code(),
signal: termination_signal(&status),
stdout,
stdout_truncated,
stderr,
stderr_truncated,
compile_stderr: Vec::new(),
compile_stderr_truncated: false,
})
}
#[cfg(unix)]
fn terminate_process_group(child: &mut std::process::Child, label: &str) -> Result<()> {
let group = format!("-{}", child.id());
let status = Command::new("kill")
.args(["-KILL", "--", &group])
.status()
.map_err(|error| {
VerificationError::new(
ErrorCode::ToolFailed,
format!("cannot terminate {label} process group {group}: {error}"),
)
})?;
if status.success() {
return Ok(());
}
let _ = child.kill();
Err(VerificationError::new(
ErrorCode::ToolFailed,
format!("cannot terminate {label} process group {group}: kill exited with {status}"),
))
}
#[cfg(not(unix))]
fn terminate_process_group(child: &mut std::process::Child, label: &str) -> Result<()> {
child.kill().map_err(|error| {
VerificationError::new(
ErrorCode::ToolFailed,
format!("cannot terminate {label}: {error}"),
)
})
}
fn drain_stream<R: Read + Send + 'static>(
mut reader: R,
cap: usize,
) -> thread::JoinHandle<(Vec<u8>, bool)> {
thread::spawn(move || {
let mut buffer = Vec::new();
let mut chunk = [0u8; READ_CHUNK];
let mut truncated = false;
loop {
match reader.read(&mut chunk) {
Ok(0) => break,
Ok(read) => {
if buffer.len() < cap {
let take = (cap - buffer.len()).min(read);
buffer.extend_from_slice(&chunk[..take]);
if take < read {
truncated = true;
}
} else {
truncated = true;
}
}
Err(error) if error.kind() == ErrorKind::Interrupted => continue,
Err(_) => break,
}
}
(buffer, truncated)
})
}
#[cfg(unix)]
fn termination_signal(status: &ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
status.signal()
}
#[cfg(not(unix))]
fn termination_signal(_status: &ExitStatus) -> Option<i32> {
None
}
fn locate_node() -> Result<PathBuf> {
let path = env::var_os("PATH").ok_or_else(|| {
VerificationError::new(
ErrorCode::ToolMissing,
"PATH is unset; cannot locate the Node oracle",
)
})?;
for dir in env::split_paths(&path) {
if dir.as_os_str().is_empty() {
continue;
}
let candidate = dir.join("node");
if is_executable_file(&candidate) {
return Ok(candidate);
}
}
Err(VerificationError::new(
ErrorCode::ToolMissing,
"cannot locate `node` on PATH",
))
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
fs::metadata(path)
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
fn normalized_env() -> Vec<(String, String)> {
NORMALIZED_ENV
.iter()
.map(|entry| {
let (key, value) = entry
.split_once('=')
.expect("normalized env entry is KEY=VALUE");
(key.to_owned(), value.to_owned())
})
.collect()
}
fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T> {
let bytes = fs::read(path).map_err(|error| io_error(path, &error))?;
let text = std::str::from_utf8(&bytes).map_err(|_| {
VerificationError::new(
ErrorCode::Toml,
format!("{}: TOML must be UTF-8", path.display()),
)
})?;
toml::from_str(text).map_err(|error| {
VerificationError::new(ErrorCode::Toml, format!("{}: {error}", path.display()))
})
}
fn require_schema(path: &Path, schema: u32) -> Result<()> {
if schema == SCHEMA_VERSION {
Ok(())
} else {
Err(schema_error(
path,
format!("schema must be {SCHEMA_VERSION}, found {schema}"),
))
}
}
fn require_match(path: &Path, field: &str, actual: &str, expected: &str) -> Result<()> {
if actual == expected {
Ok(())
} else {
Err(schema_error(
path,
format!("spec {field} `{actual}` does not match manifest `{expected}`"),
))
}
}
fn require_nonempty(path: &Path, field: &str, value: &str) -> Result<()> {
if value.trim().is_empty() {
Err(schema_error(path, format!("{field} must be nonempty")))
} else {
Ok(())
}
}
fn require_commit(path: &Path, id: &str, commit: &str) -> Result<()> {
if is_lower_hex(commit, COMMIT_LEN) {
Ok(())
} else {
Err(schema_error(
path,
format!(
"project `{id}` commit must be a {COMMIT_LEN}-char lowercase hex pin, found `{commit}`"
),
))
}
}
fn require_ts_entrypoint(path: &Path, id: &str, entrypoint: &str) -> Result<()> {
let is_ts = Path::new(entrypoint)
.extension()
.and_then(|extension| extension.to_str())
== Some("ts");
if is_ts {
Ok(())
} else {
Err(schema_error(
path,
format!("project `{id}` entrypoint must be a `.ts` file, found `{entrypoint}`"),
))
}
}
fn require_clean_relative(path: &Path, kind: &str, value: &str) -> Result<()> {
if value.is_empty() {
return Err(schema_error(path, format!("{kind} path must not be empty")));
}
let candidate = Path::new(value);
let clean = !candidate.is_absolute()
&& candidate
.components()
.all(|component| matches!(component, Component::Normal(_)));
if clean {
Ok(())
} else {
Err(schema_error(
path,
format!("{kind} path `{value}` must be a clean relative path"),
))
}
}
fn require_unique_nonempty(path: &Path, field: &str, values: &[String]) -> Result<()> {
if values.is_empty() {
return Err(schema_error(path, format!("{field} must not be empty")));
}
let mut seen = BTreeSet::new();
for value in values {
if value.trim().is_empty() {
return Err(schema_error(
path,
format!("{field} must not contain empty entries"),
));
}
if !seen.insert(value.as_str()) {
return Err(schema_error(
path,
format!("{field} has duplicate entry `{value}`"),
));
}
}
Ok(())
}
fn require_exact_set(path: &Path, kind: &str, actual: &[String], expected: &[&str]) -> Result<()> {
let expected_set: BTreeSet<&str> = expected.iter().copied().collect();
let mut actual_set = BTreeSet::new();
for value in actual {
if !actual_set.insert(value.as_str()) {
return Err(schema_error(
path,
format!("{kind} has duplicate entry `{value}`"),
));
}
}
if actual_set == expected_set {
Ok(())
} else {
Err(VerificationError::new(
ErrorCode::SetMismatch,
format!(
"{}: {kind}: {}",
path.display(),
set_difference(&expected_set, &actual_set)
),
))
}
}
fn set_difference(expected: &BTreeSet<&str>, actual: &BTreeSet<&str>) -> String {
let missing: Vec<&str> = expected.difference(actual).copied().collect();
let extra: Vec<&str> = actual.difference(expected).copied().collect();
format!(
"missing [{}]; extra [{}]",
missing.join(", "),
extra.join(", ")
)
}
fn require_dir(root: &Path, spec_path: &Path, relative: &str) -> Result<()> {
let path = root.join(relative);
let metadata = fs::symlink_metadata(&path).map_err(|error| io_error(&path, &error))?;
if metadata.file_type().is_dir() {
Ok(())
} else {
Err(schema_error(
spec_path,
format!("`{relative}` must be a directory"),
))
}
}
fn require_file(root: &Path, spec_path: &Path, relative: &str) -> Result<()> {
let path = root.join(relative);
let metadata = fs::symlink_metadata(&path).map_err(|error| io_error(&path, &error))?;
let file_type = metadata.file_type();
if file_type.is_symlink() || !file_type.is_file() {
return Err(schema_error(
spec_path,
format!("`{relative}` must be a regular file"),
));
}
Ok(())
}
fn is_lower_hex(value: &str, length: usize) -> bool {
value.len() == length
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
}
fn spawn_error(label: &str, program: &Path, error: &std::io::Error) -> VerificationError {
let code = if error.kind() == ErrorKind::NotFound {
ErrorCode::ToolMissing
} else {
ErrorCode::Io
};
VerificationError::new(
code,
format!("cannot spawn {label} `{}`: {error}", program.display()),
)
}
fn pipe_error(label: &str, stream: &str) -> VerificationError {
VerificationError::new(
ErrorCode::Io,
format!("{label} {stream} pipe was not captured"),
)
}
fn wait_error(label: &str, error: std::io::Error) -> VerificationError {
VerificationError::new(ErrorCode::Io, format!("cannot wait on {label}: {error}"))
}
fn thread_error(label: &str, stream: &str) -> VerificationError {
VerificationError::new(
ErrorCode::ToolFailed,
format!("{label} {stream} reader thread panicked"),
)
}
fn io_error(path: &Path, error: &std::io::Error) -> VerificationError {
VerificationError::new(ErrorCode::Io, format!("{}: {error}", path.display()))
}
fn schema_error(path: &Path, detail: impl Into<String>) -> VerificationError {
VerificationError::new(
ErrorCode::Schema,
format!("{}: {}", path.display(), detail.into()),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(2)
.expect("workspace root is two levels above the crate")
.to_path_buf()
}
fn scratch(tag: &str) -> PathBuf {
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = env::temp_dir().join(format!(
"bamts-corpus-{tag}-{}-{unique}",
std::process::id()
));
fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
fn run_script(cwd: &Path, source: &str, limits: OracleLimits) -> OracleOutcome {
let node = locate_node().expect("node on PATH");
fs::write(cwd.join("case.js"), source).expect("write script");
run_node(
&node,
cwd,
&normalized_env(),
&[OsString::from("case.js")],
&limits,
)
.expect("oracle run")
}
#[test]
fn load_corpus_accepts_the_real_repository() {
let corpus = load_corpus(&repo_root()).expect("real corpus validates");
assert_eq!(corpus.cases.len(), corpus.manifest.projects.len());
assert!(corpus.cases.len() >= 20);
assert_eq!(corpus.manifest.node_version, NODE_VERSION);
for (project, case) in corpus.manifest.projects.iter().zip(&corpus.cases) {
assert_eq!(project.id, case.id);
assert_eq!(project.commit, case.commit);
assert!(is_lower_hex(&case.commit, COMMIT_LEN));
assert!(case.entrypoint.ends_with(".ts"));
}
assert!(corpus.case("tiny-invariant").is_some());
}
#[test]
fn exact_layout_rejects_orphan_and_missing_files() {
let manifest = CorpusManifest {
node_version: NODE_VERSION.to_owned(),
environment: NORMALIZED_ENV.iter().map(|s| s.to_string()).collect(),
compare: COMPARE_KEYS.iter().map(|s| s.to_string()).collect(),
projects: vec![ManifestProject {
id: "only".into(),
repository: "https://example.com/only".into(),
commit: "a".repeat(40),
spec: "corpus/specs/only.toml".into(),
entrypoint: "corpus/cases/only.ts".into(),
}],
};
let root = scratch("layout");
fs::create_dir_all(root.join("corpus/specs")).unwrap();
fs::create_dir_all(root.join("corpus/cases")).unwrap();
fs::write(root.join("corpus/specs/only.toml"), "x").unwrap();
fs::write(root.join("corpus/cases/only.ts"), "x").unwrap();
assert!(verify_exact_layout(&root, &manifest).is_ok());
fs::write(root.join("corpus/specs/orphan.toml"), "x").unwrap();
assert_eq!(
verify_exact_layout(&root, &manifest).unwrap_err().code(),
ErrorCode::SetMismatch
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn manifest_rejects_unknown_field() {
let manifest = format!(
"schema = 1\nnode_version = \"{NODE_VERSION}\"\nenvironment = {env:?}\ncompare = {cmp:?}\nrogue = true\n[[projects]]\nid=\"a\"\nrepository=\"r\"\ncommit=\"{c}\"\nspec=\"corpus/specs/a.toml\"\nentrypoint=\"corpus/cases/a.ts\"\n",
env = NORMALIZED_ENV,
cmp = COMPARE_KEYS,
c = "a".repeat(40),
);
let err = toml::from_str::<RawManifest>(&manifest)
.err()
.expect("unknown field rejected");
assert!(err.to_string().contains("rogue"), "{err}");
}
#[test]
fn manifest_rejects_wrong_schema_and_node_version() {
let base = |schema: u32, version: &str| RawManifest {
schema,
node_version: version.to_owned(),
environment: NORMALIZED_ENV.iter().map(|s| s.to_string()).collect(),
compare: COMPARE_KEYS.iter().map(|s| s.to_string()).collect(),
projects: vec![RawProject {
id: "a".into(),
repository: "r".into(),
commit: "a".repeat(40),
spec: "corpus/specs/a.toml".into(),
entrypoint: "corpus/cases/a.ts".into(),
}],
};
let path = Path::new("manifest.toml");
assert_eq!(
validate_manifest(path, base(2, NODE_VERSION))
.unwrap_err()
.code(),
ErrorCode::Schema
);
assert_eq!(
validate_manifest(path, base(1, "20.0.0"))
.unwrap_err()
.code(),
ErrorCode::Schema
);
}
#[test]
fn manifest_rejects_environment_and_compare_mismatch() {
let make = |environment: Vec<String>, compare: Vec<String>| RawManifest {
schema: 1,
node_version: NODE_VERSION.to_owned(),
environment,
compare,
projects: vec![RawProject {
id: "a".into(),
repository: "r".into(),
commit: "a".repeat(40),
spec: "corpus/specs/a.toml".into(),
entrypoint: "corpus/cases/a.ts".into(),
}],
};
let path = Path::new("manifest.toml");
let canon_env: Vec<String> = NORMALIZED_ENV.iter().map(|s| s.to_string()).collect();
let canon_cmp: Vec<String> = COMPARE_KEYS.iter().map(|s| s.to_string()).collect();
let short_env = canon_env[..3].to_vec();
assert_eq!(
validate_manifest(path, make(short_env, canon_cmp.clone()))
.unwrap_err()
.code(),
ErrorCode::SetMismatch
);
let mut long_env = canon_env.clone();
long_env.push("EXTRA=1".into());
assert_eq!(
validate_manifest(path, make(long_env, canon_cmp.clone()))
.unwrap_err()
.code(),
ErrorCode::SetMismatch
);
let mut dup_env = canon_env.clone();
dup_env[3] = dup_env[0].clone();
assert_eq!(
validate_manifest(path, make(dup_env, canon_cmp.clone()))
.unwrap_err()
.code(),
ErrorCode::Schema
);
assert_eq!(
validate_manifest(
path,
make(canon_env, vec!["stdout".into(), "stderr".into()])
)
.unwrap_err()
.code(),
ErrorCode::SetMismatch
);
}
#[test]
fn manifest_rejects_bad_pins_and_paths_and_duplicates() {
let project = |id: &str, commit: &str, spec: &str, entry: &str| RawProject {
id: id.into(),
repository: "r".into(),
commit: commit.into(),
spec: spec.into(),
entrypoint: entry.into(),
};
let make = |projects: Vec<RawProject>| RawManifest {
schema: 1,
node_version: NODE_VERSION.to_owned(),
environment: NORMALIZED_ENV.iter().map(|s| s.to_string()).collect(),
compare: COMPARE_KEYS.iter().map(|s| s.to_string()).collect(),
projects,
};
let path = Path::new("manifest.toml");
let good = "a".repeat(40);
assert_eq!(
validate_manifest(
path,
make(vec![project(
"a",
&"a".repeat(39),
"corpus/specs/a.toml",
"corpus/cases/a.ts"
)])
)
.unwrap_err()
.code(),
ErrorCode::Schema
);
assert_eq!(
validate_manifest(
path,
make(vec![project(
"a",
&"A".repeat(40),
"corpus/specs/a.toml",
"corpus/cases/a.ts"
)])
)
.unwrap_err()
.code(),
ErrorCode::Schema
);
assert_eq!(
validate_manifest(
path,
make(vec![project(
"a",
&good,
"../evil.toml",
"corpus/cases/a.ts"
)])
)
.unwrap_err()
.code(),
ErrorCode::Schema
);
assert_eq!(
validate_manifest(
path,
make(vec![project(
"a",
&good,
"corpus/specs/a.toml",
"corpus/cases/a.js"
)])
)
.unwrap_err()
.code(),
ErrorCode::Schema
);
assert_eq!(
validate_manifest(
path,
make(vec![
project("a", &good, "corpus/specs/a.toml", "corpus/cases/a.ts"),
project("a", &good, "corpus/specs/b.toml", "corpus/cases/b.ts"),
])
)
.unwrap_err()
.code(),
ErrorCode::Schema
);
}
fn valid_project() -> ManifestProject {
ManifestProject {
id: "sample".into(),
repository: "https://example.com/sample".into(),
commit: "a".repeat(40),
spec: "corpus/specs/sample.toml".into(),
entrypoint: "corpus/cases/sample.ts".into(),
}
}
fn valid_raw_spec() -> RawSpec {
RawSpec {
schema: 1,
id: "sample".into(),
repository: "https://example.com/sample".into(),
commit: "a".repeat(40),
license: "MIT".into(),
source_dir: "corpus/projects/sample".into(),
entrypoint: "corpus/cases/sample.ts".into(),
node_args: vec![],
expected_timeout_ms: 5000,
constructs: vec!["one".into(), "two".into()],
source_files: vec!["corpus/projects/sample/index.ts".into()],
}
}
#[test]
fn spec_cross_checks_against_project() {
let path = Path::new("spec.toml");
let project = valid_project();
assert!(validate_spec(path, valid_raw_spec(), &project).is_ok());
let mut raw = valid_raw_spec();
raw.id = "other".into();
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.commit = "b".repeat(40);
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.entrypoint = "corpus/cases/other.ts".into();
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
}
#[test]
fn spec_rejects_boundary_values() {
let path = Path::new("spec.toml");
let project = valid_project();
let mut raw = valid_raw_spec();
raw.expected_timeout_ms = 0;
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.expected_timeout_ms = MAX_TIMEOUT_MS + 1;
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.constructs = vec![];
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.constructs = vec!["dup".into(), "dup".into()];
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.source_files = vec![];
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.source_files = vec!["../evil".into()];
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
let mut raw = valid_raw_spec();
raw.node_args = vec![String::new()];
assert_eq!(
validate_spec(path, raw, &project).unwrap_err().code(),
ErrorCode::Schema
);
}
#[test]
fn verify_node_version_accepts_pinned_and_rejects_missing() {
let node = locate_node().expect("node on PATH");
verify_node_version(&node).expect("pinned node verifies");
let err = verify_node_version(Path::new("/nonexistent/definitely-not-node")).unwrap_err();
assert_eq!(err.code(), ErrorCode::ToolMissing);
}
#[test]
fn oracle_captures_raw_stdout_and_exact_exit_code() {
let dir = scratch("stdout");
let outcome = run_script(
&dir,
"process.stdout.write('hello');process.exit(3);",
OracleLimits {
timeout: Duration::from_secs(10),
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
},
);
assert!(!outcome.timed_out);
assert_eq!(outcome.stdout, b"hello");
assert_eq!(outcome.exit_code, Some(3));
assert_eq!(outcome.parity_key(), (b"hello".as_slice(), Some(3)));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn oracle_normalizes_environment() {
let dir = scratch("env");
let outcome = run_script(
&dir,
"process.stdout.write([process.env.TZ,process.env.NO_COLOR,process.env.PATH].map(String).join(','));",
OracleLimits {
timeout: Duration::from_secs(10),
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
},
);
assert_eq!(outcome.stdout, b"UTC,1,undefined");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn oracle_bounds_output_without_deadlock() {
let dir = scratch("bound");
let outcome = run_script(
&dir,
"process.stdout.write('x'.repeat(100000));",
OracleLimits {
timeout: Duration::from_secs(10),
max_output_bytes: 1000,
},
);
assert!(!outcome.timed_out);
assert_eq!(outcome.exit_code, Some(0));
assert_eq!(outcome.stdout.len(), 1000);
assert!(outcome.stdout_truncated);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn oracle_enforces_timeout() {
let dir = scratch("timeout");
let outcome = run_script(
&dir,
"while(true){}",
OracleLimits {
timeout: Duration::from_millis(200),
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
},
);
assert!(outcome.timed_out);
assert_eq!(outcome.exit_code, None);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn aot_executable_uses_the_full_case_timeout() {
let spec = CaseSpec {
id: "aot-budget".into(),
repository: "https://example.com/aot-budget".into(),
commit: "a".repeat(40),
license: "MIT".into(),
source_dir: "corpus/projects/aot-budget".into(),
entrypoint: "corpus/cases/aot-budget.ts".into(),
node_args: Vec::new(),
expected_timeout_ms: 250,
constructs: Vec::new(),
source_files: Vec::new(),
};
let limits = aot_execution_limits(&spec, 123);
assert_eq!(limits.timeout, Duration::from_millis(250));
assert_eq!(limits.max_output_bytes, 123);
}
#[test]
fn aot_compile_evidence_cannot_fake_a_successful_timeout_exit() {
let runtime = OracleOutcome {
timed_out: true,
exit_code: None,
signal: Some(9),
stdout: b"runtime stdout".to_vec(),
stdout_truncated: false,
stderr: b"runtime stderr".to_vec(),
stderr_truncated: false,
compile_stderr: Vec::new(),
compile_stderr_truncated: false,
};
let outcome = with_aot_compile_evidence(runtime, b"compile warning".to_vec(), 128);
assert!(outcome.timed_out);
assert_eq!(outcome.exit_code, None);
assert_eq!(outcome.stdout, b"runtime stdout");
assert_eq!(outcome.stderr, b"runtime stderr");
assert_eq!(outcome.compile_stderr, b"compile warning");
}
#[test]
fn oracle_treats_stderr_as_evidence_not_parity() {
let dir = scratch("stderr");
let outcome = run_script(
&dir,
"process.stderr.write('warning');process.stdout.write('ok');",
OracleLimits {
timeout: Duration::from_secs(10),
max_output_bytes: DEFAULT_MAX_OUTPUT_BYTES,
},
);
assert_eq!(outcome.stdout, b"ok");
assert_eq!(outcome.stderr, b"warning");
assert_eq!(outcome.parity_key().0, b"ok");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn parity_never_affirms_unreliable_outcomes() {
let reliable = |stdout: &[u8], code: i32| OracleOutcome {
timed_out: false,
exit_code: Some(code),
signal: None,
stdout: stdout.to_vec(),
stdout_truncated: false,
stderr: Vec::new(),
stderr_truncated: false,
compile_stderr: Vec::new(),
compile_stderr_truncated: false,
};
assert!(reliable(b"same", 0).parity_matches(&reliable(b"same", 0)));
assert!(!reliable(b"a", 0).parity_matches(&reliable(b"b", 0)));
assert!(!reliable(b"same", 0).parity_matches(&reliable(b"same", 1)));
let killed = OracleOutcome {
timed_out: true,
exit_code: None,
signal: Some(9),
stdout: Vec::new(),
stdout_truncated: false,
stderr: Vec::new(),
stderr_truncated: false,
compile_stderr: Vec::new(),
compile_stderr_truncated: false,
};
assert!(!killed.is_reliable());
assert!(!killed.parity_matches(&killed));
let truncated = OracleOutcome {
timed_out: false,
exit_code: Some(0),
signal: None,
stdout: b"prefix".to_vec(),
stdout_truncated: true,
stderr: Vec::new(),
stderr_truncated: false,
compile_stderr: Vec::new(),
compile_stderr_truncated: false,
};
assert!(!truncated.is_reliable());
assert!(!truncated.parity_matches(&truncated));
assert!(!truncated.parity_matches(&reliable(b"prefix", 0)));
}
#[test]
fn oracle_runs_a_real_corpus_case() {
let root = repo_root();
let corpus = load_corpus(&root).expect("corpus validates");
let oracle = NodeOracle::discover(&root).expect("discover pinned node");
let case = corpus.case("tiny-invariant").expect("case present");
let outcome = oracle.run_case(case).expect("case runs");
assert!(!outcome.timed_out);
assert_eq!(outcome.exit_code, Some(0));
assert!(
outcome.stdout.windows(6).any(|window| window == b"truthy"),
"stdout should contain project-derived output"
);
}
}