Files
core/test/dever-tests/tests/time_codec.rs
T

490 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
mod support;
use std::fs;
use std::process::Command;
use dever_core::wire::Policy;
use dever_runtime::{time, wire};
#[test]
fn time_calendar_precision_and_checked_arithmetic() {
for (text, expected) in [
("1970-01-01T00:00:00Z", 0),
("1969-12-31T23:59:59.999Z", -1),
("1970-01-01T08:00:00+08:00", 0),
("2000-02-29T00:00:00Z", 951_782_400_000),
] {
let value = time::parse_datetime(text).unwrap();
assert_eq!(value, expected);
assert_eq!(
time::parse_datetime(&time::format_datetime(value).unwrap()).unwrap(),
value
);
}
assert_eq!(time::format_datetime(0).unwrap(), "1970-01-01T00:00:00Z");
assert_eq!(
time::format_datetime(-1).unwrap(),
"1969-12-31T23:59:59.999Z"
);
for text in [
"1900-02-29T00:00:00Z",
"2000-02-29",
"2016-12-31T23:59:60Z",
"1970-01-01T00:00:00.0001Z",
"1970-01-01T00:00:00.0000000001Z",
] {
assert!(time::parse_datetime(text).is_err(), "{text}");
}
for text in ["0000-01-01", "1969-12-31", "2000-02-29", "9999-12-31"] {
assert_eq!(
time::format_date(time::parse_date(text).unwrap()).unwrap(),
text
);
}
for text in ["1900-02-29", "2024-2-01", "2024-02-30", "+2024-02-01"] {
assert!(time::parse_date(text).is_err());
}
for text in ["00:00:00", "23:59:59.999", "12:30:05.001"] {
assert_eq!(
time::format_time(time::parse_time(text).unwrap()).unwrap(),
text
);
}
for text in ["24:00:00", "23:59:60", "12:00:00.1", "12:00:00.0001"] {
assert!(time::parse_time(text).is_err());
}
assert!(time::format_time(86_400_000).is_err());
assert!(time::format_time(-1).is_err());
assert!(time::format_date(1).is_err());
assert!(time::format_datetime(i64::MAX).is_err());
assert!(time::format_datetime(i64::MIN).is_err());
assert!(time::add(i64::MAX, 1).is_err());
assert!(time::subtract(i64::MIN, 1).is_err());
assert!(time::difference(i64::MAX, -1).is_err());
assert_eq!(time::add(-1, time::duration(1)).unwrap(), 0);
assert_eq!(time::difference(-1, 0).unwrap(), -1);
}
#[test]
fn parser_rejects_ambiguous_and_unbounded_input_without_losing_json_numbers() {
for text in [
r#"{"a":{"x":1,"x":2}}"#,
r#"[{"x":1,"x":2}]"#,
"true false",
"NaN",
"Infinity",
"[1,]",
r#""\ud800""#,
] {
assert!(wire::parse(text).is_err(), "{text}");
}
let precise =
r#"{"big":1234567890123456789012345678901234567890,"tiny":1.234567890123456789e-900}"#;
assert_eq!(wire::parse(precise).unwrap().raw(), precise);
assert_eq!(
wire::parse("-9223372036854775808").unwrap().int().unwrap(),
i64::MIN
);
for text in ["9223372036854775808", "1.0", "1e0", "true", "\"1\""] {
assert!(wire::parse(text).unwrap().int().is_err());
}
assert!(wire::parse("1e900").unwrap().float().is_err());
let at_depth = format!(
"{}0{}",
"[".repeat(wire::MAX_DEPTH),
"]".repeat(wire::MAX_DEPTH)
);
assert!(wire::parse(&at_depth).is_ok());
assert!(wire::parse(&format!("[{at_depth}]")).is_err());
assert!(wire::parse(&format!("\"{}\"", "x".repeat(wire::MAX_BYTES))).is_err());
assert!(wire::parse(&format!("[{}]", vec!["0"; wire::MAX_ELEMENTS].join(","))).is_err());
}
#[test]
fn encoder_enforces_limits_and_preserves_raw_json_in_api_envelope() {
let raw = "1234567890123456789012345678901234567890";
let mut writer = wire::Encoder::default();
writer.begin_object().unwrap();
writer.key("raw").unwrap();
writer.json(raw).unwrap();
writer.key("when").unwrap();
writer.text(&time::format_datetime(-1).unwrap()).unwrap();
writer.end().unwrap();
let response = dever_runtime::api::success(writer.finish().unwrap());
let text = std::str::from_utf8(response.body.values()).unwrap();
assert_eq!(response.status, 200);
assert_eq!(
text,
format!(
"{{\"code\":0,\"message\":\"ok\",\"data\":{{\"raw\":{raw},\"when\":\"1969-12-31T23:59:59.999Z\"}}}}"
)
);
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(wire::Encoder::default().float(value).is_err());
}
let mut writer = wire::Encoder::default();
writer.begin_array().unwrap();
for _ in 1..wire::MAX_ELEMENTS {
writer.null().unwrap();
}
assert!(writer.null().is_err());
assert!(wire::Encoder::default().json(r#"{"x":1,"x":2}"#).is_err());
let mut writer = wire::Encoder::default();
writer.begin_object().unwrap();
writer.key("missing").unwrap();
assert!(writer.end().is_err());
assert!(writer.finish().is_err());
let mut writer = wire::Encoder::default();
writer.begin_array().unwrap();
writer.text(&"x".repeat(wire::MAX_BYTES - 3)).unwrap();
assert!(writer.end().is_err());
assert!(writer.finish().is_err());
let mut writer = wire::Encoder::default();
writer.begin_array().unwrap();
writer.text(&"x".repeat(wire::MAX_BYTES - 4)).unwrap();
assert!(writer.int(1).is_err());
assert!(writer.end().is_err());
assert!(writer.finish().is_err());
}
const TYPES: &str = r#"
public type Entry { label: Text
count: Int? }
public type Payload {
active: Bool
count: Int
ratio: Float
amount: Decimal
label: Text
key: Id
uuid: Uuid
instant: DateTime
date: Date
clock: Time
elapsed: Duration
raw: Json
entries: List<Entry>
}
public type Credentials { password: Secret }
public main() () {}
"#;
#[test]
fn schema_policy_and_field_contracts_have_one_owner() {
let program = support::checked(&support::sources(TYPES));
let output = program.wire_schema("main.Payload", Policy::Output).unwrap();
let input = program
.wire_schema("main.Payload", Policy::ApiInput)
.unwrap();
let job = program.wire_schema("main.Payload", Policy::Job).unwrap();
assert_ne!(output.fingerprint(), input.fingerprint());
assert_ne!(output.fingerprint(), job.fingerprint());
let shifted = support::checked(&support::sources(&format!(
"public type Unrelated {{ value: Text }}\n{TYPES}"
)));
assert_eq!(
output.fingerprint(),
shifted
.wire_schema("main.Payload", Policy::Output)
.unwrap()
.fingerprint()
);
for policy in [Policy::ApiInput, Policy::SettingInput] {
let schema = program.wire_schema("main.Credentials", policy).unwrap();
let code = dever_core::native::wire::emit(&schema, 0);
assert!(code.contains("Secret::from_input"));
assert!(!code.contains("_encode"));
}
for policy in [Policy::Output, Policy::Job] {
assert!(program.wire_schema("main.Credentials", policy).is_err());
}
for declaration in [
"public type Bad { private name: Text }",
"public type Bad { port: Int >= 1 and <= 65535 }",
"public type Bad { stream: Stream<Int> }",
"public type Bad { values: Map<Text, Int> }",
"public type Bad { One\nTwo }",
] {
let program = support::checked(&support::sources(&format!(
"{declaration}\npublic main() () {{}}"
)));
for policy in [
Policy::Output,
Policy::ApiInput,
Policy::SettingInput,
Policy::Job,
] {
assert!(
program.wire_schema("main.Bad", policy).is_err(),
"{declaration}"
);
}
}
let mut bounded = support::sources("main() () {}");
bounded.add(
"counter/api/value.dever",
"get_value() (response: Int >= 0) { response = 1 }",
);
assert!(
dever_core::check(&bounded)
.unwrap_err()
.iter()
.any(|error| error
.message
.contains("wire fields must be public and unconstrained"))
);
}
#[test]
fn wire_schemas_do_not_change_model_time_storage_identity() {
let mut sources = support::sources("main() () { calendar.event.save() }");
sources.add(
"calendar/event/model.dever",
"type Event { instant: DateTime\ndate: Date\ntime: Time\nelapsed: Duration }",
);
sources.add(
"calendar/event/app.dever",
r#"type ClockView { instant: DateTime
date: Date
time: Time
elapsed: Duration }
save() () {
stored = model.create({
instant = dever.time.parse_datetime("1969-12-31T23:59:59.999Z")
date = dever.time.parse_date("2000-02-29")
time = dever.time.parse_time("23:59:59.999")
elapsed = dever.time.duration(-7)
})
}"#,
);
let program = support::checked(&sources);
let before = program.model_snapshot();
for (name, ty) in [
("instant", "DateTime"),
("date", "Date"),
("time", "Time"),
("elapsed", "Duration"),
] {
assert!(
before.contains(&format!("field {name} {ty} nullable=false")),
"{before}"
);
}
for policy in [
Policy::Output,
Policy::ApiInput,
Policy::SettingInput,
Policy::Job,
] {
program
.wire_schema("calendar.event.ClockView", policy)
.unwrap();
let id = program
.wire_schema("calendar.event.model.id", policy)
.unwrap();
assert!(
id.fingerprint()
.contains("ModelId(\"calendar.event.model.id\")")
);
let model = program
.wire_schema("calendar.event.model.Event", policy)
.unwrap();
assert!(
model
.fingerprint()
.contains("ModelId(\"calendar.event.model.id\")")
);
}
assert_eq!(before, program.model_snapshot());
let generated = dever_core::native::emit(&program, &sources, "main.main").unwrap();
assert!(generated.contains("f0: i64,\nf1: i64,\nf2: i64,\nf3: i64,"));
assert_eq!(
generated
.matches("dever_runtime::orm::Value::Int(p")
.count(),
4
);
}
#[test]
fn official_time_bridge_is_pure_and_matches_reference() {
let source = r#"public main() (stamp: Text, date: Text, clock: Text, delta: Duration, invalid: dever.time.DateResult) pure {
instant = dever.time.parse_datetime("1969-12-31T23:59:59.999Z")
stamp = dever.time.format_datetime(dever.time.add(instant, dever.time.duration(1)))
date = dever.time.format_date(dever.time.parse_date("2000-02-29"))
clock = dever.time.format_time(dever.time.parse_time("23:59:59.999"))
delta = dever.time.difference(dever.time.subtract(instant, dever.time.duration(1)), instant)
invalid = result(dever.time.parse_date("1900-02-29"))
}"#;
let sources = support::sources(source);
let program = support::checked(&sources);
let expected = "stamp = 1970-01-01T00:00:00Z\ndate = 2000-02-29\nclock = 23:59:59.999\ndelta = -1\ninvalid = dever.time.DateResult.Failed(invalid calendar Date)\n";
assert_eq!(support::stdout(source), expected);
let reference = dever_core::reference::evaluate(&program, "main.main").unwrap();
assert_eq!(reference.stdout, expected);
support::rejected(
"public main() (value: DateTime) pure { value = dever.time.now() }",
"pure",
);
}
#[test]
fn time_api_has_equal_plain_and_markdown_contracts() {
let code = "main() (value: Text) pure { value = dever.time.format_datetime(dever.time.parse_datetime(\"2000-02-29T08:00:00+08:00\")) }";
let plain = support::sources(code);
let mut markdown = dever_core::source::SourceMap::default();
markdown.add("main.dever.md", format!("# 时间转换\n\n验证 UTC 毫秒合同。\n\n- 包:`main`\n- 公开类型:无\n- 公开方法:无\n- 使用:无\n\n## 入口\n\n转换日期时间。\n\n- 函数:`main`\n- 输入:无\n- 输出:\n - `value: Text`:UTC 时间。\n\n```dever\n{code}\n```\n"));
let plain_program = support::checked(&plain);
let markdown_program = support::checked(&markdown);
assert_eq!(
plain_program.api_snapshot(),
markdown_program.api_snapshot()
);
let expected = "value = 2000-02-29T00:00:00Z\n";
for program in [&plain_program, &markdown_program] {
let output = dever_core::reference::evaluate(program, "main.main").unwrap();
assert!(output.fault.is_none());
assert_eq!(output.stdout, expected);
}
}
#[test]
fn generated_api_uses_shared_time_codec_without_a_listener() {
let mut sources = support::sources("main() () { dever.api.serve() }");
sources.add("calendar/api/time.dever", "get_now() (response: DateTime) { response = dever.time.parse_datetime(\"1970-01-01T08:00:00+08:00\") }\nget_again() (response: DateTime) { response = dever.time.parse_datetime(\"1969-12-31T23:59:59.999Z\") }");
let program = support::checked(&sources);
let mut code = dever_core::native::emit(&program, &sources, "main.main")
.unwrap()
.replace("fn main()", "fn unused_main()");
assert_eq!(code.matches("fn wire_0_encode").count(), 1);
assert!(!code.contains("fn wire_1_encode"));
code.push_str(r#"
fn main() {
let request = dever_runtime::http::Request {
method: "GET".into(), target: "/calendar/time/now".into(), headers: vec![], body: Bytes::from_text(""),
};
let response = task::run_entry(dever_runtime::api::scoped_request(request, api_route)).unwrap();
assert_eq!(response.status, 200);
assert_eq!(std::str::from_utf8(response.body.values()).unwrap(), "{\"code\":0,\"message\":\"ok\",\"data\":\"1970-01-01T00:00:00Z\"}");
}
"#);
compile_and_run(&code);
}
#[test]
fn generated_concrete_codec_roundtrips_and_rejects_bad_records() {
let sources = support::sources(TYPES);
let program = support::checked(&sources);
let mut code = dever_core::native::emit(&program, &sources, "main.main")
.unwrap()
.replace("fn main()", "fn unused_main()");
code.push_str(&dever_core::native::wire::emit(
&program.wire_schema("main.Payload", Policy::Output).unwrap(),
0,
));
code.push_str(&dever_core::native::wire::emit(
&program
.wire_schema("main.Credentials", Policy::SettingInput)
.unwrap(),
1,
));
code.push_str(r###"
fn main() {
let raw = r#"{"active":true,"count":-9223372036854775808,"ratio":-0.0,"amount":"1234567890123456789012345678901234","label":"你好","key":"9007199254740993","uuid":"12345678-1234-5678-9012-123456789abc","instant":"1969-12-31T23:59:59.999Z","date":"2000-02-29","clock":"23:59:59.999","elapsed":-7,"raw":1234567890123456789012345678901234567890,"entries":[{"label":"entry"}]}"#;
let decoded = wire_0_decode(raw).unwrap();
assert_eq!(decoded.f1, i64::MIN);
assert_eq!(decoded.f2.to_bits(), (-0.0f64).to_bits());
assert_eq!(decoded.f3, DecimalValue::parse("1234567890123456789012345678901234").unwrap());
assert_eq!(decoded.f4, "你好");
assert_eq!(decoded.f5.0, "9007199254740993");
assert_eq!(decoded.f6.to_string(), "12345678-1234-5678-9012-123456789abc");
assert_eq!(decoded.f7, -1);
assert_eq!(decoded.f8, 951782400000);
assert_eq!(decoded.f9, 86399999);
assert_eq!(decoded.f10, -7);
assert!(decoded.f12.values()[0].f1.is_none());
let encoded = wire_0_encode(&decoded).unwrap();
assert!(encoded.as_str().contains("1234567890123456789012345678901234567890"));
assert!(encoded.as_str().contains("1969-12-31T23:59:59.999Z"));
let again = wire_0_decode(encoded.as_str()).unwrap();
assert_eq!(encoded.as_str(), wire_0_encode(&again).unwrap().as_str());
assert!(wire_0_decode(&raw.replace("\"count\":-9223372036854775808,", "")).is_err());
assert!(wire_0_decode(&raw.replace("\"active\":true", "\"extra\":true,\"active\":true")).is_err());
assert!(wire_0_decode(&raw.replace("\"label\":\"entry\"", "\"label\":\"entry\",\"label\":\"again\"")).is_err());
assert!(wire_0_decode(&raw.replace("\"ratio\":-0.0", "\"ratio\":1e900")).is_err());
assert!(wire_1_decode(r#"{"password":"secret"}"#).is_ok());
assert!(wire_1_decode(r#"{"password":42}"#).is_err());
}
"###);
compile_and_run(&code);
}
fn compile_and_run(code: &str) {
let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let built = Command::new("cargo")
.current_dir(&workspace)
.args([
"build",
"--locked",
"--offline",
"-p",
"dever-runtime",
"--features",
"api",
"--config",
"profile.dev.debug=0",
"--message-format=json",
])
.output()
.unwrap();
assert!(
built.status.success(),
"{}",
String::from_utf8_lossy(&built.stderr)
);
let artifact = String::from_utf8(built.stdout)
.unwrap()
.lines()
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
.find(|value| {
value["reason"] == "compiler-artifact" && value["target"]["name"] == "dever_runtime"
})
.unwrap();
let library = artifact["filenames"]
.as_array()
.unwrap()
.iter()
.filter_map(|value| value.as_str())
.find(|name| name.ends_with(".rlib"))
.unwrap();
let directory = support::temp::TemporaryDirectory::new();
let source = directory.path().join("codec.rs");
let executable = directory.path().join("codec");
fs::write(&source, code).unwrap();
let dependencies = std::path::Path::new(library).parent().unwrap().join("deps");
let built = Command::new("rustc")
.args(["--edition=2024", "-C", "debuginfo=0", "--extern"])
.arg(format!("dever_runtime={library}"))
.arg("-L")
.arg(format!("dependency={}", dependencies.display()))
.arg(&source)
.arg("-o")
.arg(&executable)
.output()
.unwrap();
assert!(
built.status.success(),
"{}",
String::from_utf8_lossy(&built.stderr)
);
let result = Command::new(executable).output().unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
}