264 lines
12 KiB
Rust
264 lines
12 KiB
Rust
use kf_contracts::judging_versions as versions;
|
|
use kf_contracts::{judging as v1, judging_v2 as v2};
|
|
use serde::{Serialize, de::DeserializeOwned};
|
|
use std::{fs, path::PathBuf};
|
|
|
|
fn fixtures() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../tests/fixtures/contracts-v2")
|
|
}
|
|
fn fixture<T: DeserializeOwned>(name: &str) -> T {
|
|
v1::decode_wire(&fs::read(fixtures().join(format!("{name}.json"))).unwrap()).unwrap()
|
|
}
|
|
fn roundtrip<T: DeserializeOwned + Serialize>(data: &[u8], invalid: bool, name: &str) {
|
|
let result = v1::decode_wire::<T>(data);
|
|
if invalid {
|
|
assert!(result.is_err(), "must reject {name}");
|
|
} else {
|
|
assert_eq!(
|
|
serde_json::to_value(result.unwrap_or_else(|e| panic!("{name}: {e}"))).unwrap(),
|
|
serde_json::from_slice::<serde_json::Value>(data).unwrap(),
|
|
"roundtrip {name}"
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn shared_v2_acceptance_rejection_and_canonical_bytes_match_python() {
|
|
let mut count = 0;
|
|
for entry in fs::read_dir(fixtures()).unwrap() {
|
|
let path = entry.unwrap().path();
|
|
if path.extension().is_none_or(|e| e != "json") {
|
|
continue;
|
|
}
|
|
let name = path.file_name().unwrap().to_string_lossy();
|
|
let data = fs::read(&path).unwrap();
|
|
let invalid = name.contains(".invalid-");
|
|
match name.split('.').next().unwrap() {
|
|
"judge-manifest" => roundtrip::<v2::JobManifest>(&data, invalid, &name),
|
|
"judge-lease" => roundtrip::<v2::LeaseGrant>(&data, invalid, &name),
|
|
"judge-stage" => roundtrip::<v2::StageReport>(&data, invalid, &name),
|
|
"judge-result" => roundtrip::<v2::JudgementReport>(&data, invalid, &name),
|
|
"judge-admission" => roundtrip::<v2::AdmissionPolicy>(&data, invalid, &name),
|
|
"judge-registry" => roundtrip::<v2::AdmissionRegistry>(&data, invalid, &name),
|
|
"judge-stopped" => roundtrip::<v2::StoppedReport>(&data, invalid, &name),
|
|
_ => panic!("unrecognized fixture {name}"),
|
|
}
|
|
count += 1;
|
|
}
|
|
assert!(count >= 53);
|
|
for language in ["c17", "cpp20", "python313", "java21"] {
|
|
let manifest: v2::JobManifest = fixture(&format!("judge-manifest.valid-{language}"));
|
|
assert_eq!(
|
|
v2::manifest_bytes(&manifest).unwrap(),
|
|
fs::read(fixtures().join(format!("canonical-manifest.{language}.txt"))).unwrap()
|
|
);
|
|
let data =
|
|
fs::read(fixtures().join(format!("judge-manifest.valid-{language}.json"))).unwrap();
|
|
assert!(v1::decode_wire::<v1::JobManifest>(&data).is_err());
|
|
}
|
|
}
|
|
#[test]
|
|
fn runtime_identity_admission_and_program_binding_are_exact() {
|
|
let policy: v2::AdmissionPolicy = fixture("judge-admission.valid");
|
|
for language in ["c17", "cpp20", "python313", "java21"] {
|
|
let manifest: v2::JobManifest = fixture(&format!("judge-manifest.valid-{language}"));
|
|
for attempt in [1, 2, 3] {
|
|
policy.admit(&manifest, attempt).unwrap();
|
|
}
|
|
for attempt in [0, 4] {
|
|
assert!(policy.admit(&manifest, attempt).is_err());
|
|
}
|
|
let mut wrong = manifest.clone();
|
|
wrong.toolchain.image_revision += 1;
|
|
assert!(policy.admit(&wrong, 1).is_err());
|
|
wrong = manifest.clone();
|
|
wrong.toolchain.image_digest = "f".repeat(64).try_into().unwrap();
|
|
assert!(policy.admit(&wrong, 1).is_err());
|
|
let digest: v1::Digest =
|
|
fs::read_to_string(fixtures().join(format!("canonical-manifest.{language}.sha256")))
|
|
.unwrap()
|
|
.trim()
|
|
.to_owned()
|
|
.try_into()
|
|
.unwrap();
|
|
let result: v2::JudgementReport = fixture(&format!("judge-result.valid-{language}"));
|
|
result.validate_for_manifest(&manifest, &digest).unwrap();
|
|
let mut wrong = manifest.clone();
|
|
wrong.source.bytes += 1;
|
|
assert!(result.validate_for_manifest(&wrong, &digest).is_err());
|
|
let stage: v2::StageReport = fixture(&format!("judge-stage.valid-{language}"));
|
|
stage.validate_for_manifest(&manifest).unwrap();
|
|
}
|
|
let mut restricted = policy;
|
|
restricted
|
|
.capabilities
|
|
.retain(|c| c.language != v2::Language::Python313);
|
|
let manifest: v2::JobManifest = fixture("judge-manifest.valid-python313");
|
|
assert!(restricted.admit(&manifest, 1).is_err());
|
|
}
|
|
#[test]
|
|
fn runtime_entry_profiles_and_jvm_nonheap_memory_are_fixed() {
|
|
let python = v2::Language::Python313.contract();
|
|
assert_eq!(python.source_name, "main.py");
|
|
assert_eq!(python.preparation_profile, "python_check");
|
|
assert_eq!(python.preparation_kind, v2::PreparationKind::SyntaxCheck);
|
|
let java = v2::Language::Java21.contract();
|
|
assert_eq!(java.source_name, "Main.java");
|
|
assert_eq!(java.run_profile, "java_run");
|
|
let manifest: v2::JobManifest = fixture("judge-manifest.valid-java21");
|
|
let heap = v2::jvm_heap_bytes(&manifest.run_limits).unwrap();
|
|
assert!(heap >= 32 * v1::MIB && heap < manifest.run_limits.memory_bytes);
|
|
let mut more = manifest.run_limits.clone();
|
|
more.processes = 32;
|
|
assert!(v2::jvm_heap_bytes(&more).unwrap() < heap);
|
|
more.memory_bytes = 128 * v1::MIB;
|
|
assert!(v2::jvm_heap_bytes(&more).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn storage_boundary_retains_version_identity_and_rejects_mismatched_results() {
|
|
let original = fixtures().parent().unwrap().join("contracts");
|
|
let old_manifest: versions::JobManifest =
|
|
v1::decode_wire(&fs::read(original.join("judge-manifest.valid.json")).unwrap()).unwrap();
|
|
let old_report: versions::JudgementReport =
|
|
v1::decode_wire(&fs::read(original.join("judge-result.valid.json")).unwrap()).unwrap();
|
|
let manifest: versions::JobManifest = fixture("judge-manifest.valid-python313");
|
|
let report: versions::JudgementReport = fixture("judge-result.valid-python313");
|
|
let digest: v1::Digest = match &old_report {
|
|
versions::JudgementReport::V1(r) => r.manifest_digest.clone(),
|
|
_ => unreachable!(),
|
|
};
|
|
old_report
|
|
.validate_for_manifest(&old_manifest, &digest)
|
|
.unwrap();
|
|
let new_digest: v1::Digest =
|
|
fs::read_to_string(fixtures().join("canonical-manifest.python313.sha256"))
|
|
.unwrap()
|
|
.trim()
|
|
.to_owned()
|
|
.try_into()
|
|
.unwrap();
|
|
report
|
|
.validate_for_manifest(&manifest, &new_digest)
|
|
.unwrap();
|
|
assert!(
|
|
old_report
|
|
.validate_for_manifest(&manifest, &digest)
|
|
.is_err()
|
|
);
|
|
assert!(
|
|
report
|
|
.validate_for_manifest(&old_manifest, &digest)
|
|
.is_err()
|
|
);
|
|
assert_eq!(
|
|
old_manifest.canonical_bytes().unwrap(),
|
|
fs::read(original.join("canonical-manifest.v1.txt")).unwrap()
|
|
);
|
|
let data = fs::read(fixtures().join("judge-manifest.valid-python313.json")).unwrap();
|
|
let text = String::from_utf8(data).unwrap();
|
|
for replacement in [
|
|
"\"schema_version\": 3",
|
|
"\"schema_version\": true",
|
|
"\"schema_version\": 2, \"schema_version\": 2",
|
|
] {
|
|
let invalid = text.replace("\"schema_version\": 2", replacement);
|
|
assert!(v1::decode_wire::<versions::JobManifest>(invalid.as_bytes()).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn registry_explicitly_admits_both_protocols_without_promoting_legacy_policy() {
|
|
let registry: v2::AdmissionRegistry = fixture("judge-registry.valid");
|
|
let original = fixtures().parent().unwrap().join("contracts");
|
|
let old: versions::JobManifest =
|
|
v1::decode_wire(&fs::read(original.join("judge-manifest.valid.json")).unwrap()).unwrap();
|
|
registry.admit(&old, 1).unwrap();
|
|
for language in ["c17", "cpp20", "python313", "java21"] {
|
|
let manifest: versions::JobManifest = fixture(&format!("judge-manifest.valid-{language}"));
|
|
registry.admit(&manifest, 1).unwrap();
|
|
let mut restricted = registry.clone();
|
|
restricted.multi_runtime = None;
|
|
assert!(restricted.admit(&manifest, 1).is_err());
|
|
restricted = registry.clone();
|
|
restricted.legacy = None;
|
|
assert!(restricted.admit(&old, 1).is_err());
|
|
}
|
|
let mut wrong = registry;
|
|
wrong.multi_runtime.as_mut().unwrap().max_retries += 1;
|
|
assert!(wrong.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn operator_admission_and_transport_dispatch_keep_generations_explicit() {
|
|
let original = fixtures().parent().unwrap().join("contracts");
|
|
let legacy_policy: versions::NodeAdmission =
|
|
v1::decode_wire(&fs::read(original.join("judge-admission.valid.json")).unwrap()).unwrap();
|
|
let multi_policy: versions::NodeAdmission = fixture("judge-admission.valid");
|
|
let registry: versions::NodeAdmission = fixture("judge-registry.valid");
|
|
let old_manifest: versions::JobManifest =
|
|
v1::decode_wire(&fs::read(original.join("judge-manifest.valid.json")).unwrap()).unwrap();
|
|
let new_manifest: versions::JobManifest = fixture("judge-manifest.valid-python313");
|
|
for policy in [&legacy_policy, &multi_policy, ®istry] {
|
|
policy.validate().unwrap();
|
|
assert_eq!(policy.max_concurrent_jobs(), 1);
|
|
assert_eq!(policy.prefetch(), 1);
|
|
assert_eq!(policy.max_retries(), 2);
|
|
}
|
|
registry.admit(&old_manifest, 1).unwrap();
|
|
registry.admit(&new_manifest, 1).unwrap();
|
|
assert!(legacy_policy.admit(&new_manifest, 1).is_err());
|
|
assert!(multi_policy.admit(&old_manifest, 1).is_err());
|
|
let old_stage: versions::StageReport =
|
|
v1::decode_wire(&fs::read(original.join("judge-stage.valid.json")).unwrap()).unwrap();
|
|
let new_stage: versions::StageReport = fixture("judge-stage.valid-python313");
|
|
old_stage.validate_for_manifest(&old_manifest).unwrap();
|
|
new_stage.validate_for_manifest(&new_manifest).unwrap();
|
|
assert!(old_stage.validate_for_manifest(&new_manifest).is_err());
|
|
assert!(new_stage.validate_for_manifest(&old_manifest).is_err());
|
|
let old_stop: versions::StoppedReport =
|
|
v1::decode_wire(&fs::read(original.join("judge-stopped.valid.json")).unwrap()).unwrap();
|
|
let new_stop: versions::StoppedReport = fixture("judge-stopped.valid");
|
|
assert!(old_stop.validate_for_manifest(&new_manifest).is_err());
|
|
assert!(new_stop.validate_for_manifest(&old_manifest).is_err());
|
|
for manifest in [&old_manifest, &new_manifest] {
|
|
let legacy_grant: v1::LeaseGrant =
|
|
v1::decode_wire(&fs::read(original.join("judge-lease.valid.json")).unwrap()).unwrap();
|
|
let grant = versions::LeaseGrant::for_manifest(
|
|
manifest,
|
|
legacy_grant.binding,
|
|
legacy_grant.lease_token,
|
|
legacy_grant.attempt_number,
|
|
legacy_grant.expires_at_unix_ms,
|
|
legacy_grant.manifest_digest,
|
|
);
|
|
let value = serde_json::to_value(&grant).unwrap();
|
|
assert_eq!(
|
|
value["schema_version"].as_u64().unwrap(),
|
|
u64::from(manifest.version())
|
|
);
|
|
v1::decode_wire::<versions::LeaseGrant>(&serde_json::to_vec(&value).unwrap()).unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn typed_compiler_output_checks_individual_files_and_collection_storage() {
|
|
let report: versions::JudgementReport = fixture("judge-result.valid-java21");
|
|
let mut manifest: versions::JobManifest = fixture("judge-manifest.valid-java21");
|
|
report.validate_program_budget(&manifest).unwrap();
|
|
let versions::JobManifest::V2(m) = &mut manifest else {
|
|
unreachable!()
|
|
};
|
|
m.compile_limits.stdout_bytes = 32;
|
|
m.compile_limits.stderr_bytes = 32;
|
|
m.compile_limits.file_bytes = 63;
|
|
m.validate().unwrap();
|
|
assert!(report.validate_program_budget(&manifest).is_err());
|
|
let versions::JobManifest::V2(m) = &mut manifest else {
|
|
unreachable!()
|
|
};
|
|
m.compile_limits.file_bytes = 64;
|
|
m.compile_limits.workdir_bytes = 128;
|
|
m.validate().unwrap();
|
|
assert!(report.validate_program_budget(&manifest).is_err());
|
|
}
|