Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,304 @@
|
||||
use super::*;
|
||||
use crate::wire::document::{MAX_BYTES, MAX_VALUES};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Arguments {
|
||||
name: String,
|
||||
count: i64,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn form_decode_cost() {
|
||||
use std::{hint::black_box, time::Instant};
|
||||
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
|
||||
for bytes in [64, 4096, 65536] {
|
||||
let text = "a".repeat(bytes);
|
||||
for sample in 0..15 {
|
||||
for offset in 0..2 {
|
||||
let native = (sample + offset) % 2 == 1;
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..1000 {
|
||||
let fields = [
|
||||
("name".into(), black_box(&text).clone()),
|
||||
("count".into(), "-7".into()),
|
||||
("enabled".into(), "true".into()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let result: Arguments = if native {
|
||||
native_form(fields).unwrap()
|
||||
} else {
|
||||
Input::fields(fields).decode(crate::wire::decode).unwrap()
|
||||
};
|
||||
assert_eq!(result.count, -7);
|
||||
assert!(result.enabled);
|
||||
checksum += black_box(result.name.len());
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
let implementation = if native { "rust" } else { "dever" };
|
||||
println!("form_{bytes},{sample},{implementation},1000,{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn native_form(mut fields: BTreeMap<String, String>) -> Option<Arguments> {
|
||||
if fields.len() != 3 {
|
||||
return None;
|
||||
}
|
||||
let result = Arguments {
|
||||
name: fields.remove("name")?,
|
||||
count: fields.remove("count")?.parse().ok()?,
|
||||
enabled: fields.remove("enabled")?.parse().ok()?,
|
||||
};
|
||||
// 字段名固定;Int、Bool 和标点合计不超过 60 字节。
|
||||
if result.name.len().saturating_mul(6).saturating_add(60) > MAX_BYTES
|
||||
&& serde_json::to_vec(&result).ok()?.len() > MAX_BYTES
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_fields_use_target_types_without_url_decoding() {
|
||||
let fields = BTreeMap::from([
|
||||
("name".into(), "%FF+雪".into()),
|
||||
("count".into(), "-7".into()),
|
||||
("enabled".into(), "true".into()),
|
||||
]);
|
||||
let parsed: Arguments = Input::fields(fields.clone())
|
||||
.decode(crate::wire::decode)
|
||||
.unwrap();
|
||||
assert_eq!(parsed, native_form(fields.clone()).unwrap());
|
||||
assert_eq!(parsed.name, "%FF+雪");
|
||||
for (name, value) in [("count", "1.5"), ("enabled", "1"), ("extra", "x")] {
|
||||
let mut invalid = fields.clone();
|
||||
invalid.insert(name.into(), value.into());
|
||||
assert!(
|
||||
Input::fields(invalid.clone())
|
||||
.decode::<Arguments>(crate::wire::decode)
|
||||
.is_err()
|
||||
);
|
||||
assert!(native_form(invalid).is_none());
|
||||
}
|
||||
let mut missing = fields;
|
||||
missing.remove("enabled");
|
||||
assert!(
|
||||
Input::fields(missing)
|
||||
.decode::<Arguments>(crate::wire::decode)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
fn query_request(query: &str) -> Request {
|
||||
hyper::Request::builder()
|
||||
.uri(format!("/test?{query}"))
|
||||
.body(Default::default())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_fields_decode_without_changing_argument_rules() {
|
||||
let request = query_request("name=%E9%9B%AA+test&count=-7&enabled=true");
|
||||
assert_eq!(
|
||||
input(&request)
|
||||
.unwrap()
|
||||
.decode::<Arguments>(crate::wire::decode)
|
||||
.unwrap(),
|
||||
Arguments {
|
||||
name: "雪 test".into(),
|
||||
count: -7,
|
||||
enabled: true
|
||||
}
|
||||
);
|
||||
for query in [
|
||||
"name=x&name=y",
|
||||
"unknown=x",
|
||||
"count=1.5",
|
||||
"enabled=1",
|
||||
"name=%FF",
|
||||
"name=%",
|
||||
] {
|
||||
assert!(
|
||||
input(&query_request(query))
|
||||
.unwrap()
|
||||
.decode::<Arguments>(crate::wire::decode)
|
||||
.is_err(),
|
||||
"{query}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
input(&query_request("name=x"))
|
||||
.unwrap()
|
||||
.decode::<Arguments>(crate::wire::decode)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_budgets_count_json_escaping_and_root_value() {
|
||||
let mut fields = BTreeMap::new();
|
||||
fields.insert("text".into(), "\0".repeat(MAX_BYTES / 6));
|
||||
assert!(
|
||||
Input::fields(fields)
|
||||
.decode::<BTreeMap<String, String>>(crate::wire::decode)
|
||||
.is_err()
|
||||
);
|
||||
let fields = (0..MAX_VALUES - 1)
|
||||
.map(|index| (index.to_string(), "false".to_owned()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert!(
|
||||
Input::fields(fields.clone())
|
||||
.decode::<BTreeMap<String, String>>(crate::wire::decode)
|
||||
.is_ok()
|
||||
);
|
||||
let mut fields = fields;
|
||||
fields.insert("extra".into(), "true".to_owned());
|
||||
assert!(
|
||||
Input::fields(fields)
|
||||
.decode::<BTreeMap<String, String>>(crate::wire::decode)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_rejects_bad_trailing_optional_fields_and_non_finite_numbers() {
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Scalars {
|
||||
ratio: f64,
|
||||
tag: Option<String>,
|
||||
}
|
||||
for query in [
|
||||
"ratio=1&tag=%",
|
||||
"ratio=1&tag=%FF",
|
||||
"ratio=1&tag",
|
||||
"ratio=1&tag=a&tag=b",
|
||||
"ratio=1&extra=x",
|
||||
"ratio=NaN",
|
||||
"ratio=inf",
|
||||
"ratio=1e999",
|
||||
] {
|
||||
assert!(
|
||||
input(&query_request(query))
|
||||
.unwrap()
|
||||
.decode::<Scalars>(crate::wire::decode)
|
||||
.is_err(),
|
||||
"{query}"
|
||||
);
|
||||
}
|
||||
let result: Scalars = input(&query_request("ratio=-0&tag=%E9%9B%AA+test"))
|
||||
.unwrap()
|
||||
.decode(crate::wire::decode)
|
||||
.unwrap();
|
||||
assert_eq!(result.ratio.to_bits(), (-0.0f64).to_bits());
|
||||
assert_eq!(result.tag.as_deref(), Some("雪 test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_query_uses_exact_existing_json_byte_budget() {
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct TextArgument {
|
||||
text: String,
|
||||
}
|
||||
let overhead = serde_json::to_vec(&serde_json::json!({"text":""}))
|
||||
.unwrap()
|
||||
.len();
|
||||
for (plain, count) in [
|
||||
(true, MAX_BYTES / 6 + 1),
|
||||
(true, MAX_BYTES - overhead),
|
||||
(true, MAX_BYTES - overhead + 1),
|
||||
(false, (MAX_BYTES - overhead) / 6),
|
||||
(false, (MAX_BYTES - overhead) / 6 + 1),
|
||||
] {
|
||||
let text = if plain { "a" } else { "\0" }.repeat(count);
|
||||
let encoded = if plain { "a" } else { "%00" }.repeat(count);
|
||||
let source = serde_json::to_string(&serde_json::json!({"text": text})).unwrap();
|
||||
let expected = crate::wire::decode::<TextArgument>(&source);
|
||||
let fields = BTreeMap::from([("text".into(), text)]);
|
||||
let form = Input::fields(fields).decode::<TextArgument>(crate::wire::decode);
|
||||
assert_eq!(form.is_ok(), expected.is_ok());
|
||||
if let (Ok(form), Ok(expected)) = (&form, &expected) {
|
||||
assert_eq!(form.text, expected.text);
|
||||
}
|
||||
// 此处独立验证字段 codec 的 JSON 预算;超长 URI 会先被 HTTP 库拒绝。
|
||||
let query = format!("text={encoded}");
|
||||
let actual = Input(Data::Query(&query)).decode::<TextArgument>(crate::wire::decode);
|
||||
assert_eq!(
|
||||
actual.is_ok(),
|
||||
expected.is_ok(),
|
||||
"plain={plain}, count={count}"
|
||||
);
|
||||
if let (Ok(actual), Ok(expected)) = (actual, expected) {
|
||||
assert_eq!(actual.text, expected.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "calendar", feature = "uuid"))]
|
||||
#[test]
|
||||
fn query_uses_existing_scalar_codecs_and_optional_types() {
|
||||
use crate::{
|
||||
time::calendar::{Date, DateTime, Duration, Time},
|
||||
value::{decimal::Decimal, id::Id, uuid::Uuid},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Scalars {
|
||||
decimal: Decimal,
|
||||
id: Id,
|
||||
uuid: Uuid,
|
||||
date: Date,
|
||||
time: Time,
|
||||
datetime: DateTime,
|
||||
duration: Duration,
|
||||
count: Option<i64>,
|
||||
enabled: Option<bool>,
|
||||
ratio: Option<f64>,
|
||||
}
|
||||
|
||||
let fields = serde_json::json!({
|
||||
"decimal": "123456789.0123456789",
|
||||
"id": "雪",
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"date": "2026-10-26",
|
||||
"time": "12:34:56.789",
|
||||
"datetime": "2026-10-26T12:34:56Z",
|
||||
"duration": -1500,
|
||||
"count": 7,
|
||||
"enabled": true,
|
||||
"ratio": 1.25
|
||||
});
|
||||
let expected: Scalars = crate::wire::decode(&fields.to_string()).unwrap();
|
||||
let query = "decimal=123456789.0123456789&id=%E9%9B%AA&uuid=550e8400-e29b-41d4-a716-446655440000&date=2026-10-26&time=12:34:56.789&datetime=2026-10-26T12:34:56Z&duration=-1500&count=7&enabled=true&ratio=1.25";
|
||||
let actual: Scalars = input(&query_request(query))
|
||||
.unwrap()
|
||||
.decode(crate::wire::decode)
|
||||
.unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
for (valid, invalid) in [
|
||||
("123456789.0123456789", "NaN"),
|
||||
("550e8400-e29b-41d4-a716-446655440000", "bad-uuid"),
|
||||
("date=2026-10-26", "date=2026-02-30"),
|
||||
("time=12:34:56.789", "time=25:34:56.789"),
|
||||
("duration=-1500", "duration=1.5"),
|
||||
("count=7", "count=9223372036854775808"),
|
||||
("enabled=true", "enabled=yes"),
|
||||
("ratio=1.25", "ratio=NaN"),
|
||||
] {
|
||||
assert!(
|
||||
input(&query_request(&query.replace(valid, invalid)))
|
||||
.unwrap()
|
||||
.decode::<Scalars>(crate::wire::decode)
|
||||
.is_err(),
|
||||
"{invalid}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user