use std::io::Write; use std::path::PathBuf; use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicUsize, Ordering}; static NEXT_FILE: AtomicUsize = AtomicUsize::new(0); struct SourceFile(PathBuf); impl SourceFile { fn new(content: &[u8]) -> Self { let path = std::env::temp_dir().join(format!( "aster-parse-{}-{}.endor", std::process::id(), NEXT_FILE.fetch_add(1, Ordering::Relaxed) )); std::fs::write(&path, content).unwrap(); Self(path) } } impl Drop for SourceFile { fn drop(&mut self) { let _ = std::fs::remove_file(&self.0); } } #[test] fn parse_command_prints_ast_without_executing_source() { let source = SourceFile::new("function 表示(): String { return 未定義() }".as_bytes()); let output = Command::new(env!("CARGO_BIN_EXE_lyra")) .arg("parse") .arg(&source.0) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stdout.contains("表示")); assert!(stdout.contains("未定義")); assert!(stdout.contains("Call")); } #[test] fn parse_command_reports_source_locations_and_rejects_invalid_utf8() { let source = SourceFile::new(b"function f(): Number {\n return )\n}"); let output = Command::new(env!("CARGO_BIN_EXE_lyra")) .arg("parse") .arg(&source.0) .output() .unwrap(); assert!(!output.status.success()); assert!(output.stdout.is_empty()); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.contains(":2:10:"), "{stderr}"); assert!(stderr.contains("expected")); let source = SourceFile::new(&[0xff]); let output = Command::new(env!("CARGO_BIN_EXE_lyra")) .arg("parse") .arg(&source.0) .output() .unwrap(); assert!(!output.status.success()); assert!(output.stdout.is_empty()); } fn parse_stdin(source: &[u8]) -> std::process::Output { let mut child = Command::new(env!("CARGO_BIN_EXE_lyra")) .args(["parse", "-"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let mut stdin = child.stdin.take().unwrap(); stdin.write_all(source).unwrap(); drop(stdin); child.wait_with_output().unwrap() } #[test] fn parse_command_reads_stdin_to_eof_and_reports_its_origin() { let output = parse_stdin("function 表示(): String { return \"日\" }".as_bytes()); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert!(String::from_utf8(output.stdout).unwrap().contains("表示")); let output = parse_stdin(b"function f(): Number {\n return )\n}"); assert!(!output.status.success()); assert!( String::from_utf8(output.stderr) .unwrap() .contains(":2:10:") ); let output = parse_stdin(&[0xff]); assert!(!output.status.success()); assert!( String::from_utf8(output.stderr) .unwrap() .contains(": invalid UTF-8 at byte 0") ); } #[test] fn parse_command_exports_versioned_json_for_expression_and_interactive_inputs() { for (mode, source) in [("expression", "f(\"日\")"), ("interactive", "let x = 1")] { let source = SourceFile::new(source.as_bytes()); let output = Command::new(env!("CARGO_BIN_EXE_lyra")) .args(["parse", "--json", "--mode", mode]) .arg(&source.0) .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); let document = frontend::ast::AstDocument::from_json(std::str::from_utf8(&output.stdout).unwrap()) .unwrap(); assert_eq!(document.schema, frontend::ast::AST_SCHEMA_ID); match (mode, document.root) { ("expression", frontend::ast::AstRoot::Expression(_)) | ("interactive", frontend::ast::AstRoot::Interactive(_)) => {} _ => panic!("incorrect parse mode"), } } let output = Command::new(env!("CARGO_BIN_EXE_lyra")) .args(["parse", "--mode", "unknown", "-"]) .output() .unwrap(); assert!(!output.status.success()); assert!(output.stdout.is_empty()); }