use std::{fs, path::Path};
#[test]
fn ui() {
let cases = trybuild::TestCases::new();
cases.compile_fail("tests/ui/macros/*.rs");
cases.compile_fail("tests/ui/schema/*.rs");
cases.compile_fail("tests/ui/antipattern/*.rs");
cases.compile_fail("tests/ui/traits/*.rs");
cases.pass("tests/ui/pass/*.rs");
}
#[test]
fn every_rejected_schema_type_has_a_case() {
const SOURCE: &str = include_str!("../src/schema/mod.rs");
let rows = rejection_table_rows(SOURCE);
assert!(
rows > 0,
"the rejection table in `schema/mod.rs` was not found; this test is looking for the \
heading `# Types deliberately left without an implementation`"
);
let cases = compile_fail_cases(&Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/ui/schema"));
assert_eq!(
cases, rows,
"`tests/ui/schema/` holds {cases} case(s) for {rows} row(s) of the rejection table in \
`schema/mod.rs`; a row added without a case leaves a documented refusal that nothing \
checks"
);
}
fn rejection_table_rows(source: &str) -> usize {
source
.lines()
.map(str::trim_start)
.skip_while(|line| !line.ends_with("# Types deliberately left without an implementation"))
.map(|line| line.trim_start_matches("//!").trim())
.skip_while(|line| !line.starts_with('|'))
.take_while(|line| line.starts_with('|'))
.skip(2)
.count()
}
fn compile_fail_cases(directory: &Path) -> usize {
fs::read_dir(directory)
.unwrap_or_else(|error| panic!("reading {}: {error}", directory.display()))
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().is_some_and(|it| it == "rs"))
.count()
}
#[test]
fn every_guided_diagnostic_has_a_snapshot() {
const RECORDED: &[(&str, &str)] = &[
(
"AdmitsAny",
"macros/schema_field_skipped_on_read_beside_open_map.stderr",
),
("Alternative", "traits/alternative.stderr"),
("ByteSource", "traits/byte_source.stderr"),
("Carries", "traits/carries.stderr"),
(
"ClosedFlatten",
"macros/schema_flatten_internally_tagged_denying_unknown_fields.stderr",
),
("Describe", "traits/describe.stderr"),
("EndpointMeta", "traits/endpoint_meta.stderr"),
("Flatten", "traits/flatten.stderr"),
("FromRequest", "antipattern/raw_request_extractor.stderr"),
(
"FromRequestParts",
"antipattern/raw_header_map_extractor.stderr",
),
("Handler", "traits/handler.stderr"),
("IntoEndpoints", "traits/into_endpoints.stderr"),
("IntoResponse", "antipattern/bare_status_code.stderr"),
("Languages", "traits/languages.stderr"),
("MapKey", "traits/map_key.stderr"),
("OpenMap", "traits/open_map.stderr"),
("ParamValue", "macros/query_params_object_field.stderr"),
("Provides", "antipattern/inject_without_provider.stderr"),
("Rangeable", "traits/rangeable.stderr"),
("RequestContent", "traits/request_content.stderr"),
("Responses", "antipattern/problem_as_return_type.stderr"),
("Schema", "schema/serde_json_value.stderr"),
("ShortCircuit", "traits/short_circuit.stderr"),
];
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
for (trait_name, snapshot) in RECORDED {
let path = root.join("tests/ui").join(snapshot);
let recorded = fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("`{trait_name}` names {snapshot}, which is not there"));
assert!(
!recorded.trim().is_empty(),
"`{trait_name}`'s snapshot is empty, so it records no diagnostic"
);
}
let guided = guided_traits_in_source(&root.join("src"));
assert_eq!(
guided,
RECORDED.len(),
"`crates/kynos/src` guides {guided} trait(s) and {} have a snapshot; a diagnostic nobody \
snapshotted is a diagnostic nobody checked",
RECORDED.len()
);
}
fn guided_traits_in_source(directory: &Path) -> usize {
let mut found = 0;
for entry in fs::read_dir(directory).expect("the crate's own source is readable") {
let path = entry.expect("readable entry").path();
if path.is_dir() {
found += guided_traits_in_source(&path);
continue;
}
if path.extension().is_none_or(|extension| extension != "rs") {
continue;
}
found += fs::read_to_string(&path)
.expect("readable source")
.matches("#[diagnostic::on_unimplemented")
.count();
}
found
}
#[test]
fn every_builder_preserves_the_type_parameters() {
const PARAMETERS: usize = 4;
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut checked = 0;
for relative in ["src/router/mod.rs", "src/router/group.rs"] {
let path = root.join(relative);
let source = fs::read_to_string(&path).expect("the crate's own source is readable");
for (number, line) in source.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("//") {
continue;
}
for type_name in ["Router", "Group"] {
let opening = format!("-> {type_name}<");
let Some(at) = line.find(&opening) else {
continue;
};
let arguments = &line[at + opening.len()..];
let count = top_level_arguments(arguments).unwrap_or_else(|| {
panic!(
"{relative}:{} returns `{type_name}<` whose argument list does not close \
on one line; this test reads return types a line at a time",
number + 1
)
});
assert_eq!(
count,
PARAMETERS,
"{relative}:{} returns `{type_name}<` with {count} type argument(s) and \
`{type_name}` has {PARAMETERS}; the omitted one falls back to its default of \
`()`, which silently empties a phantom list the conflict check reads. Name \
all {PARAMETERS}, or return `Self`.",
number + 1
);
checked += 1;
}
}
}
assert!(
checked > 0,
"no `-> Router<` or `-> Group<` return type was found; this test is looking in \
`src/router/mod.rs` and `src/router/group.rs`, and finding none means it has stopped \
checking anything"
);
}
fn top_level_arguments(text: &str) -> Option<usize> {
let mut depth = 1usize;
let mut separators = 0;
for character in text.chars() {
match character {
'<' => depth += 1,
'>' => {
depth -= 1;
if depth == 0 {
return Some(separators + 1);
}
}
',' if depth == 1 => separators += 1,
_ => {}
}
}
None
}