#[path = "common/lsp.rs"] #[allow(dead_code)] mod support; use std::path::Path; use std::process::{Command, Output}; use support::{avril, backend, files}; fn check(root: &Path, backend: &Path, args: &[&str]) -> Output { Command::new(avril()) .current_dir(root) .args([ "check", &format!("--typescript={}", backend.display()), "--offline", ]) .args(args) .output() .unwrap() } #[test] fn generated_fjson_declarations_are_available_to_check() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; let dir = tempfile::tempdir().unwrap(); let root = dir.path(); files( root, &[ ("avril.conf", "{}"), ( "main.ts", r#" import {apply, parse, stringify} from 'avril:fjson'; const value: unknown = parse('answer: 42'); const source: string = stringify(value); const result: string = apply(source, {answer: 43}); "#, ), ], ); let clean = check(root, &backend, &["main.ts"]); assert!( clean.status.success(), "{}{}", String::from_utf8_lossy(&clean.stdout), String::from_utf8_lossy(&clean.stderr), ); files( root, &[( "main.ts", r#" import {apply, parse} from 'avril:fjson'; const value: string = parse('answer: 42'); parse(123); apply('', 123); "#, )], ); let broken = check(root, &backend, &["main.ts"]); let stdout = String::from_utf8(broken.stdout).unwrap(); assert!(!broken.status.success(), "{stdout}"); assert!(stdout.contains("error TS2322:"), "{stdout}"); assert!(stdout.contains("error TS2345:"), "{stdout}"); } #[test] fn generated_test_declarations_are_available_to_check() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; let dir = tempfile::tempdir().unwrap(); let root = dir.path(); files( root, &[ ("avril.conf", "{}"), ( "main.ts", r#" import { test, type TestContext, type TestOptions } from "avril:test"; const options: TestOptions = { name: "options", fn: () => {} }; test(options); test("named", async (t) => { const context: TestContext = t; const passed: boolean = await context.step("nested", (child) => { const name: string = child.name; }); }); test.only(options); test.only("focused", () => {}); test.ignore(options); test.ignore("ignored"); "#, ), ], ); let clean = check(root, &backend, &["main.ts"]); assert!( clean.status.success(), "{}{}", String::from_utf8_lossy(&clean.stdout), String::from_utf8_lossy(&clean.stderr), ); files( root, &[( "main.ts", r#" import { test, runTests } from "avril:test"; const result: string = test("name", () => {}); "#, )], ); let broken = check(root, &backend, &["main.ts"]); let stdout = String::from_utf8(broken.stdout).unwrap(); assert!(!broken.status.success(), "{stdout}"); assert!(stdout.contains("error TS2322:"), "{stdout}"); assert!(stdout.contains("error TS2305:"), "{stdout}"); files( root, &[( "main.ts", r#" // @ts-expect-error TypeScript suppression cannot grant private-module access. import { op_test_event } from "ext:core/ops"; "#, )], ); let denied = check(root, &backend, &["main.ts"]); let output = format!( "{}{}", String::from_utf8_lossy(&denied.stdout), String::from_utf8_lossy(&denied.stderr), ); assert!(!denied.status.success(), "{output}"); assert!(output.contains("Extension modules are private"), "{output}"); } #[test] fn type_checks_the_module_graph_without_executing_it() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; let dir = tempfile::tempdir().unwrap(); let root = dir.path(); files( root, &[ ("avril.conf", "{}"), ("dep.ts", "export const value = 42 as const;"), ( "main.ts", "import {value} from \"./dep.ts\";\nconst checked: number = value;\nchecked;\nthrow Error(\"must not execute\");", ), ], ); let clean = check(root, &backend, &["main.ts"]); assert!( clean.status.success(), "{}", String::from_utf8_lossy(&clean.stderr) ); assert!(clean.stdout.is_empty()); files( root, &[( "main.ts", "import {value} from \"./dep.ts\";\nconst checked: string = value;\nchecked;\nthrow Error(\"must not execute\");", )], ); // The separate-value form works like `avril lsp --typescript PATH`. let broken = Command::new(avril()) .current_dir(root) .args([ "check", "--typescript", backend.to_str().unwrap(), "--offline", "main.ts", ]) .output() .unwrap(); let stdout = String::from_utf8(broken.stdout).unwrap(); assert!(!broken.status.success(), "{stdout}"); assert!(stdout.contains("main.ts:2:"), "{stdout}"); assert!(stdout.contains("error TS2322:"), "{stdout}"); files(root, &[("main.ts", "import \"./missing.ts\";")]); let unresolved = check(root, &backend, &["main.ts"]); let stdout = String::from_utf8(unresolved.stdout).unwrap(); assert!(!unresolved.status.success(), "{stdout}"); assert!(stdout.contains("./missing.ts"), "{stdout}"); // The entry grant does not extend to dependencies under `-0`: the // denied dependency has no source in the program, but its diagnostic // must still fail the check. files( root, &[( "main.ts", "import {value} from \"./dep.ts\";\nconst checked: number = value;\nchecked;", )], ); let denied = check(root, &backend, &["-0", "main.ts"]); let stdout = String::from_utf8(denied.stdout).unwrap(); assert!(!denied.status.success(), "{stdout}"); assert!(stdout.contains("PermissionDenied"), "{stdout}"); }