#[cfg(test)]
mod tests {
use std::{
fs,
path::{Path, PathBuf},
time::Duration,
};
use luau_analyze::{CancellationToken, CheckOptions, Checker, Severity};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Expectation {
Pass,
Fail,
}
#[test]
fn strict_type_mismatch_reports_error() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker
.check(
r#"
--!strict
local x: number = "hello"
"#,
)
.unwrap();
assert!(!result.is_ok(), "expected strict type mismatch");
assert!(
result
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
);
assert!(!result.timed_out);
assert!(!result.cancelled);
}
#[test]
fn strict_mode_is_enforced_without_hot_comment() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker
.check(
r#"
local x: number = "hello"
"#,
)
.unwrap();
assert!(!result.is_ok(), "strict type mismatch should be reported");
assert!(
result
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
);
}
#[test]
fn invalid_definitions_fail() {
let mut checker = Checker::new().expect("checker creation should succeed");
let invalid_defs = read_example("definitions/invalid_api.d.luau");
let error = checker
.add_definitions(&invalid_defs)
.expect_err("invalid definitions should fail");
let message = error.to_string();
assert!(!message.trim().is_empty());
assert!(message.contains("failed to load Luau definitions"));
}
#[test]
fn invalid_definitions_include_custom_label() {
let mut checker = Checker::new().expect("checker creation should succeed");
let invalid_defs = read_example("definitions/invalid_api.d.luau");
let error = checker
.add_definitions_with_name(&invalid_defs, "defs/invalid_api.d.luau")
.expect_err("invalid definitions should fail");
assert!(error.to_string().contains("defs/invalid_api.d.luau"));
}
#[test]
fn multiple_definition_labels_keep_all_types_available() {
let mut checker = Checker::new().expect("checker creation should succeed");
checker
.add_definitions_with_name("declare function alpha_id(): string", "defs/alpha.d.luau")
.expect("alpha definitions should load");
checker
.add_definitions_with_name("declare function beta_count(): number", "defs/beta.d.luau")
.expect("beta definitions should load");
let result = checker
.check(
r#"
--!strict
local id: string = alpha_id()
local count: number = beta_count()
"#,
)
.unwrap();
assert!(
result.is_ok(),
"both definition files should remain active: {result:#?}"
);
}
#[test]
fn definitions_change_check_behavior() {
let mut checker = checker_with_demo_definitions();
let ok_result = checker
.check(
r#"
--!strict
local todo = Todo.create():content("Review"):due("today"):save()
todo:complete()
"#,
)
.unwrap();
assert!(
ok_result.is_ok(),
"expected script to pass with valid API usage: {ok_result:#?}"
);
let bad_result = checker
.check(
r#"
--!strict
local todo = Todo.create():content("Review"):due(42):save()
"#,
)
.unwrap();
assert!(!bad_result.is_ok(), "expected type error for due(42)");
}
#[test]
fn checker_reuse_keeps_definitions() {
let mut checker = checker_with_demo_definitions();
let first = checker
.check(
r#"
--!strict
local _todo = Todo.create():content("one"):save()
"#,
)
.unwrap();
assert!(first.is_ok(), "first check should succeed");
let second = checker
.check(
r#"
--!strict
local _todo = Todo.create():content("two"):due(123):save()
"#,
)
.unwrap();
assert!(!second.is_ok(), "second check should fail");
let third = checker
.check(
r#"
--!strict
local id = make_id("todo")
local _: string = id
"#,
)
.unwrap();
assert!(third.is_ok(), "third check should still succeed");
}
#[test]
fn empty_script_is_ok() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker.check("").unwrap();
assert!(result.is_ok(), "empty script should not produce errors");
}
#[test]
fn syntax_error_is_reported() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker
.check(
r#"
--!strict
local value: number =
"#,
)
.unwrap();
assert!(!result.is_ok(), "expected syntax error");
assert!(
result
.diagnostics
.iter()
.any(|diagnostic| !diagnostic.message.is_empty())
);
}
#[test]
fn timeout_marks_result_and_uses_module_label() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker
.check_with_options(
"--!strict\nlocal x = 1\n",
CheckOptions {
timeout: Some(Duration::ZERO),
module_name: Some("custom/module_timeout.luau"),
cancellation_token: None,
},
)
.unwrap();
assert!(result.timed_out, "expected timeout marker");
assert!(!result.is_ok(), "timeout should fail check");
assert!(
result
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("custom/module_timeout.luau"))
);
}
#[test]
fn cancellation_marks_result() {
let mut checker = Checker::new().expect("checker creation should succeed");
let token = CancellationToken::new().expect("token should be created");
token.cancel();
let result = checker
.check_with_options(
"--!strict\nlocal x = 1\n",
CheckOptions {
timeout: None,
module_name: Some("cancelled.luau"),
cancellation_token: Some(&token),
},
)
.unwrap();
assert!(result.cancelled, "expected cancelled marker");
assert!(!result.is_ok(), "cancelled check should fail");
assert!(
result
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("cancelled"))
);
}
#[test]
fn single_file_require_is_not_supported() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker
.check(
r#"
--!strict
local dep = require("./other_module")
local _: number = dep.value
"#,
)
.unwrap();
assert!(
!result.is_ok(),
"expected unresolved module diagnostic for single-file checker"
);
}
#[test]
fn diagnostics_are_sorted() {
let mut checker = Checker::new().expect("checker creation should succeed");
let result = checker
.check(
r#"
--!strict
local a: number = "x"
local b: number = "y"
"#,
)
.unwrap();
for pair in result.diagnostics.windows(2) {
let left = &pair[0];
let right = &pair[1];
let ordered = (left.line, left.col, left.severity, &left.message)
<= (right.line, right.col, right.severity, &right.message);
assert!(
ordered,
"diagnostics were not sorted: {left:?} then {right:?}"
);
}
}
#[test]
fn bundled_examples_match_expectations() {
let mut checker = checker_with_demo_definitions();
let scripts_dir = examples_root().join("scripts");
let mut scripts = collect_scripts_recursive(&scripts_dir)
.expect("scripts should be collected recursively");
scripts.sort();
let mut mismatches = Vec::new();
for script in scripts {
let source = fs::read_to_string(&script).expect("example script should be readable");
let expected = parse_expectation(&source);
let result = checker.check(&source).unwrap();
let actual = if result.is_ok() {
Expectation::Pass
} else {
Expectation::Fail
};
if actual != expected {
mismatches.push(format!(
"{} expected {:?} got {:?}",
script.display(),
expected,
actual
));
}
}
assert!(
mismatches.is_empty(),
"script expectation mismatches:\n{}",
mismatches.join("\n")
);
}
#[test]
fn bundled_examples_match_workspace_examples() {
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples");
if !workspace_root.exists() {
return;
}
let bundled_root = examples_root();
let mut bundled_files = collect_scripts_recursive(&bundled_root)
.expect("bundled examples should be collected recursively")
.into_iter()
.map(|path| {
path.strip_prefix(&bundled_root)
.expect("bundled file should stay under bundled root")
.to_path_buf()
})
.collect::<Vec<_>>();
let mut workspace_files = collect_scripts_recursive(&workspace_root)
.expect("workspace examples should be collected recursively")
.into_iter()
.map(|path| {
path.strip_prefix(&workspace_root)
.expect("workspace file should stay under workspace root")
.to_path_buf()
})
.collect::<Vec<_>>();
bundled_files.sort();
workspace_files.sort();
assert_eq!(
bundled_files, workspace_files,
"bundled fixtures drifted from workspace examples"
);
for relative_path in bundled_files {
let bundled = fs::read_to_string(bundled_root.join(&relative_path))
.expect("bundled example should be readable");
let workspace = fs::read_to_string(workspace_root.join(&relative_path))
.expect("workspace example should be readable");
assert_eq!(
bundled,
workspace,
"bundled fixture `{}` drifted from workspace example",
relative_path.display()
);
}
}
fn checker_with_demo_definitions() -> Checker {
let mut checker = Checker::new().expect("checker creation should succeed");
let defs = read_example("definitions/api.d.luau");
checker
.add_definitions(&defs)
.expect("demo definitions should load");
checker
}
fn read_example(relative_path: &str) -> String {
let path = examples_root().join(relative_path);
fs::read_to_string(&path).unwrap_or_else(|error| {
panic!("failed to read `{}`: {error}", path.display());
})
}
fn examples_root() -> PathBuf {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/examples");
assert!(
root.exists(),
"examples root should exist at `{}`",
root.display()
);
root
}
fn parse_expectation(source: &str) -> Expectation {
for line in source.lines().take(10) {
let normalized = line.trim();
if let Some(rest) = normalized.strip_prefix("-- expect:") {
let marker = rest.trim();
if marker.eq_ignore_ascii_case("fail") || marker.eq_ignore_ascii_case("error") {
return Expectation::Fail;
}
if marker.eq_ignore_ascii_case("pass") || marker.eq_ignore_ascii_case("ok") {
return Expectation::Pass;
}
}
if !normalized.is_empty() && !normalized.starts_with("--") {
break;
}
}
Expectation::Pass
}
fn collect_scripts_recursive(root: &Path) -> Result<Vec<PathBuf>, String> {
let mut scripts = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in fs::read_dir(&dir).map_err(|error| {
format!("failed to read scripts dir `{}`: {error}", dir.display())
})? {
let entry =
entry.map_err(|error| format!("failed to read directory entry: {error}"))?;
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|ext| ext == "luau") {
scripts.push(path);
}
}
}
Ok(scripts)
}
}