Files
core/test/dever-tests/tests/application_testing.rs

412 lines
11 KiB
Rust

#[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());
}