// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2025 The Gleam contributors use std::{io::Read, process::Stdio}; use camino::{Utf8Path, Utf8PathBuf}; use gleam_core::{ build::{Runtime, Target}, io::Command, paths::ProjectPaths, }; use gleam_cli::{ fs, run::{self, Which}, }; fn run_and_produce_pretty_snapshot( target: Option, runtime: Option, project_directory: Utf8PathBuf, ) -> String { let project_root = fs::get_project_root(project_directory).expect("project root"); // Node has a bug with paths on Windows at the moment, so we have to // edit the path to work around it. // https://github.com/nodejs/node/issues/60435 #[cfg(windows)] let project_root = project_root .to_string() .strip_prefix(r"\\?\") .map(Utf8PathBuf::from) .unwrap_or(project_root); let paths = ProjectPaths::new(project_root); let output = run_and_capture_output(&paths, "main", target, runtime) // Since the echo output's contains a path we will replace the `\` with a `/` // so that the snapshot doesn't fail on Windows in CI. .replace("src\\", "src/"); let main_module_content = fs::read(paths.src_directory().join("main.gleam")).expect("read main module"); format!( "--- main.gleam ---------------------- {main_module_content} --- gleam run output ---------------- {output} " ) } fn run_and_capture_output( paths: &ProjectPaths, main_module: &str, target: Option, runtime: Option, ) -> String { fs::delete_directory(&paths.build_directory()).expect("delete build directory content"); let Command { program, args, env, cwd: _, stdio: _, } = run::setup( paths, vec![], target, runtime, Some(main_module.into()), Which::Src, true, ) .expect("run setup"); let mut process = std::process::Command::new(&program) .args(args) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::piped()) .envs(env.iter().map(|pair| (&pair.0, &pair.1))) .current_dir(paths.root()) .spawn() .unwrap_or_else(|e| panic!("Failed to spawn process '{}': {}", &program, &e)); let mut stderr = process.stderr.take().expect("take stderr"); let mut output = String::new(); let _ = stderr.read_to_string(&mut output).expect("read stderr"); let _ = process.wait().expect("run with no errors"); output } macro_rules! assert_echo { ($project_name: expr) => { let snapshot_name = snapshot_name(None, None, $project_name); insta::allow_duplicates! { assert_echo!(&snapshot_name, Some(Target::Erlang), None, $project_name); assert_echo!(&snapshot_name, Some(Target::JavaScript), Some(Runtime::Bun), $project_name); assert_echo!(&snapshot_name, Some(Target::JavaScript), Some(Runtime::Deno), $project_name); assert_echo!(&snapshot_name, Some(Target::JavaScript), Some(Runtime::NodeJs), $project_name); } }; ($target: expr, $project_name: expr) => { let snapshot_name = snapshot_name(Some($target), None, $project_name); match $target { Target::JavaScript => insta::allow_duplicates! { assert_echo!(&snapshot_name, Some($target), Some(Runtime::Bun), $project_name); assert_echo!(&snapshot_name, Some($target), Some(Runtime::Deno), $project_name); assert_echo!(&snapshot_name, Some($target), Some(Runtime::NodeJs), $project_name); }, Target::Erlang => { assert_echo!(&snapshot_name, Some($target), None, $project_name); } Target::Cranelift => { assert_echo!(&snapshot_name, Some($target), None, $project_name); } } }; ($snapshot_name: expr, $target: expr, $runtime: expr, $project_name: expr) => { let path = fs::canonicalise(&Utf8Path::new("../test-output/cases").join($project_name)) .expect("canonicalise path"); let output = run_and_produce_pretty_snapshot($target, $runtime, path); insta::assert_snapshot!($snapshot_name.to_string(), output); }; } fn snapshot_name(target: Option, runtime: Option, suffix: &str) -> String { let show_target = |target: Target| match target { Target::Erlang => "erlang", Target::JavaScript => "javascript", }; let show_runtime = |runtime: Runtime| match runtime { Runtime::NodeJs => "nodejs", Runtime::Deno => "deno", Runtime::Bun => "bun", }; let prefix = match (target, runtime) { (None, None) => "".into(), (None, Some(runtime)) => format!("{}-", show_runtime(runtime)), (Some(target), None) => format!("{}-", show_target(target)), (Some(target), Some(runtime)) => { format!("{}-{}-", show_target(target), show_runtime(runtime)) } }; format!("{prefix}{suffix}") } #[test] fn echo_bitarray() { assert_echo!(Target::JavaScript, "echo_bitarray"); assert_echo!(Target::Erlang, "echo_bitarray"); } #[test] fn echo_bool() { assert_echo!("echo_bool"); } #[test] fn echo_charlist() { assert_echo!("echo_charlist"); } #[test] fn echo_custom_type() { assert_echo!(Target::Erlang, "echo_custom_type"); assert_echo!(Target::JavaScript, "echo_custom_type"); } #[test] fn echo_dict() { assert_echo!("echo_dict"); } #[test] fn echo_float() { assert_echo!(Target::Erlang, "echo_float"); assert_echo!(Target::JavaScript, "echo_float"); } #[test] fn echo_function() { assert_echo!("echo_function"); } #[test] fn echo_importing_module_named_inspect() { assert_echo!("echo_importing_module_named_inspect"); } #[test] fn echo_int() { assert_echo!("echo_int"); } #[test] fn echo_list() { assert_echo!("echo_list"); } #[test] fn echo_nil() { assert_echo!("echo_nil"); } #[test] fn echo_string() { assert_echo!("echo_string"); } #[test] fn echo_tuple() { assert_echo!("echo_tuple"); } #[test] fn echo_non_record_atom_tag() { assert_echo!(Target::Erlang, "echo_non_record_atom_tag"); } #[test] fn echo_circular_reference() { assert_echo!(Target::JavaScript, "echo_circular_reference"); } #[test] fn echo_singleton() { assert_echo!("echo_singleton"); } #[test] fn echo_with_message() { assert_echo!("echo_with_message"); } #[test] fn linked_process_exit() { assert_echo!(Target::Erlang, "linked_process_exit"); }