feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,451 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
#[path = "../dever-tests/tests/support/process.rs"]
|
||||
pub mod process;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
#[path = "support/llvm_pack.rs"]
|
||||
pub mod llvm_pack;
|
||||
|
||||
static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TemporaryProject(PathBuf);
|
||||
|
||||
impl TemporaryProject {
|
||||
fn new() -> Self {
|
||||
let suffix = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("dever-cms-project-{}-{suffix}", std::process::id()));
|
||||
fs::create_dir(&root).unwrap();
|
||||
Self(root)
|
||||
}
|
||||
|
||||
fn copy_from(source: &Path) -> Self {
|
||||
let project = Self::new();
|
||||
copy_directory(&source.join("config"), &project.path().join("config"));
|
||||
copy_directory(&source.join("module"), &project.path().join("module"));
|
||||
let tests = source.join("test");
|
||||
if tests.exists() {
|
||||
copy_directory(&tests, &project.path().join("test"));
|
||||
}
|
||||
fs::create_dir(project.path().join("data")).unwrap();
|
||||
project
|
||||
}
|
||||
|
||||
fn package(source: &Path, executable: &Path) -> Self {
|
||||
let project = Self::new();
|
||||
copy_directory(&source.join("config"), &project.path().join("config"));
|
||||
fs::copy(executable, project.path().join("cms-app")).unwrap();
|
||||
project
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn block_deployment_database(&self) {
|
||||
// Package metadata needs valid JSON. A directory as the configured
|
||||
// database proves tests use their own SQLite configuration instead.
|
||||
fs::create_dir(self.path().join("data/deployment.db")).unwrap();
|
||||
fs::write(
|
||||
self.path().join("config/setting.json"),
|
||||
r#"{"database":{"default":{"type":"sqlite","path":"data/deployment.db"}}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TemporaryProject {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_directory(source: &Path, target: &Path) {
|
||||
fs::create_dir(target).unwrap();
|
||||
for entry in fs::read_dir(source).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let source = entry.path();
|
||||
let target = target.join(entry.file_name());
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
copy_directory(&source, &target);
|
||||
} else {
|
||||
fs::copy(source, target).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dever(project: &Path, action: &str, arguments: &[&str]) -> Output {
|
||||
dever_with_pid(project, action, arguments).1
|
||||
}
|
||||
|
||||
fn dever_with_pid(project: &Path, action: &str, arguments: &[&str]) -> (u32, Output) {
|
||||
output_with_pid(
|
||||
Command::new(env!("CARGO_BIN_EXE_dever"))
|
||||
.arg(action)
|
||||
.arg(project)
|
||||
.args(arguments),
|
||||
)
|
||||
}
|
||||
|
||||
fn output_with_pid(command: &mut Command) -> (u32, Output) {
|
||||
let capture = TemporaryProject::new();
|
||||
let stdout = capture.path().join("stdout");
|
||||
let stderr = capture.path().join("stderr");
|
||||
let mut child = command
|
||||
.env_clear()
|
||||
.stdout(Stdio::from(fs::File::create(&stdout).unwrap()))
|
||||
.stderr(Stdio::from(fs::File::create(&stderr).unwrap()))
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let pid = child.id();
|
||||
let status = process::wait(&mut child, Duration::from_secs(180)).unwrap();
|
||||
(
|
||||
pid,
|
||||
Output {
|
||||
status,
|
||||
stdout: fs::read(stdout).unwrap(),
|
||||
stderr: fs::read(stderr).unwrap(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn assert_test_artifacts_removed(pid: u32) {
|
||||
let test_prefix = format!("dever-application-test-{pid}-");
|
||||
let build_prefix = format!("dever-build-{pid}-");
|
||||
let leftovers = fs::read_dir(std::env::temp_dir())
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
||||
.filter(|name| name.starts_with(&test_prefix) || name.starts_with(&build_prefix))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
leftovers.is_empty(),
|
||||
"temporary test artifacts remain: {leftovers:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_success(output: &Output) {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn clean_removes_only_stale_owned_dever_artifacts() {
|
||||
use std::os::unix::fs::{MetadataExt, symlink};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let project = TemporaryProject::new();
|
||||
let pid = u64::from(std::process::id()) + 1_000_000;
|
||||
let suffix = NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed);
|
||||
let stale = std::env::temp_dir().join(format!("dever-build-{pid}-{suffix}"));
|
||||
fs::create_dir(&stale).unwrap();
|
||||
let owner = fs::metadata(&stale).unwrap().uid();
|
||||
fs::write(
|
||||
stale.join(".dever-owner"),
|
||||
format!(
|
||||
"dever-build-owner-v1\ncreated=1\npid={pid}\nowner={owner}\ncompiler={}\n",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(stale.join("program"), b"stale").unwrap();
|
||||
|
||||
let fresh_pid = pid + 1;
|
||||
let fresh = std::env::temp_dir().join(format!("dever-build-{fresh_pid}-{suffix}"));
|
||||
fs::create_dir(&fresh).unwrap();
|
||||
let created = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
fs::write(
|
||||
fresh.join(".dever-owner"),
|
||||
format!(
|
||||
"dever-build-owner-v1\ncreated={created}\npid={fresh_pid}\nowner={owner}\ncompiler={}\n",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
).unwrap();
|
||||
let unknown = std::env::temp_dir().join(format!("dever-build-{}-{suffix}", pid + 2));
|
||||
fs::create_dir(&unknown).unwrap();
|
||||
let link = std::env::temp_dir().join(format!("dever-build-{}-{suffix}", pid + 3));
|
||||
symlink(&unknown, &link).unwrap();
|
||||
|
||||
let mut active_process = Command::new("/bin/sleep").arg("60").spawn().unwrap();
|
||||
let active_pid = u64::from(active_process.id());
|
||||
let active = std::env::temp_dir().join(format!("dever-build-{active_pid}-{suffix}"));
|
||||
fs::create_dir(&active).unwrap();
|
||||
fs::write(
|
||||
active.join(".dever-owner"),
|
||||
format!(
|
||||
"dever-build-owner-v1\ncreated=1\npid={active_pid}\nowner={owner}\ncompiler={}\n",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(active.join("program"), b"active").unwrap();
|
||||
|
||||
let legacy = project.path().join(format!(".dever-run-{pid}"));
|
||||
fs::write(&legacy, b"stale legacy").unwrap();
|
||||
let legacy_file = fs::OpenOptions::new().write(true).open(&legacy).unwrap();
|
||||
legacy_file
|
||||
.set_times(
|
||||
fs::FileTimes::new().set_modified(UNIX_EPOCH + std::time::Duration::from_secs(1)),
|
||||
)
|
||||
.unwrap();
|
||||
let fresh_legacy = project.path().join(format!(".dever-run-{}", pid + 1));
|
||||
fs::write(&fresh_legacy, b"fresh legacy").unwrap();
|
||||
let active_legacy = project.path().join(format!(".dever-run-{active_pid}"));
|
||||
fs::write(&active_legacy, b"active legacy").unwrap();
|
||||
let active_legacy_file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.open(&active_legacy)
|
||||
.unwrap();
|
||||
active_legacy_file
|
||||
.set_times(
|
||||
fs::FileTimes::new().set_modified(UNIX_EPOCH + std::time::Duration::from_secs(1)),
|
||||
)
|
||||
.unwrap();
|
||||
let explicit = project.path().join("application");
|
||||
fs::write(&explicit, b"explicit output").unwrap();
|
||||
|
||||
let output = dever(project.path(), "clean", &[]);
|
||||
active_process.kill().unwrap();
|
||||
active_process.wait().unwrap();
|
||||
assert_success(&output);
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stdout).starts_with("cleaned 1 directories, 3 files, ")
|
||||
);
|
||||
assert!(!stale.exists());
|
||||
assert!(fresh.exists());
|
||||
assert!(unknown.exists());
|
||||
assert!(link.symlink_metadata().is_ok());
|
||||
assert!(active.exists());
|
||||
assert!(!legacy.exists());
|
||||
assert!(fresh_legacy.exists());
|
||||
assert!(active_legacy.exists());
|
||||
assert!(explicit.exists());
|
||||
|
||||
fs::remove_dir_all(fresh).unwrap();
|
||||
fs::remove_file(link).unwrap();
|
||||
fs::remove_dir_all(unknown).unwrap();
|
||||
fs::remove_dir_all(active).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cms_project_checks_runs_and_builds_from_an_isolated_copy() {
|
||||
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
for format in ["dever", "md"] {
|
||||
let project = TemporaryProject::copy_from(&workspace.join("examples/cms").join(format));
|
||||
let settings_path = project.path().join("config/setting.json");
|
||||
let production_settings = fs::read(&settings_path).unwrap();
|
||||
project.block_deployment_database();
|
||||
for _ in 0..2 {
|
||||
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
|
||||
assert_cms_tests(&output);
|
||||
assert_test_artifacts_removed(pid);
|
||||
}
|
||||
fs::write(&settings_path, production_settings).unwrap();
|
||||
fs::write(project.path().join("test/ignored.dever"), "not valid Dever").unwrap();
|
||||
|
||||
assert_success(&dever(project.path(), "check", &[]));
|
||||
for _ in 0..2 {
|
||||
assert_health_stdout(&dever(
|
||||
project.path(),
|
||||
"run",
|
||||
&["--", "system.health.ping", "{}"],
|
||||
));
|
||||
}
|
||||
|
||||
let executable = build(project.path(), "cms-app");
|
||||
let package = TemporaryProject::package(project.path(), &executable);
|
||||
assert!(!package.path().join("module").exists());
|
||||
for _ in 0..2 {
|
||||
let output = output_with_pid(
|
||||
Command::new(package.path().join("cms-app"))
|
||||
.args(["system.health.ping", "{}"])
|
||||
.current_dir(package.path()),
|
||||
)
|
||||
.1;
|
||||
assert_health_stdout(&output);
|
||||
}
|
||||
assert!(fs::read_dir(project.path()).unwrap().all(|entry| {
|
||||
!entry
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.starts_with(".dever-run-")
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_without_tests_reports_zero_and_succeeds() {
|
||||
let project = TemporaryProject::new();
|
||||
fs::create_dir(project.path().join("module")).unwrap();
|
||||
fs::write(project.path().join("module/main.dever"), "main() () {}").unwrap();
|
||||
|
||||
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
|
||||
assert_success(&output);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
"running 0 tests\n\ntest result: ok. 0 passed; 0 failed; 0 not run\n"
|
||||
);
|
||||
assert_test_artifacts_removed(pid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_suite_initializes_database_only_for_database_cases() {
|
||||
let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let project = TemporaryProject::copy_from(&workspace.join("examples/cms/dever"));
|
||||
project.block_deployment_database();
|
||||
let health = project.path().join("test/system/health");
|
||||
fs::create_dir_all(&health).unwrap();
|
||||
fs::write(
|
||||
health.join("ping.dever"),
|
||||
"ping() () { assert_eq(\"ready\", \"ready\") }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
|
||||
assert_success(&output);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
"running 6 tests\ntest content/category/lifecycle ... ok\ntest news/article/publish ... ok\ntest system/health/ping ... ok\ntest user/account/bootstrap ... ok\ntest user/account/validation ... ok\ntest user/session/lifecycle ... ok\n\ntest result: ok. 6 passed; 0 failed; 0 not run\n"
|
||||
);
|
||||
assert_test_artifacts_removed(pid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_test_reports_captured_output_and_does_not_stop_later_tests() {
|
||||
let project = TemporaryProject::new();
|
||||
fs::create_dir(project.path().join("module")).unwrap();
|
||||
fs::create_dir_all(project.path().join("test/sample/case")).unwrap();
|
||||
fs::write(project.path().join("module/main.dever"), "main() () {}").unwrap();
|
||||
fs::write(
|
||||
project.path().join("test/sample/case/fails.dever"),
|
||||
r#"fails() () {
|
||||
blocking(dever.io.println("failed output"))
|
||||
assert_eq(1, 2)
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
project.path().join("test/sample/case/passes.dever"),
|
||||
r#"passes() () {
|
||||
blocking(dever.io.println("hidden successful output"))
|
||||
assert(true)
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let (pid, output) = dever_with_pid(project.path(), "test", &[]);
|
||||
assert!(!output.status.success());
|
||||
assert!(output.stderr.is_empty());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let failed = stdout.find("test sample/case/fails ... FAILED").unwrap();
|
||||
let passed = stdout.find("test sample/case/passes ... ok").unwrap();
|
||||
assert!(failed < passed, "{stdout}");
|
||||
assert!(
|
||||
stdout.contains("---- stdout ----\nfailed output\n"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert!(stdout.contains("---- stderr ----"), "{stdout}");
|
||||
assert!(
|
||||
stdout.contains("assert_eq failed: actual = 1, expected = 2"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert!(!stdout.contains("hidden successful output"), "{stdout}");
|
||||
assert!(
|
||||
stdout.ends_with("test result: FAILED. 1 passed; 1 failed; 0 not run\n"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert_test_artifacts_removed(pid);
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
#[test]
|
||||
fn suite_compilation_failure_marks_every_discovered_case_failed() {
|
||||
let project = TemporaryProject::new();
|
||||
fs::create_dir(project.path().join("module")).unwrap();
|
||||
fs::create_dir_all(project.path().join("test/sample/case")).unwrap();
|
||||
fs::write(project.path().join("module/main.dever"), "main() () {}").unwrap();
|
||||
fs::write(
|
||||
project.path().join("test/sample/case/first.dever"),
|
||||
"first() () { assert(true) }\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
project.path().join("test/sample/case/second.dever"),
|
||||
"second() () { assert(true) }\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let toolchain = TemporaryProject::new();
|
||||
let compiler = llvm_pack::compiler(
|
||||
Path::new(env!("CARGO_BIN_EXE_dever")),
|
||||
toolchain.path(),
|
||||
"dever",
|
||||
);
|
||||
let (pid, output) = output_with_pid(Command::new(compiler).arg("test").arg(project.path()));
|
||||
assert!(!output.status.success());
|
||||
assert!(output.stderr.is_empty());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("test sample/case/first ... FAILED"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("test sample/case/second ... FAILED"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert_eq!(
|
||||
stdout.matches("cannot inspect runtime pack").count(),
|
||||
2,
|
||||
"{stdout}"
|
||||
);
|
||||
assert!(
|
||||
stdout.ends_with("test result: FAILED. 0 passed; 2 failed; 0 not run\n"),
|
||||
"{stdout}"
|
||||
);
|
||||
assert_test_artifacts_removed(pid);
|
||||
assert!(!toolchain.path().join("cache").exists());
|
||||
}
|
||||
|
||||
fn build(project: &Path, name: &str) -> PathBuf {
|
||||
let executable = project.join(name);
|
||||
let build = dever(
|
||||
project,
|
||||
"build",
|
||||
&["--output", executable.to_str().unwrap()],
|
||||
);
|
||||
assert_success(&build);
|
||||
assert!(executable.is_file());
|
||||
executable
|
||||
}
|
||||
|
||||
fn assert_health_stdout(output: &Output) {
|
||||
assert_success(output);
|
||||
let body: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(body["code"], 0);
|
||||
assert_eq!(body["message"], "ok");
|
||||
assert_eq!(body["data"]["status"], "ready");
|
||||
}
|
||||
|
||||
fn assert_cms_tests(output: &Output) {
|
||||
assert_success(output);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
"running 5 tests\ntest content/category/lifecycle ... ok\ntest news/article/publish ... ok\ntest user/account/bootstrap ... ok\ntest user/account/validation ... ok\ntest user/session/lifecycle ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 not run\n"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user