use std::fs;
use std::path::Path;
use typespec_rs::checker::Checker;
use typespec_rs::diagnostics::Diagnostic;
use typespec_rs::parser::parse;
#[derive(Debug, PartialEq)]
enum ExpectedDiag {
Error(String),
Warning(String),
}
fn parse_expectations(source: &str) -> (Vec<ExpectedDiag>, bool) {
let mut expectations = Vec::new();
let mut expect_clean = false;
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("// expect-error(") {
if let Some(code) = rest.strip_suffix(')') {
expectations.push(ExpectedDiag::Error(code.to_string()));
}
} else if let Some(rest) = trimmed.strip_prefix("// expect-warning(") {
if let Some(code) = rest.strip_suffix(')') {
expectations.push(ExpectedDiag::Warning(code.to_string()));
}
} else if trimmed.contains("// expect-clean") {
expect_clean = true;
}
}
(expectations, expect_clean)
}
fn compile(source: &str) -> (Checker, Vec<Diagnostic>) {
let result = parse(source);
let parse_diags: Vec<Diagnostic> = result
.diagnostics
.iter()
.map(|pd| Diagnostic::error(pd.code, &pd.message))
.collect();
let mut checker = Checker::new();
checker.set_parse_result(result.root_id, result.builder);
checker.check_program();
(checker, parse_diags)
}
fn verify_expectations(source: &str, checker: &Checker, parse_diags: &[Diagnostic]) {
let (expectations, expect_clean) = parse_expectations(source);
let all_diags: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
if expect_clean || expectations.is_empty() {
assert!(
all_diags.is_empty(),
"Expected clean compilation, but got {} diagnostic(s):\n{:#?}",
all_diags.len(),
all_diags
);
return;
}
for expected in &expectations {
match expected {
ExpectedDiag::Error(code) => {
let found = all_diags.iter().any(|d| d.code == *code);
assert!(
found,
"Expected error diagnostic with code '{}', but not found.\nAll diagnostics: {:#?}",
code, all_diags
);
}
ExpectedDiag::Warning(code) => {
let found = all_diags.iter().any(|d| d.code == *code);
assert!(
found,
"Expected warning diagnostic with code '{}', but not found.\nAll diagnostics: {:#?}",
code, all_diags
);
}
}
}
}
fn run_scenario(name: &str) {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/e2e/scenarios")
.join(format!("{}.tsp", name));
let source = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to read scenario '{}': {}", name, e));
let (checker, parse_diags) = compile(&source);
verify_expectations(&source, &checker, &parse_diags);
}
#[test]
fn e2e_simple() {
run_scenario("simple");
}
#[test]
fn e2e_deprecated_warn() {
run_scenario("deprecated-warn");
}
#[test]
fn e2e_pet_store() {
run_scenario("pet-store");
}
#[test]
fn e2e_template_models() {
run_scenario("template-models");
}
#[test]
fn e2e_nested_namespaces() {
run_scenario("nested-namespaces");
}
#[test]
fn e2e_scalar_extends() {
run_scenario("scalar-extends");
}
#[test]
fn e2e_union_types() {
run_scenario("union-types");
}
#[test]
fn e2e_const_values() {
run_scenario("const-values");
}
#[test]
fn e2e_model_extends() {
run_scenario("model-extends");
}
#[test]
fn e2e_decorators() {
run_scenario("decorators");
}
#[test]
fn e2e_inline_simple_model() {
let source = "model Foo {}";
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
assert!(all.is_empty(), "Expected clean, got: {:#?}", all);
assert!(checker.declared_types.contains_key("Foo"));
}
#[test]
fn e2e_inline_deprecated_model_reference() {
let source = r#"
#deprecated "Deprecated"
model Foo {}
model Bar {
foo: Foo;
}
"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
let has_deprecated = all.iter().any(|d| d.code == "deprecated");
assert!(
has_deprecated,
"Expected deprecated warning, got: {:#?}",
all
);
}
#[test]
fn e2e_inline_model_with_basic_property_types() {
let source = r#"
model BasicTypes {
s: string;
i: int32;
b: boolean;
}
"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
assert!(all.is_empty(), "Expected clean, got: {:#?}", all);
assert!(checker.declared_types.contains_key("BasicTypes"));
}
#[test]
fn e2e_inline_enum_with_values() {
let source = r#"
enum Direction {
up: 1,
down: 2,
left: 3,
right: 4,
}
"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
assert!(all.is_empty(), "Expected clean, got: {:#?}", all);
assert!(checker.declared_types.contains_key("Direction"));
}
#[test]
fn e2e_inline_interface_with_operations() {
let source = r#"
interface Service {
getItem(id: string): string;
listItems(): string[];
createItem(item: string): void;
}
"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
assert!(all.is_empty(), "Expected clean, got: {:#?}", all);
assert!(checker.declared_types.contains_key("Service"));
}
#[test]
fn e2e_inline_template_instantiation() {
let source = r#"
model Page<T> {
item: T;
count: int32;
}
model StringPage is Page<string> {}
"#;
let (checker, _parse_diags) = compile(source);
assert!(checker.declared_types.contains_key("Page"));
assert!(checker.declared_types.contains_key("StringPage"));
}
#[test]
fn e2e_inline_const_assignability() {
let source = r#"const a: int32 = "abc";"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
let has_unassignable = all.iter().any(|d| d.code == "unassignable");
assert!(
has_unassignable,
"Expected unassignable diagnostic for string→int32, got: {:#?}",
all
);
}
#[test]
fn e2e_inline_namespace_with_types() {
let source = r#"
namespace Services {
model Error {
code: int32;
message: string;
}
}
"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
assert!(all.is_empty(), "Expected clean, got: {:#?}", all);
assert!(checker.declared_types.contains_key("Services"));
}
#[test]
fn e2e_inline_scalar_chain() {
let source = r#"
scalar uuid extends string;
scalar myId extends uuid;
"#;
let (checker, parse_diags) = compile(source);
let all: Vec<&Diagnostic> = parse_diags
.iter()
.chain(checker.diagnostics().iter())
.collect();
assert!(all.is_empty(), "Expected clean, got: {:#?}", all);
assert!(checker.declared_types.contains_key("uuid"));
assert!(checker.declared_types.contains_key("myId"));
}