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,411 @@
|
||||
#[path = "support/temp.rs"]
|
||||
mod temp;
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use dever_core::diagnostic::Diagnostic;
|
||||
use dever_core::source::SourceMap;
|
||||
use temp::TemporaryDirectory;
|
||||
|
||||
fn project(module: &[(&str, &str)], tests: &[(&str, &str)]) -> (TemporaryDirectory, SourceMap) {
|
||||
let root = TemporaryDirectory::new();
|
||||
write_sources(&root.path().join("module"), module);
|
||||
if !tests.is_empty() {
|
||||
write_sources(&root.path().join("test"), tests);
|
||||
}
|
||||
let sources =
|
||||
SourceMap::load_project(&root.path().join("module"), &root.path().join("test")).unwrap();
|
||||
(root, sources)
|
||||
}
|
||||
|
||||
fn write_sources(root: &Path, files: &[(&str, &str)]) {
|
||||
fs::create_dir_all(root).unwrap();
|
||||
for (relative, text) in files {
|
||||
let path = root.join(relative);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, text).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn checked(sources: &SourceMap) -> dever_core::hir::Program {
|
||||
dever_core::check(sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn rejected(sources: &SourceMap, code: &str, message: &str) -> Vec<Diagnostic> {
|
||||
let errors = dever_core::check(sources).expect_err("source must be rejected");
|
||||
assert!(
|
||||
errors
|
||||
.iter()
|
||||
.any(|error| error.code == code && error.message.contains(message)),
|
||||
"expected {code}: {message}; got {}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(sources))
|
||||
.collect::<String>()
|
||||
);
|
||||
errors
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_filename_entries_in_stable_order_and_allows_no_tests() {
|
||||
let (_root, sources) = project(&[("main.dever", "main() () {}")], &[]);
|
||||
assert!(checked(&sources).tests().is_empty());
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[
|
||||
(
|
||||
"user/account/register.dever",
|
||||
"register() () { assert(true) }",
|
||||
),
|
||||
(
|
||||
"news/article/publish.dever",
|
||||
"publish() () { assert_eq(1, 1) }",
|
||||
),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
checked(&sources)
|
||||
.tests()
|
||||
.iter()
|
||||
.map(|test| test.name())
|
||||
.collect::<Vec<_>>(),
|
||||
["news/article/publish", "user/account/register"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_test_paths_and_entry_contracts() {
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[("user/account/nested/register.dever", "register() () {}")],
|
||||
);
|
||||
rejected(&sources, "C003", "test source must be");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[("user/account/register.dever", "helper_entry() () {}")],
|
||||
);
|
||||
rejected(
|
||||
&sources,
|
||||
"C005",
|
||||
"must declare the ordinary 'register() ()' entry",
|
||||
);
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[("user/account/register.dever", "register(value: Int) () {}")],
|
||||
);
|
||||
rejected(&sources, "C005", "zero-input, zero-output");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_documented_markdown_tests_and_rejects_duplicate_topics() {
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[(
|
||||
"user/account/register.dever.md",
|
||||
r#"# 注册账户测试
|
||||
|
||||
验证账户注册行为。
|
||||
|
||||
- 包:`user.account.register`
|
||||
- 公开类型:无
|
||||
- 公开方法:无
|
||||
- 使用:无
|
||||
|
||||
## 注册账户
|
||||
|
||||
执行注册断言。
|
||||
|
||||
- 函数:`register`
|
||||
- 输入:无
|
||||
- 输出:无
|
||||
|
||||
```dever
|
||||
register() () { assert(true) }
|
||||
```
|
||||
"#,
|
||||
)],
|
||||
);
|
||||
assert_eq!(checked(&sources).tests()[0].name(), "user/account/register");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[
|
||||
("user/account/register.dever", "register() () {}"),
|
||||
(
|
||||
"user/account/register.dever.md",
|
||||
r#"# 注册账户测试
|
||||
|
||||
验证账户注册行为。
|
||||
|
||||
- 包:`user.account.register`
|
||||
- 公开类型:无
|
||||
- 公开方法:无
|
||||
- 使用:无
|
||||
|
||||
## 注册账户
|
||||
|
||||
执行注册断言。
|
||||
|
||||
- 函数:`register`
|
||||
- 输入:无
|
||||
- 输出:无
|
||||
|
||||
```dever
|
||||
register() () {}
|
||||
```
|
||||
"#,
|
||||
),
|
||||
],
|
||||
);
|
||||
rejected(&sources, "C002", "duplicate package");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visibility_uses_domain_boundaries_without_exposing_test_helpers() {
|
||||
let (_root, sources) = project(
|
||||
&[
|
||||
("main.dever", "main() () {}"),
|
||||
(
|
||||
"user/account/domain.dever",
|
||||
"normalize(value: Text) (result: Text) { result = text.lower(text.trim(value)) }",
|
||||
),
|
||||
(
|
||||
"user/account/app.dever",
|
||||
"normalize(value: Text) (result: Text) { result = domain.normalize(value) }",
|
||||
),
|
||||
(
|
||||
"news/article/app.dever",
|
||||
"label() (result: Text) { result = \"article\" }",
|
||||
),
|
||||
],
|
||||
&[(
|
||||
"user/account/normalize.dever",
|
||||
r#"normalize() () {
|
||||
assert_eq(app.normalize(" Editor "), "editor")
|
||||
assert_eq(domain.normalize(" ADMIN "), "admin")
|
||||
assert_eq(news.article.label(), "article")
|
||||
helper()
|
||||
}
|
||||
helper() () {}"#,
|
||||
)],
|
||||
);
|
||||
assert_eq!(checked(&sources).tests().len(), 1);
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[
|
||||
("main.dever", "main() () {}"),
|
||||
(
|
||||
"news/article/domain.dever",
|
||||
"secret() (result: Text) { result = \"private\" }",
|
||||
),
|
||||
],
|
||||
&[(
|
||||
"user/account/steal.dever",
|
||||
"steal() () { assert_eq(news.article.domain.secret(), \"private\") }",
|
||||
)],
|
||||
);
|
||||
rejected(&sources, "C006", "not callable from this source role");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[
|
||||
("main.dever", "main() () {}"),
|
||||
("user/account/model.dever", "type Account { email: Text }"),
|
||||
],
|
||||
&[(
|
||||
"user/account/count.dever",
|
||||
"count() () { assert_eq(model.count(), 0) }",
|
||||
)],
|
||||
);
|
||||
rejected(&sources, "C006", "Model operations are private");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[
|
||||
("main.dever", "main() () {}"),
|
||||
(
|
||||
"user/account/app.dever",
|
||||
"leak() () { user.account.register.helper() }",
|
||||
),
|
||||
],
|
||||
&[(
|
||||
"user/account/register.dever",
|
||||
"register() () {}\nhelper() () {}",
|
||||
)],
|
||||
);
|
||||
rejected(&sources, "C006", "not callable from this source role");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[
|
||||
(
|
||||
"user/account/register.dever",
|
||||
"register() () {}\nhelper() () {}",
|
||||
),
|
||||
(
|
||||
"user/account/profile.dever",
|
||||
"profile() () { user.account.register.helper() }",
|
||||
),
|
||||
],
|
||||
);
|
||||
rejected(&sources, "C006", "not callable from this source role");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assertions_are_test_only_exact_and_comparable() {
|
||||
let (_root, sources) = project(&[("main.dever", "main() () { assert(true) }")], &[]);
|
||||
rejected(&sources, "C004", "unknown function 'assert'");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[("user/account/value.dever", "value() () { assert(1) }")],
|
||||
);
|
||||
rejected(&sources, "C005", "expected Bool");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[(
|
||||
"user/account/types.dever",
|
||||
"types() () { assert_eq(1, 1.0) }",
|
||||
)],
|
||||
);
|
||||
rejected(&sources, "C005", "same type on both sides");
|
||||
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[(
|
||||
"user/account/stream.dever",
|
||||
"stream() () { assert_eq(stream([1]), stream([1])) }",
|
||||
)],
|
||||
);
|
||||
rejected(&sources, "C005", "assert_eq does not support");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests_cannot_start_the_application_api() {
|
||||
let (_root, sources) = project(
|
||||
&[("main.dever", "main() () {}")],
|
||||
&[(
|
||||
"user/account/server.dever",
|
||||
"server() () { dever.api.serve() }",
|
||||
)],
|
||||
);
|
||||
rejected(&sources, "C006", "cannot start the application HTTP API");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests_use_existing_result_choices_for_business_failures() {
|
||||
let (_root, sources) = project(
|
||||
&[
|
||||
("main.dever", "main() () {}"),
|
||||
(
|
||||
"user/account/app.dever",
|
||||
r#"type RegistrationResult {
|
||||
Created(account: Int)
|
||||
error Invalid(message: Text)
|
||||
}
|
||||
register(value: 0) (account: Int) {
|
||||
fail(RegistrationResult.Invalid("invalid account"))
|
||||
}
|
||||
register(value: other) (account: Int) {
|
||||
account = value
|
||||
}"#,
|
||||
),
|
||||
],
|
||||
&[(
|
||||
"user/account/register.dever",
|
||||
r#"register() () {
|
||||
verify(result(app.register(0)))
|
||||
}
|
||||
verify(result: app.RegistrationResult.Invalid(message)) () recover("test verifies expected failure") {
|
||||
assert_eq(message, "invalid account")
|
||||
}
|
||||
verify(result: app.RegistrationResult.Created(account)) () recover("test verifies expected failure") {
|
||||
assert(false)
|
||||
}"#,
|
||||
)],
|
||||
);
|
||||
assert_eq!(checked(&sources).tests().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_test_suite_dispatches_cases_reports_locations_and_reuses_cache() {
|
||||
let (_root, sources) = project(
|
||||
&[
|
||||
("main.dever", "main() () {}"),
|
||||
("user/profile/model.dever", "type Profile { name: Text }"),
|
||||
],
|
||||
&[
|
||||
(
|
||||
"news/article/publish.dever",
|
||||
"publish() () { assert_eq(1, 1) }",
|
||||
),
|
||||
(
|
||||
"user/account/register.dever",
|
||||
r#"type Pair {
|
||||
left: Int
|
||||
right: Int
|
||||
}
|
||||
type Number {
|
||||
Present(value: Int)
|
||||
Missing
|
||||
}
|
||||
register() () {
|
||||
actual = Pair {
|
||||
left = 1
|
||||
right = 2
|
||||
}
|
||||
expected = Pair {
|
||||
left = 1
|
||||
right = 2
|
||||
}
|
||||
assert_eq(actual, expected)
|
||||
assert_eq(Number.Present(1), Number.Present(1))
|
||||
assert_eq(first([1]), first([1]))
|
||||
assert_eq(missing(), null)
|
||||
assert_eq([1, 2], [1, 2])
|
||||
values = { "one" = 1 }
|
||||
assert_eq(values, { "one" = 1 })
|
||||
assert_eq(entries(values), entries({ "one" = 1 }))
|
||||
assert_eq(1, 2)
|
||||
}
|
||||
missing() (value: Int?) {
|
||||
value = null
|
||||
}"#,
|
||||
),
|
||||
],
|
||||
);
|
||||
let program = checked(&sources);
|
||||
assert!(!program.tests()[0].uses_database());
|
||||
assert!(!program.tests()[1].uses_database());
|
||||
let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
|
||||
let native = dever_core::native::compile_test_suite(&program, &sources, &rustc).unwrap();
|
||||
let success = Command::new(native.executable()).arg("0").output().unwrap();
|
||||
assert!(success.status.success());
|
||||
let output = Command::new(native.executable()).arg("1").output().unwrap();
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("test/user/account/register.dever:26:"),
|
||||
"{stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("assert_eq failed: actual = 1, expected = 2"),
|
||||
"{stderr}"
|
||||
);
|
||||
drop(native);
|
||||
let cached = dever_core::native::compile_test_suite(&program, &sources, &rustc).unwrap();
|
||||
assert!(cached.cache_hit());
|
||||
}
|
||||
Reference in new issue
Block a user