use crate::diagnostic::Severity;
use crate::ir;
use crate::parser;
use crate::symbol;
fn validate_errors(source: &str) -> Vec<String> {
let ast = parser::parse_pht(source, "test.pht").expect("parse failed");
let (table, sym_diags) = symbol::build(&ast, "test.pht");
assert!(
sym_diags.iter().all(|d| d.severity != Severity::Error),
"unexpected symbol errors: {sym_diags:#?}"
);
let (module, ir_diags) = ir::lower(&ast, &table, "test.pht");
assert!(
ir_diags.iter().all(|d| d.severity != Severity::Error),
"unexpected IR errors: {ir_diags:#?}"
);
let sem_diags = super::validate(&module, "test.pht");
sem_diags
.iter()
.filter(|d| d.severity == Severity::Error)
.map(|d| d.summary.clone())
.collect()
}
fn validate_ok(source: &str) {
let errors = validate_errors(source);
assert!(
errors.is_empty(),
"unexpected validation errors: {errors:#?}"
);
}
fn expect_error(source: &str, expected_substr: &str) {
let errors = validate_errors(source);
let has_match = errors.iter().any(|e| e.contains(expected_substr));
assert!(
has_match,
"expected error containing `{expected_substr}`, got: {errors:#?}"
);
}
#[test]
fn valid_csv_fixture() {
let source = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/valid/csv_basic.pht"
))
.expect("fixture file missing");
validate_ok(&source);
}
#[test]
fn valid_simple_type() {
validate_ok(
r#"
test/types:
Record:
name: required string,
@(name)
;
.
"#,
);
}
#[test]
fn valid_optional_with_ifset() {
validate_ok(
r#"
test/types:
Record:
name: required string,
footer: optional string,
@(name),
@ifset(footer) { @(footer) }
;
.
"#,
);
}
#[test]
fn valid_collection_with_join() {
validate_ok(
r#"
test/types:
Record:
tags: required string*,
@join(tags, ", ")
;
.
"#,
);
}
#[test]
fn valid_plural_with_join() {
validate_ok(
r#"
test/types:
Item plural Items:
value: required string,
@(value)
;
Container:
items: required Items,
sep: required string,
@join(items, sep)
;
.
"#,
);
}
#[test]
fn valid_ifnotempty_on_collection() {
validate_ok(
r#"
test/types:
Record:
tags: required string*,
@ifnotempty(tags) { @join(tags, ", ") }
;
.
"#,
);
}
#[test]
fn valid_eol_with_string_field() {
validate_ok(
r#"
test/types:
Record:
value: required string,
eol: required string,
@(value),
@eol(eol)
;
.
"#,
);
}
#[test]
fn valid_bare_eol() {
validate_ok(
r#"
test/types:
Record:
value: required string,
@(value),
@eol
;
.
"#,
);
}
#[test]
fn valid_text_only_render() {
validate_ok(
r#"
test/types:
Record:
value: required string,
"prefix: ",
@(value)
;
.
"#,
);
}
#[test]
fn error_emit_collection_field() {
expect_error(
r#"
test/types:
Record:
tags: required string*,
@(tags)
;
.
"#,
"cannot directly emit collection field `tags`",
);
}
#[test]
fn error_emit_plural_field() {
expect_error(
r#"
test/types:
Item plural Items:
value: required string,
@(value)
;
Container:
items: required Items,
@(items)
;
.
"#,
"cannot directly emit collection field `items`",
);
}
#[test]
fn error_emit_one_or_more_field() {
expect_error(
r#"
test/types:
Record:
tags: required string+,
@(tags)
;
.
"#,
"cannot directly emit collection field `tags`",
);
}
#[test]
fn error_emit_optional_outside_ifset() {
expect_error(
r#"
test/types:
Record:
name: required string,
footer: optional string,
@(name),
@(footer)
;
.
"#,
"cannot directly emit optional field `footer`",
);
}
#[test]
fn valid_emit_optional_inside_ifset() {
validate_ok(
r#"
test/types:
Record:
name: required string,
footer: optional string,
@(name),
@ifset(footer) { @(footer) }
;
.
"#,
);
}
#[test]
fn error_ifset_on_required_field() {
expect_error(
r#"
test/types:
Record:
name: required string,
@ifset(name) { @(name) }
;
.
"#,
"@ifset on non-optional field `name`",
);
}
#[test]
fn error_ifnotempty_on_scalar_field() {
expect_error(
r#"
test/types:
Record:
name: required string,
@ifnotempty(name) { @(name) }
;
.
"#,
"@ifnotempty on non-collection field `name`",
);
}
#[test]
fn error_join_separator_non_string() {
expect_error(
r#"
test/types:
Record:
items: required string*,
count: required int64,
@join(items, count)
;
.
"#,
"@join separator field `count`",
);
}
#[test]
fn error_join_separator_collection() {
expect_error(
r#"
test/types:
Record:
items: required string*,
seps: required string*,
@join(items, seps)
;
.
"#,
"must be singular",
);
}
#[test]
fn error_join_on_scalar_field() {
expect_error(
r#"
test/types:
Record:
name: required string,
@join(name, ", ")
;
.
"#,
"@join field `name` in type `Record` is not a collection",
);
}
#[test]
fn error_direct_cycle() {
expect_error(
r#"
test/types:
Node:
child: required Node,
@(child)
;
.
"#,
"cyclic type definition involving `Node`",
);
}
#[test]
fn error_indirect_cycle() {
expect_error(
r#"
test/types:
A:
b: required B,
@(b)
;
B:
a: required A,
@(a)
;
.
"#,
"cyclic type definition",
);
}
#[test]
fn valid_optional_breaks_cycle() {
validate_ok(
r#"
test/types:
Node:
name: required string,
child: optional Node,
@(name),
@ifset(child) { @(child) }
;
.
"#,
);
}
#[test]
fn valid_collection_breaks_cycle() {
validate_ok(
r#"
test/types:
Node:
name: required string,
children: required Node*,
@(name),
@ifnotempty(children) { @join(children, ", ") }
;
.
"#,
);
}
#[test]
fn error_empty_render_body() {
}
#[test]
fn error_eol_with_non_string_field() {
expect_error(
r#"
test/types:
Record:
value: required string,
count: required int64,
@(value),
@eol(count)
;
.
"#,
"@eol field `count` in type `Record` must be a string",
);
}
#[test]
fn valid_conditional_ref_optional() {
validate_ok(
r#"
test/types:
Record:
name: required string,
desc: optional string,
@(name),
@(desc)?
;
.
"#,
);
}
#[test]
fn valid_conditional_ref_optional_block() {
validate_ok(
r###"
test/types:
Record:
subtitle: optional string,
@(subtitle)? { "## ", @(subtitle) }
;
.
"###,
);
}
#[test]
fn valid_conditional_ref_collection() {
validate_ok(
r#"
test/types:
Record:
tags: required string*,
@(tags)? { @join(tags, ", ") }
;
.
"#,
);
}
#[test]
fn valid_conditional_join() {
validate_ok(
r#"
test/types:
Record:
tags: required string*,
@join(tags, ", ")?
;
.
"#,
);
}
#[test]
fn error_conditional_ref_on_required() {
expect_error(
r#"
test/types:
Record:
name: required string,
@(name)?
;
.
"#,
"@ifset on non-optional field `name`",
);
}
#[test]
fn valid_nested_phenotype() {
validate_ok(
r#"
test/types:
Parent:
name: required string,
Child:
value: required string,
@(value)
;,
@(name)
;
.
"#,
);
}
#[test]
fn valid_nested_with_plural() {
validate_ok(
r#"
test/types:
Container:
name: required string,
Item plural Items:
value: required string,
@(value)
;,
@(name)
;
.
"#,
);
}