feat: release Dever 0.1.4 with enforceable project guidance

This commit is contained in:
Antigravity committed 2026-10-09 05:33:57 +00:00
1 parent 201bdee372
commit 89629c2197
86 files changed
+2798 -338

No files matched your search

+182
View File
@@ -151,6 +151,12 @@ fn application_check_rejects_redundancy_before_preparing_build_resources() {
] {
assert!(diagnostic.contains(expected), "{expected}: {diagnostic}");
}
assert_eq!(diagnostic.matches("error[C016]").count(), 3, "{diagnostic}");
assert!(!diagnostic.contains("warning[W005]"), "{diagnostic}");
assert!(
diagnostic.contains("sample/profile/domain.dever:1:1: note:"),
"{diagnostic}"
);
// The same source fails before native packs, output publication or execution.
let executable = root.join("must-not-build");
for (action, args) in [
@@ -166,6 +172,120 @@ fn application_check_rejects_redundancy_before_preparing_build_resources() {
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_rejects_proven_duplicates_before_runtime_or_database_access() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
for (domain, app, diagnostic, location, warning) in [
(
"first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }",
"total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
"same implementation and contract",
"sample/count/domain.dever:2:1: error[C016]",
"warning[W003]",
),
(
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }",
"earlier result is still available",
"sample/count/app.dever:2:",
"warning[W004]",
),
] {
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(root, "module/sample/count/model.dever", "type Count { value: Int }");
write(root, "module/sample/count/domain.dever", domain);
write(root, "module/sample/count/app.dever", app);
write(root, "module/sample/count/api.dever", "cmd total = app.total");
let checked = cli(&compiler, root, "check", &[]);
for expected in ["C016", diagnostic, location, "previous implementation or computation"] {
failure(&checked, expected);
}
let stderr = String::from_utf8_lossy(&checked.stderr);
assert_eq!(stderr.matches("error[C016]").count(), 1, "{stderr}");
assert!(!stderr.contains(warning), "{stderr}");
assert_eq!(
stderr.matches("note: previous implementation or computation").count(),
1,
"{stderr}"
);
let executable = root.join("must-not-build");
for (action, arguments) in [
("run", vec!["--", "sample.count.total", "{\"value\":1}"]),
("build", vec!["--output", executable.to_str().unwrap()]),
] {
let rejected = cli(&compiler, root, action, &arguments);
assert!(!rejected.status.success());
assert_eq!(rejected.stderr, checked.stderr);
}
let snapshot = cli(&compiler, root, "api", &[]);
success(&snapshot);
let stderr = String::from_utf8_lossy(&snapshot.stderr);
assert!(stderr.contains(warning), "{stderr}");
assert!(!stderr.contains("C016"), "{stderr}");
write(
root,
"test/sample/count/value.dever",
"value() () { assert_eq(app.total(1), 4) }",
);
let tests = cli(&compiler, root, "test", &[]);
failure(&tests, "runtime pack");
let stderr = String::from_utf8_lossy(&tests.stderr);
assert!(stderr.contains(warning), "{stderr}");
assert!(!stderr.contains("C016"), "{stderr}");
write(root, "dever.api", "invalid baseline");
let baseline = cli(&compiler, root, "check", &[]);
failure(&baseline, "invalid API baseline");
let stderr = String::from_utf8_lossy(&baseline.stderr);
assert!(stderr.contains(warning), "{stderr}");
assert!(!stderr.contains("C016"), "{stderr}");
assert!(!executable.exists());
assert!(!root.join("data").exists());
}
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_keeps_warnings_with_distinct_evidence_and_other_codes() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
write(root, "config/setting.json", "{}");
write(
root,
"module/sample/count/domain.dever",
"orphan(value: Int) (answer: Int) pure { answer = value + 1 }\nfirst(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(value: Int) (answer: Int) pure { answer = value + 1 }",
);
write(
root,
"module/sample/count/app.dever",
"total(value: Int) (answer: Int) { unused = 0\nfirst = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }",
);
write(root, "module/sample/count/api.dever", "cmd total = app.total");
let checked = cli(&compiler, root, "check", &[]);
failure(&checked, "C016");
let stderr = String::from_utf8_lossy(&checked.stderr);
assert_eq!(stderr.matches("error[C016]").count(), 2, "{stderr}");
// Warning proof includes the orphan; production proof starts at the first reachable function.
assert_eq!(stderr.matches("warning[W003]").count(), 2, "{stderr}");
assert_eq!(stderr.matches("warning[W001]").count(), 1, "{stderr}");
assert_eq!(
stderr.matches("sample/count/domain.dever:1:1: note:").count(),
2,
"{stderr}"
);
assert_eq!(
stderr.matches("sample/count/domain.dever:2:1: note:").count(),
1,
"{stderr}"
);
assert!(!root.join("data").exists());
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn application_check_rejects_plain_model_get_wrapper_without_opening_database() {
let toolchain = TemporaryDirectory::new();
@@ -303,6 +423,68 @@ fn application_check_does_not_apply_production_reachability_to_tests() {
assert!(!toolchain.path().join("cache").exists());
}
#[test]
fn test_filter_still_checks_unselected_sources_and_reports_no_match_before_compilation() {
let toolchain = TemporaryDirectory::new();
let compiler = compiler(toolchain.path(), "dever");
let project_dir = TemporaryDirectory::new();
let root = project_dir.path();
project(root);
write(root, "test/sample/echo/first.dever", "first() () { assert_eq(1, 1) }");
write(root, "test/sample/echo/second.dever", "second() () { assert_eq(1, 1) }");
for filter in ["sam", "sample.ec", "sample.echo.fir", "sample.echo.missing"] {
let result = cli(&compiler, root, "test", &["--filter", filter]);
failure(&result, "no tests match filter");
assert!(!String::from_utf8_lossy(&result.stdout).contains("runtime pack"));
}
let selected = cli(&compiler, root, "test", &["--filter", "sample.echo.second"]);
failure(&selected, "runtime pack");
let report = String::from_utf8_lossy(&selected.stdout);
assert!(report.contains("0 passed; 1 failed; 1 not run"), "{report}");
assert!(!report.contains("test sample/echo/first ..."), "{report}");
write(root, "test/sample/echo/first.dever", "first() () { missing() }");
failure(&cli(&compiler, root, "test", &["--filter", "sample.echo.second"]), "C004");
assert!(!toolchain.path().join("cache").exists());
}
#[test]
#[ignore = "requires an explicitly prepared runtime pack adjacent to CARGO_BIN_EXE_dever; owned short-lived cases only"]
fn llvm_test_controls_preserve_indices_prefix_boundaries_and_timeout_continuation() {
let compiler = Path::new(env!("CARGO_BIN_EXE_dever"));
let directory = TemporaryDirectory::new();
let root = directory.path();
project(root);
for (relative, source) in [
("test/sample/echo/alpha.dever", "alpha() () { assert_eq(1, 2) }"),
("test/sample/echo/beta.dever", "beta() () { assert_eq(7, 7) }"),
("test/sample/echo/slow.dever", "slow() () { dever.task.sleep(30000) }"),
("test/sample/echo/zeta.dever", "zeta() () { assert_eq(9, 9) }"),
("test/sample/echoes/first.dever", "first() () { assert_eq(1, 2) }"),
("test/samples/echo/first.dever", "first() () { assert_eq(1, 2) }"),
] {
write(root, relative, source);
}
let selected = cli(compiler, root, "test", &["--filter", "sample.echo.beta"]);
success(&selected);
let report = String::from_utf8_lossy(&selected.stdout);
assert!(report.contains("test sample/echo/beta ... ok"), "{report}");
assert!(report.contains("1 passed; 0 failed; 5 not run"), "{report}");
assert!(!report.contains("sample/echo/alpha"), "{report}");
for (filter, counts) in [
("sample.echo", "2 passed; 2 failed; 2 not run"),
("sample", "2 passed; 3 failed; 1 not run"),
] {
let selected = cli(compiler, root, "test", &["--filter", filter, "--timeout-ms", "250"]);
failure(&selected, counts);
let report = String::from_utf8_lossy(&selected.stdout);
assert!(report.contains("test process timed out after 250 ms"), "{report}");
assert!(report.contains("test sample/echo/zeta ... ok"), "{report}");
assert!(!report.contains("test samples/echo/first"), "{report}");
}
failure(&cli(compiler, root, "test", &["--filter", "sample.echo.bet"]), "no tests match filter");
}
#[test]
fn cross_build_requires_the_selected_pack_even_when_host_pack_exists() {
let root = TemporaryDirectory::new();
+246
View File
@@ -0,0 +1,246 @@
use super::*;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::process::Output;
struct Project {
temporary: TemporaryDirectory,
layout: Layout,
root: PathBuf,
key: Ed25519KeyPair,
}
impl Project {
fn new() -> Self {
let temporary = TemporaryDirectory::new();
let layout = Layout::new(temporary.path().join("machine"));
layout.initialize().unwrap();
let key = signing_key();
fs::write(
layout.state().join("trusted-release-key"),
format!("{}\n", hex(key.public_key().as_ref())),
)
.unwrap();
write_release(
&layout,
&key,
"1.0.0",
&temporary.path().join("dispatch"),
true,
true,
);
MachineManager::new(layout.clone())
.install_requested("1.0.0")
.unwrap();
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
let root = temporary.path().join("project");
fs::create_dir_all(root.join("module")).unwrap();
fs::create_dir(root.join("config")).unwrap();
fs::write(root.join("config/setting.json"), "{}").unwrap();
Self {
temporary,
layout,
root,
key,
}
}
fn run(&self, root: &Path, extra: &[&str]) -> Output {
Command::new(self.layout.bin().join("dever"))
.args(["skill", "install"])
.args(extra)
.current_dir(root)
.env_clear()
.output()
.unwrap()
}
fn succeeds(&self) {
let output = self.run(&self.root, &[]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("AGENTS.md"));
self.assert_no_staging();
}
fn fails(&self, message: &str) {
let output = self.run(&self.root, &[]);
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr).contains(message),
"{}",
String::from_utf8_lossy(&output.stderr)
);
self.assert_no_staging();
}
fn assert_no_staging(&self) {
assert!(fs::read_dir(&self.root).unwrap().all(|entry| {
!entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with(".dever-skill-")
}));
}
fn sign_manifest(&self, manifest: &ReleaseManifest) {
let release = self.layout.versions().join("1.0.0");
let bytes = serde_json::to_vec(manifest).unwrap();
fs::write(release.join("manifest.json"), &bytes).unwrap();
fs::write(
release.join("manifest.sig"),
hex(self.key.sign(&bytes).as_ref()),
)
.unwrap();
}
}
#[test]
fn project_skill_preserves_user_bytes_permissions_and_updates_only_its_block() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
let before = "# 项目规则\r\n保留用户内容,无末尾换行";
fs::write(&agents, before).unwrap();
fs::set_permissions(&agents, fs::Permissions::from_mode(0o640)).unwrap();
fixture.succeeds();
assert_eq!(
fs::read_to_string(&agents).unwrap(),
format!("{before}\n\n{}", project_rules("1.0.0"))
);
assert_eq!(
fs::metadata(&agents).unwrap().permissions().mode() & 0o777,
0o640
);
let installed = fs::read(&agents).unwrap();
fixture.succeeds();
assert_eq!(fs::read(&agents).unwrap(), installed);
let prefix = "# 自定义前文\r\n\r\n";
let suffix = "\r\n# 自定义后文\r\n原样保留";
fs::write(
&agents,
format!("{prefix}<!-- dever-language:start -->\n旧规则\n<!-- dever-language:end -->{suffix}"),
)
.unwrap();
fixture.succeeds();
assert_eq!(
fs::read_to_string(&agents).unwrap(),
format!("{prefix}{}{suffix}", project_rules("1.0.0").trim_end())
);
}
#[test]
fn project_skill_rejects_ambiguous_or_unsafe_agents_without_changes() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
for content in [
"<!-- dever-language:start -->",
"<!-- dever-language:end -->",
"<!-- dever-language:end --><!-- dever-language:start -->",
"<!-- dever-language:start --><!-- dever-language:start --><!-- dever-language:end -->",
"<!-- dever-language:start --><!-- dever-language:end --><!-- dever-language:start --><!-- dever-language:end -->",
] {
fs::write(&agents, content).unwrap();
fixture.fails("exactly one complete");
assert_eq!(fs::read_to_string(&agents).unwrap(), content);
}
fs::write(&agents, [0xff, 0xfe]).unwrap();
fixture.fails("UTF-8");
assert_eq!(fs::read(&agents).unwrap(), [0xff, 0xfe]);
fs::write(&agents, "do not overwrite").unwrap();
fs::set_permissions(&agents, fs::Permissions::from_mode(0o444)).unwrap();
fixture.fails("read-only");
fs::set_permissions(&agents, fs::Permissions::from_mode(0o644)).unwrap();
fs::remove_file(&agents).unwrap();
fs::create_dir(&agents).unwrap();
fixture.fails("regular file");
fs::remove_dir(&agents).unwrap();
let other = fixture.temporary.path().join("other-agents");
fs::write(&other, "untouched").unwrap();
symlink(&other, &agents).unwrap();
fixture.fails("regular file");
assert!(fs::symlink_metadata(&agents).unwrap().is_symlink());
assert_eq!(fs::read_to_string(&other).unwrap(), "untouched");
}
#[test]
fn project_skill_requires_project_root_valid_version_and_signed_template() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
let output = fixture.run(&fixture.root, &["unwanted-directory"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("accepts no arguments"));
assert!(!fixture.root.join("unwanted-directory").exists());
let output = fixture.run(fixture.temporary.path(), &[]);
assert!(!output.status.success());
assert!(!fixture.temporary.path().join("AGENTS.md").exists());
fs::remove_dir(fixture.root.join("module")).unwrap();
symlink(fixture.temporary.path(), fixture.root.join("module")).unwrap();
fixture.fails("real module/");
fs::remove_file(fixture.root.join("module")).unwrap();
fs::create_dir(fixture.root.join("module")).unwrap();
let settings = fixture.root.join("config/setting.json");
for invalid in [
"not json",
"[]",
r#"{"dever":{"version":"latest"}}"#,
r#"{"dever":{"version":"9.0.0"}}"#,
] {
fs::write(&settings, invalid).unwrap();
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert!(!agents.exists());
}
fs::remove_file(&settings).unwrap();
fixture.fails("cannot read");
fs::write(&settings, "{}").unwrap();
let template = fixture
.layout
.versions()
.join("1.0.0/skills/dever-language/assets/template/AGENTS.md");
fs::write(&template, "tampered instructions").unwrap();
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert!(!agents.exists());
fixture.assert_no_staging();
}
#[test]
fn project_skill_refuses_unsigned_extra_or_invalid_signed_template() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
fs::write(&agents, "user-owned instructions").unwrap();
let release = fixture.layout.versions().join("1.0.0");
let mut manifest: ReleaseManifest =
serde_json::from_slice(&fs::read(release.join("manifest.json")).unwrap()).unwrap();
let logical = "skills/dever-language/assets/template/AGENTS.md";
manifest.artifacts.retain(|entry| entry.path != logical);
fixture.sign_manifest(&manifest);
fixture.fails("does not declare resource");
assert_eq!(
fs::read_to_string(&agents).unwrap(),
"user-owned instructions"
);
let template = release.join(logical);
for invalid in [
"no managed block",
"<!-- dever-language:start -->missing end",
"outside\n<!-- dever-language:start -->rules<!-- dever-language:end -->",
] {
fs::write(&template, invalid).unwrap();
manifest.artifacts.retain(|entry| entry.path != logical);
manifest.artifacts.push(artifact(&template, logical));
fixture.sign_manifest(&manifest);
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert_eq!(
fs::read_to_string(&agents).unwrap(),
"user-owned instructions"
);
fixture.assert_no_staging();
}
}
+53 -25
View File
@@ -22,6 +22,10 @@ static NEXT_TEMPORARY: AtomicU64 = AtomicU64::new(0);
#[path = "shared_toolchain_extensions.rs"]
mod extensions;
#[cfg(target_os = "linux")]
#[path = "project_skill.rs"]
mod project_skill;
struct TemporaryDirectory(PathBuf);
impl TemporaryDirectory {
@@ -306,30 +310,16 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
"skill 1.0.0\n"
);
let skill_entry = temporary.path().join("dever-language");
execute_in(
&layout,
&[
"skill".into(),
"install".into(),
skill_entry.as_os_str().into(),
],
)
.unwrap();
let loader = fs::read(skill_entry.join("SKILL.md")).unwrap();
assert!(String::from_utf8_lossy(&loader).contains("skill path"));
assert!(
execute_in(
&layout,
&[
"skill".into(),
"install".into(),
skill_entry.as_os_str().into()
]
)
.is_err()
);
assert_eq!(fs::read(skill_entry.join("SKILL.md")).unwrap(), loader);
fs::copy(env!("CARGO_BIN_EXE_dever-launcher"), layout.bin().join("dever")).unwrap();
let active_project = temporary.path().join("active-project");
fs::create_dir_all(active_project.join("module")).unwrap();
fs::create_dir(active_project.join("config")).unwrap();
fs::write(active_project.join("config/setting.json"), "{}").unwrap();
install_project_skill(&layout, &active_project);
let rules = fs::read(active_project.join("AGENTS.md")).unwrap();
assert_eq!(rules, project_rules("1.0.0").as_bytes());
install_project_skill(&layout, &active_project);
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
let new_project = temporary.path().join("not-created-yet");
let CommandResult::Exited(code) = execute_in(
@@ -351,6 +341,7 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
let project = temporary.path().join("project");
fs::create_dir_all(project.join("config")).unwrap();
fs::create_dir(project.join("module")).unwrap();
fs::write(
project.join("config/setting.json"),
r#"{"dever":{"version":"2.0.0"}}"#,
@@ -368,6 +359,11 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
Path::new(&pinned_skill),
layout.versions().join("2.0.0/skills/dever-language")
);
install_project_skill(&layout, &project);
assert_eq!(
fs::read_to_string(project.join("AGENTS.md")).unwrap(),
project_rules("2.0.0")
);
let CommandResult::Exited(code) = execute_in(&layout, &arguments).unwrap() else {
panic!("project command must dispatch")
};
@@ -393,7 +389,12 @@ fn signed_versions_install_select_dispatch_update_and_rollback_atomically() {
fs::read_to_string(skill_path(&layout).join("SKILL.md")).unwrap(),
"skill 2.0.0\n"
);
assert_eq!(fs::read(skill_entry.join("SKILL.md")).unwrap(), loader);
assert_eq!(fs::read(active_project.join("AGENTS.md")).unwrap(), rules);
install_project_skill(&layout, &active_project);
assert_eq!(
fs::read_to_string(active_project.join("AGENTS.md")).unwrap(),
project_rules("2.0.0")
);
fs::write(layout.downloads().join("latest"), "1.0.0\n").unwrap();
assert!(
manager
@@ -1342,12 +1343,16 @@ fn write_release(
let skill = root.join("skills/dever-language/SKILL.md");
fs::create_dir_all(skill.parent().unwrap()).unwrap();
fs::write(&skill, format!("skill {version}\n")).unwrap();
let rules = root.join("skills/dever-language/assets/template/AGENTS.md");
fs::create_dir_all(rules.parent().unwrap()).unwrap();
fs::write(&rules, project_rules(version)).unwrap();
let manifest = ReleaseManifest::new(
Version::parse(version).unwrap(),
vec![
artifact(&core, "dever-core"),
artifact(&runtime, "runtime.pack"),
artifact(&skill, "skills/dever-language/SKILL.md"),
artifact(&rules, "skills/dever-language/assets/template/AGENTS.md"),
],
);
let bytes = serde_json::to_vec(&manifest).unwrap();
@@ -1359,6 +1364,29 @@ fn write_release(
fs::write(root.join("manifest.sig"), format!("{}\n", hex(&signature))).unwrap();
}
#[cfg(unix)]
fn project_rules(version: &str) -> String {
include_str!("../../skills/assets/template/AGENTS.md").replace(
"<!-- dever-language:end -->",
&format!("<!-- fixture version {version} -->\n<!-- dever-language:end -->"),
)
}
#[cfg(unix)]
fn install_project_skill(layout: &Layout, project: &Path) {
let output = Command::new(layout.bin().join("dever"))
.args(["skill", "install"])
.current_dir(project)
.env_clear()
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
fn artifact(path: &Path, relative: &str) -> Artifact {
let bytes = fs::read(path).unwrap();
Artifact {
+84
View File
@@ -0,0 +1,84 @@
use super::{Action, OsString, arguments_for};
use std::time::Duration;
#[test]
fn test_options_default_to_sixty_seconds_and_accept_either_order() {
for options in [
vec![],
vec!["--filter", "sample.account.case_2"],
vec!["--filter", "sample.account", "--timeout-ms", "15"],
vec!["--timeout-ms", "15", "--filter", "sample"],
vec!["--timeout-ms", "18446744073709551615"],
] {
let arguments: Vec<_> = [vec!["test", "project"], options.clone()]
.concat()
.into_iter()
.map(OsString::from)
.collect();
let (root, action) = arguments_for(&arguments).unwrap();
assert_eq!(root.to_str(), Some("project"));
let Action::Test(parsed) = action else {
panic!("expected test")
};
let timeout = options
.iter()
.position(|option| *option == "--timeout-ms")
.map(|index| options[index + 1].parse::<u64>().unwrap())
.unwrap_or(60_000);
assert_eq!(parsed.timeout, Duration::from_millis(timeout));
assert_eq!(
parsed.filter.as_deref(),
options
.iter()
.position(|option| *option == "--filter")
.map(|index| options[index + 1]),
);
}
}
#[test]
fn test_options_reject_invalid_missing_duplicate_and_unknown_arguments() {
let mut invalid = vec![
vec!["--filter"],
vec!["--timeout-ms"],
vec!["--filter", "--timeout-ms", "20"],
vec!["--filter", "sample", "--filter", "sample.account"],
vec!["--timeout-ms", "20", "--timeout-ms", "30"],
vec!["--unknown", "value"],
vec!["extra"],
];
for filter in [
"", ".sample", "sample.", "sample..case", "sample.a.b.c", "sam*", "sample/account",
"Sample", "sample.__case", "sample.case_", "sample.2case", " sample", "用户",
] {
invalid.push(vec!["--filter", filter]);
}
for timeout in [
"", "0", "-1", "+1", "1.5", "1ms", " 1", "18446744073709551616",
] {
invalid.push(vec!["--timeout-ms", timeout]);
}
for options in invalid {
let arguments: Vec<_> = [vec!["test", "project"], options]
.concat()
.into_iter()
.map(OsString::from)
.collect();
assert!(arguments_for(&arguments).is_err(), "accepted {arguments:?}");
}
}
#[cfg(unix)]
#[test]
fn test_options_reject_non_utf8_values() {
use std::os::unix::ffi::OsStringExt;
for option in ["--filter", "--timeout-ms"] {
let arguments = [
OsString::from("test"),
OsString::from("project"),
OsString::from(option),
OsString::from_vec(vec![0xff]),
];
assert!(arguments_for(&arguments).is_err());
}
}
+114
View File
@@ -0,0 +1,114 @@
use super::{TemporaryProject, process};
use std::fs;
use std::process::Command;
use std::time::{Duration, Instant};
const DESCENDANTS: &str = r#"
/bin/sh -c 'echo $$ > descendant; /bin/sleep 30 & echo $! > grandchild; wait' &
while [ ! -s grandchild ]; do :; done
"#;
// subreaper 是进程级状态;此单例测试顺序覆盖所有真实进程场景。
#[test]
fn case_process_captures_output_times_out_and_reaps_only_owned_descendants() {
let original_reaper = rustix::process::child_subreaper().unwrap();
let mut unrelated = Command::new("/bin/sleep").arg("30").spawn().unwrap();
let result = std::panic::catch_unwind(|| {
for (body, descendants, expected) in [
("printf hidden; printf hidden-error >&2; exit 0", false, None),
(
"printf output; printf diagnostic >&2; exit 7",
false,
Some("exit status: 7"),
),
(
"printf before-timeout; /bin/sleep 30",
false,
Some("timed out after 100 ms"),
),
("exit 0", true, None),
("printf owned-error >&2; exit 7", true, Some("exit status: 7")),
("printf started; wait", true, Some("timed out after 100 ms")),
] {
let root = TemporaryProject::new().unwrap();
let started = Instant::now();
let script = if descendants {
format!("{DESCENDANTS}\n{body}")
} else {
body.to_owned()
};
let result = process::run(
Command::new("/bin/sh")
.arg("-c")
.arg(&script)
.env_clear()
.current_dir(root.path()),
root.path(),
Duration::from_millis(100),
);
assert!(started.elapsed() < Duration::from_secs(5));
match expected {
None => result.unwrap(),
Some(message) => {
let failure = result.unwrap_err();
assert!(failure.message.contains(message), "{failure:?}");
if script.contains("printf output") {
assert_eq!(failure.stdout, b"output");
assert_eq!(failure.stderr, b"diagnostic");
}
if script.contains("printf before-timeout") {
assert_eq!(failure.stdout, b"before-timeout");
}
if script.contains("printf owned-error") {
assert_eq!(failure.stderr, b"owned-error");
}
}
}
for name in ["descendant", "grandchild"] {
let path = root.path().join(name);
if descendants {
let pid = fs::read_to_string(path)
.unwrap()
.trim()
.parse::<u32>()
.unwrap();
assert!(
!std::path::Path::new(&format!("/proc/{pid}")).exists(),
"unreaped {name} {pid}"
);
}
}
assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper);
}
let root = TemporaryProject::new().unwrap();
let failure = process::run(
Command::new("/bin/sh")
.arg("-c")
.arg("/bin/dd if=/dev/zero bs=131072 count=2 2>/dev/null; printf end >&2; exit 9")
.env_clear(),
root.path(),
Duration::from_secs(5),
)
.unwrap_err();
assert!(failure.message.contains("exit status: 9"));
assert_eq!(failure.stdout.len(), 262_144);
assert_eq!(failure.stderr, b"end");
let root = TemporaryProject::new().unwrap();
let failure = process::run(
&mut Command::new(root.path().join("missing-executable")),
root.path(),
Duration::from_millis(100),
)
.unwrap_err();
assert!(failure.message.contains("cannot run test process"));
assert_eq!(rustix::process::child_subreaper().unwrap(), original_reaper);
});
// 即使断言失败也回收此对照进程,不能让测试自身遗留资源。
let unrelated_running = unrelated.try_wait().unwrap().is_none();
let _ = unrelated.kill();
unrelated.wait().unwrap();
result.unwrap();
assert!(unrelated_running, "case cleanup affected an unrelated child");
}