From 04af8f6486f844cabe2f5e6a70978795e6623c45 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 13:50:32 -0700 Subject: [PATCH 1/7] [bfops/cargo-unity-test]: WIP --- .github/workflows/ci.yml | 90 +---- Cargo.lock | 12 + Cargo.toml | 1 + crates/unity-tests/Cargo.toml | 19 ++ crates/unity-tests/tests/unity.rs | 537 ++++++++++++++++++++++++++++++ tools/ci/README.md | 12 + tools/ci/src/main.rs | 32 ++ 7 files changed, 620 insertions(+), 83 deletions(-) create mode 100644 crates/unity-tests/Cargo.toml create mode 100644 crates/unity-tests/tests/unity.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9064fa6b0b6..056c650f628 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -608,38 +608,11 @@ jobs: id: checkout-stdb uses: actions/checkout@v4 - # Run cheap .NET tests first. If those fail, no need to run expensive Unity tests. - - name: Setup dotnet uses: actions/setup-dotnet@v3 with: global-json-file: global.json - - name: Override NuGet packages - run: | - dotnet pack crates/bindings-csharp/BSATN.Runtime - dotnet pack crates/bindings-csharp/Runtime - - # Write out the nuget config file to `nuget.config`. This causes the spacetimedb-csharp-sdk repository - # to be aware of the local versions of the `bindings-csharp` packages in SpacetimeDB, and use them if - # available. Otherwise, `spacetimedb-csharp-sdk` will use the NuGet versions of the packages. - # This means that (if version numbers match) we will test the local versions of the C# packages, even - # if they're not pushed to NuGet. - # See https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file for more info on the config file. - cd sdks/csharp - ./tools~/write-nuget-config.sh ../.. - - - name: Restore .NET solution - working-directory: sdks/csharp - run: dotnet restore --configfile NuGet.Config SpacetimeDB.ClientSDK.sln - - # Now, setup the Unity tests. - - name: Patch spacetimedb dependency in Cargo.toml - working-directory: demo/Blackholio/server-rust - run: | - sed -i "s|spacetimedb *=.*|spacetimedb = \{ path = \"../../../crates/bindings\" \}|" Cargo.toml - cat Cargo.toml - - name: Install Rust toolchain uses: dsherret/rust-toolchain-file@v1 - name: Set default rust toolchain @@ -668,25 +641,11 @@ jobs: cargo build --release -p v8 fi - - name: Install SpacetimeDB CLI from the local checkout - run: | - export CARGO_HOME="$HOME/.cargo" - echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - cargo install --force --path crates/cli --locked --message-format=short - cargo install --force --path crates/standalone --locked --message-format=short - # Add a handy alias using the old binary name, so that we don't have to rewrite all scripts (incl. in submodules). - ln -sf $CARGO_HOME/bin/spacetimedb-cli $CARGO_HOME/bin/spacetime - - - name: Generate client bindings - working-directory: demo/Blackholio/server-rust - run: bash ./generate.sh -y - - - name: Check for changes - run: | - tools/check-diff.sh demo/Blackholio/client-unity/Assets/Scripts/autogen || { - echo 'Error: Bindings are dirty. Please run `demo/Blackholio/server-rust/generate.sh`.' - exit 1 - } + - uses: actions/cache@v3 + with: + path: demo/Blackholio/client-unity/Library + key: Unity-${{ github.head_ref }} + restore-keys: Unity- - name: Hydrate Unity SDK DLLs run: cargo ci dlls @@ -699,45 +658,10 @@ jobs: env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - name: Start SpacetimeDB - run: | - spacetime start & - disown - - - name: Publish unity-tests module to SpacetimeDB - working-directory: demo/Blackholio/server-rust - run: | - spacetime logout && spacetime login --server-issued-login local - bash ./publish.sh - - - name: Patch com.clockworklabs.spacetimedbsdk dependency in manifest.json - working-directory: demo/Blackholio/client-unity/Packages - run: | - yq e -i '.dependencies["com.clockworklabs.spacetimedbsdk"] = "file:../../../../sdks/csharp"' manifest.json - cat manifest.json - - - uses: actions/cache@v3 - with: - path: demo/Blackholio/client-unity/Library - key: Unity-${{ github.head_ref }} - restore-keys: Unity- - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - name: Run Unity tests - uses: game-ci/unity-test-runner@v4 - with: - unityVersion: 2022.3.32f1 # Adjust Unity version to a valid tag - projectPath: demo/Blackholio/client-unity # Path to the Unity project subdirectory - githubToken: ${{ secrets.GITHUB_TOKEN }} - testMode: playmode - useHostNetwork: true - artifactsPath: "" + run: cargo ci unity-tests --skip-dlls env: + UNITY_VERSION: 2022.3.32f1 UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} diff --git a/Cargo.lock b/Cargo.lock index a51c74638ff..ab08cfff389 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8812,6 +8812,18 @@ dependencies = [ "spacetimedb-language-test-support", ] +[[package]] +name = "spacetimedb-unity-tests" +version = "2.2.0" +dependencies = [ + "anyhow", + "clap 4.5.50", + "quick-xml 0.31.0", + "serde_json", + "spacetimedb-language-test-support", + "tempfile", +] + [[package]] name = "spacetimedb-update" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 511bf69e9c9..2e11c7f4b28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/language-test-support", "crates/typescript-tests", "crates/csharp-tests", + "crates/unity-tests", "crates/fs-utils", "crates/lib", "crates/metrics", diff --git a/crates/unity-tests/Cargo.toml b/crates/unity-tests/Cargo.toml new file mode 100644 index 00000000000..77b623f092f --- /dev/null +++ b/crates/unity-tests/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "spacetimedb-unity-tests" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[[test]] +name = "unity" +path = "tests/unity.rs" +harness = false + +[dev-dependencies] +anyhow.workspace = true +clap.workspace = true +quick-xml.workspace = true +serde_json.workspace = true +spacetimedb-language-test-support = { path = "../language-test-support" } +tempfile.workspace = true diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs new file mode 100644 index 00000000000..6a85da7d4ad --- /dev/null +++ b/crates/unity-tests/tests/unity.rs @@ -0,0 +1,537 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::io; +use std::net::{SocketAddr, TcpStream}; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus, Stdio}; +use std::time::{Duration, Instant}; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::Parser; +use quick_xml::events::{BytesStart, Event}; +use quick_xml::Reader; +use serde_json::Value; +use spacetimedb_language_test_support::{print_results, target_dir, workspace_root, Outcome, TestCaseResult}; +use tempfile::TempDir; + +const UNITY_VERSION: &str = "2022.3.32f1"; +const SDK_PACKAGE: &str = "com.clockworklabs.spacetimedbsdk"; +const SDK_PACKAGE_PATH: &str = "file:../../../../sdks/csharp"; + +#[derive(Parser)] +struct Args { + #[arg(long)] + unity_path: Option, + #[arg(long)] + filter: Option, + #[arg(long, alias = "list-tests")] + list: bool, + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + passthrough: Vec, +} + +fn main() { + if let Err(error) = run() { + eprintln!("{error:?}"); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = Args::parse(); + let workspace = workspace_root(); + let server_dir = workspace.join("demo/Blackholio/server-rust"); + let unity_project_dir = workspace.join("demo/Blackholio/client-unity"); + + if args.list { + list_unity_tests(&unity_project_dir)?; + return Ok(()); + } + + let unity_path = find_unity(args.unity_path.as_deref())?; + let spacetime_bin = SpacetimeBin::prepare()?; + let _server_cargo_restore = patch_blackholio_server_manifest(&server_dir.join("Cargo.toml"))?; + let _unity_manifest_restore = patch_unity_package_manifest(&unity_project_dir.join("Packages/manifest.json"))?; + + run_command( + &server_dir, + "bash", + &["./generate.sh".into(), "-y".into()], + Some(spacetime_bin.path_env()), + ) + .context("failed to generate Blackholio Unity bindings")?; + + run_command( + &workspace, + "bash", + &[ + "tools/check-diff.sh".into(), + "demo/Blackholio/client-unity/Assets/Scripts/autogen".into(), + ], + None, + ) + .context("generated Blackholio Unity bindings differ from the checked-in files")?; + + let _server = SpacetimeServer::start(&server_dir, spacetime_bin.path_env())?; + + run_command( + &server_dir, + "spacetime", + &["logout".into()], + Some(spacetime_bin.path_env()), + ) + .context("failed to log out of local SpacetimeDB")?; + run_command( + &server_dir, + "spacetime", + &["login".into(), "--server-issued-login".into(), "local".into()], + Some(spacetime_bin.path_env()), + ) + .context("failed to log in to local SpacetimeDB")?; + run_command( + &server_dir, + "bash", + &["./publish.sh".into()], + Some(spacetime_bin.path_env()), + ) + .context("failed to publish the Blackholio module")?; + + run_unity_tests( + &unity_path, + &unity_project_dir, + args.filter.as_deref(), + &args.passthrough, + ) +} + +fn run_unity_tests(unity_path: &Path, project_dir: &Path, filter: Option<&str>, passthrough: &[String]) -> Result<()> { + let out_dir = target_dir().join("unity-tests"); + fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; + let results_path = out_dir.join("results.xml"); + let log_path = out_dir.join("unity.log"); + + let mut args = vec![ + "-batchmode".to_string(), + "-nographics".to_string(), + "-quit".to_string(), + "-projectPath".to_string(), + project_dir.display().to_string(), + "-runTests".to_string(), + "-testPlatform".to_string(), + "playmode".to_string(), + "-testResults".to_string(), + results_path.display().to_string(), + "-logFile".to_string(), + log_path.display().to_string(), + ]; + if let Some(filter) = filter { + args.push("-testFilter".to_string()); + args.push(filter.to_string()); + } + args.extend_from_slice(passthrough); + + let status = Command::new(unity_path) + .args(&args) + .status() + .with_context(|| format!("failed to run {}", unity_path.display()))?; + + if results_path.exists() { + let results = parse_unity_results(&results_path)?; + print_results("unity playmode", &results_path, &results)?; + } else if !status.success() && log_path.exists() { + print_log_excerpt(&log_path)?; + } + + ensure_success(status, &shell_line(unity_path, &args)) +} + +fn find_unity(explicit_path: Option<&Path>) -> Result { + if let Some(path) = explicit_path { + if path.exists() { + return Ok(path.to_path_buf()); + } + bail!("Unity executable does not exist: {}", path.display()); + } + + for var in ["UNITY_PATH", "UNITY_EXECUTABLE"] { + if let Some(path) = env::var_os(var).map(PathBuf::from).filter(|path| path.exists()) { + return Ok(path); + } + } + + for name in ["unity", "Unity", "unity-editor"] { + if let Some(path) = find_on_path(name) { + return Ok(path); + } + } + + let version = env::var("UNITY_VERSION").unwrap_or_else(|_| UNITY_VERSION.to_string()); + for path in [ + PathBuf::from(format!("/opt/unity/editors/{version}/Editor/Unity")), + PathBuf::from(format!("/opt/Unity/Hub/Editor/{version}/Editor/Unity")), + PathBuf::from("/opt/unity/Editor/Unity"), + PathBuf::from("/opt/Unity/Editor/Unity"), + PathBuf::from(format!( + "/Applications/Unity/Hub/Editor/{version}/Unity.app/Contents/MacOS/Unity" + )), + ] { + if path.exists() { + return Ok(path); + } + } + + bail!( + "could not find Unity. Pass --unity-path, set UNITY_PATH or UNITY_EXECUTABLE, or install Unity {version} in a standard GitHub runner path" + ) +} + +fn find_on_path(name: &str) -> Option { + let path = env::var_os("PATH")?; + env::split_paths(&path) + .map(|dir| dir.join(name)) + .find(|candidate| candidate.exists()) +} + +struct SpacetimeBin { + _temp_dir: TempDir, + path_env: OsString, +} + +impl SpacetimeBin { + fn prepare() -> Result { + let temp_dir = tempfile::tempdir().context("failed to create temporary bin directory")?; + let release_dir = target_dir().join("release"); + let exe = env::consts::EXE_SUFFIX; + let cli = release_dir.join(format!("spacetimedb-cli{exe}")); + let standalone = release_dir.join(format!("spacetimedb-standalone{exe}")); + + ensure_exists(&cli, "release spacetimedb-cli")?; + ensure_exists(&standalone, "release spacetimedb-standalone")?; + + link_or_copy(&cli, &temp_dir.path().join(format!("spacetime{exe}")))?; + link_or_copy(&cli, &temp_dir.path().join(format!("spacetimedb-cli{exe}")))?; + link_or_copy( + &standalone, + &temp_dir.path().join(format!("spacetimedb-standalone{exe}")), + )?; + + let mut paths = vec![temp_dir.path().to_path_buf()]; + if let Some(path) = env::var_os("PATH") { + paths.extend(env::split_paths(&path)); + } + let path_env = env::join_paths(paths).context("failed to build PATH for SpacetimeDB binaries")?; + + Ok(Self { + _temp_dir: temp_dir, + path_env, + }) + } + + fn path_env(&self) -> &OsString { + &self.path_env + } +} + +fn ensure_exists(path: &Path, label: &str) -> Result<()> { + if path.exists() { + Ok(()) + } else { + bail!( + "missing {label} at {}. Run this through `cargo ci unity-tests` so CargoCI builds the required binaries first", + path.display() + ) + } +} + +#[cfg(unix)] +fn link_or_copy(src: &Path, dst: &Path) -> io::Result<()> { + std::os::unix::fs::symlink(src, dst) +} + +#[cfg(not(unix))] +fn link_or_copy(src: &Path, dst: &Path) -> io::Result<()> { + fs::copy(src, dst).map(|_| ()) +} + +struct SpacetimeServer { + child: std::process::Child, +} + +impl SpacetimeServer { + fn start(cwd: &Path, path_env: &OsString) -> Result { + let child = Command::new("spacetime") + .arg("start") + .current_dir(cwd) + .env("PATH", path_env) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .context("failed to start local SpacetimeDB server")?; + wait_for_port("127.0.0.1:3000".parse().unwrap(), Duration::from_secs(30))?; + Ok(Self { child }) + } +} + +impl Drop for SpacetimeServer { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +fn wait_for_port(addr: SocketAddr, timeout: Duration) -> Result<()> { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if TcpStream::connect_timeout(&addr, Duration::from_millis(250)).is_ok() { + return Ok(()); + } + std::thread::sleep(Duration::from_millis(250)); + } + bail!("SpacetimeDB did not start listening on {addr} within {timeout:?}") +} + +struct FileRestore { + path: PathBuf, + original: String, +} + +impl Drop for FileRestore { + fn drop(&mut self) { + let _ = fs::write(&self.path, &self.original); + } +} + +fn patch_blackholio_server_manifest(path: &Path) -> Result { + let original = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let replacement = r#"spacetimedb = { path = "../../../crates/bindings" }"#; + let mut replaced = false; + let mut patched = String::new(); + + for line in original.lines() { + if line.trim_start().starts_with("spacetimedb =") { + let indent_len = line.len() - line.trim_start().len(); + patched.push_str(&line[..indent_len]); + patched.push_str(replacement); + replaced = true; + } else { + patched.push_str(line); + } + patched.push('\n'); + } + + if !replaced { + bail!("could not find spacetimedb dependency in {}", path.display()); + } + + fs::write(path, patched).with_context(|| format!("failed to patch {}", path.display()))?; + Ok(FileRestore { + path: path.to_path_buf(), + original, + }) +} + +fn patch_unity_package_manifest(path: &Path) -> Result { + let original = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let mut manifest: Value = + serde_json::from_str(&original).with_context(|| format!("failed to parse {}", path.display()))?; + + let dependencies = manifest + .get_mut("dependencies") + .and_then(Value::as_object_mut) + .ok_or_else(|| anyhow!("{} does not contain a dependencies object", path.display()))?; + dependencies.insert(SDK_PACKAGE.to_string(), Value::String(SDK_PACKAGE_PATH.to_string())); + + let patched = format!("{}\n", serde_json::to_string_pretty(&manifest)?); + fs::write(path, patched).with_context(|| format!("failed to patch {}", path.display()))?; + Ok(FileRestore { + path: path.to_path_buf(), + original, + }) +} + +fn run_command(cwd: &Path, program: &str, args: &[String], path_env: Option<&OsString>) -> Result<()> { + let mut command = Command::new(program); + command.args(args).current_dir(cwd); + if let Some(path_env) = path_env { + command.env("PATH", path_env); + } + let status = command + .status() + .with_context(|| format!("failed to run {}", shell_line(program, args)))?; + ensure_success(status, &shell_line(program, args)) +} + +fn ensure_success(status: ExitStatus, command: &str) -> Result<()> { + if status.success() { + Ok(()) + } else { + bail!("command failed with {status}: {command}") + } +} + +fn parse_unity_results(path: &Path) -> Result> { + let mut reader = Reader::from_file(path).with_context(|| format!("failed to read {}", path.display()))?; + reader.trim_text(true); + let mut buf = Vec::new(); + let mut cases = Vec::new(); + let mut current_case: Option = None; + let mut in_message = false; + + loop { + match reader.read_event_into(&mut buf)? { + Event::Start(event) if event.name().as_ref() == b"test-case" => { + current_case = Some(test_case_from_event(&event)?); + } + Event::Empty(event) if event.name().as_ref() == b"test-case" => { + cases.push(test_case_from_event(&event)?); + } + Event::Start(event) if event.name().as_ref() == b"message" && current_case.is_some() => { + in_message = true; + } + Event::Text(event) if in_message => { + if let Some(case) = &mut current_case { + let message = String::from_utf8_lossy(event.as_ref()).into_owned(); + if !message.is_empty() { + case.message = Some(message); + } + } + } + Event::End(event) if event.name().as_ref() == b"message" => { + in_message = false; + } + Event::End(event) if event.name().as_ref() == b"test-case" => { + if let Some(case) = current_case.take() { + cases.push(case); + } + } + Event::Eof => break, + _ => {} + } + buf.clear(); + } + + Ok(cases) +} + +fn test_case_from_event(event: &BytesStart<'_>) -> Result { + let name = attr(event, b"fullname")? + .or(attr(event, b"name")?) + .unwrap_or_else(|| "".to_string()); + let result = attr(event, b"result")?.unwrap_or_else(|| "Unknown".to_string()); + let outcome = match result.as_str() { + "Passed" => Outcome::Passed, + "Skipped" | "Inconclusive" => Outcome::Skipped, + _ => Outcome::Failed, + }; + + Ok(TestCaseResult { + name, + outcome, + message: None, + }) +} + +fn attr(event: &BytesStart<'_>, key: &[u8]) -> Result> { + for attr in event.attributes() { + let attr = attr?; + if attr.key.as_ref() == key { + return Ok(Some(String::from_utf8_lossy(&attr.value).into_owned())); + } + } + Ok(None) +} + +fn print_log_excerpt(path: &Path) -> Result<()> { + let log = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + eprintln!( + "Unity did not write a test result file. Last log lines from {}:", + path.display() + ); + let lines: Vec<_> = log.lines().rev().take(80).collect(); + for line in lines.into_iter().rev() { + eprintln!("{line}"); + } + Ok(()) +} + +fn list_unity_tests(project_dir: &Path) -> Result<()> { + let tests_dir = project_dir.join("Assets/PlayModeTests"); + let mut tests = Vec::new(); + collect_unity_tests(&tests_dir, &mut tests)?; + tests.sort(); + for test in tests { + println!("{test}"); + } + Ok(()) +} + +fn collect_unity_tests(dir: &Path, tests: &mut Vec) -> Result<()> { + for entry in fs::read_dir(dir).with_context(|| format!("failed to read {}", dir.display()))? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + collect_unity_tests(&path, tests)?; + } else if path.extension().and_then(|ext| ext.to_str()) == Some("cs") { + collect_unity_tests_from_file(&path, tests)?; + } + } + Ok(()) +} + +fn collect_unity_tests_from_file(path: &Path, tests: &mut Vec) -> Result<()> { + let source = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let mut class_name = None; + let mut pending_test = false; + + for line in source.lines() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("public class ") { + class_name = rest + .split(|ch: char| ch == ':' || ch.is_whitespace()) + .next() + .map(str::to_string); + } + if trimmed.contains("[UnityTest]") || trimmed.contains("[Test]") { + pending_test = true; + continue; + } + if pending_test && trimmed.starts_with("public ") { + if let Some(name) = method_name(trimmed) { + if let Some(class_name) = &class_name { + tests.push(format!("{class_name}.{name}")); + } else { + tests.push(name.to_string()); + } + } + pending_test = false; + } + } + Ok(()) +} + +fn method_name(line: &str) -> Option<&str> { + let before_args = line.split_once('(')?.0.trim_end(); + before_args.split_whitespace().last() +} + +fn shell_line(program: impl AsRef, args: &[String]) -> String { + let mut command = shell_escape(program.as_ref().as_os_str().to_string_lossy().as_ref()); + for arg in args { + command.push(' '); + command.push_str(&shell_escape(arg)); + } + command +} + +fn shell_escape(arg: &str) -> String { + if arg.is_empty() + || arg + .chars() + .any(|ch| ch.is_whitespace() || matches!(ch, '\'' | '"' | '$' | '\\')) + { + format!("'{}'", arg.replace('\'', "'\\''")) + } else { + arg.to_string() + } +} diff --git a/tools/ci/README.md b/tools/ci/README.md index 9d1928edf73..997b08f1466 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -228,6 +228,18 @@ Usage: csharp-tests - `--help`: Print help +### `unity-tests` + +**Usage:** +```bash +Usage: unity-tests [OPTIONS] +``` + +**Options:** + +- `--skip-dlls`: Skip hydrating Unity SDK DLLs before running the Unity harness +- `--help`: Print help + ### `docs` **Usage:** diff --git a/tools/ci/src/main.rs b/tools/ci/src/main.rs index 4f9b5e5b655..95a81080fbe 100644 --- a/tools/ci/src/main.rs +++ b/tools/ci/src/main.rs @@ -278,6 +278,12 @@ enum CiCmd { TypescriptTest, /// Runs C# tests through the Cargo language-test harness. CsharpTests, + /// Runs Unity playmode tests through the Cargo language-test harness. + UnityTests { + /// Skip hydrating Unity SDK DLLs before running the Unity harness. + #[arg(long)] + skip_dlls: bool, + }, /// Builds the docs site. Docs, } @@ -481,6 +487,26 @@ fn run_csharp_tests() -> Result<()> { Ok(()) } +fn run_unity_tests(skip_dlls: bool) -> Result<()> { + cmd!( + "cargo", + "build", + "--release", + "-p", + "spacetimedb-cli", + "-p", + "spacetimedb-standalone", + "--features", + "spacetimedb-standalone/allow_loopback_http_for_tests" + ) + .run()?; + if !skip_dlls { + run_dlls()?; + } + cmd!("cargo", "test", "-p", "spacetimedb-unity-tests", "--test", "unity").run()?; + Ok(()) +} + fn run_docs_build() -> Result<()> { cmd!("pnpm", "install").dir("docs").run()?; cmd!("pnpm", "build").dir("docs").run()?; @@ -512,6 +538,8 @@ fn main() -> Result<()> { "spacetimedb-typescript-tests", "--exclude", "spacetimedb-csharp-tests", + "--exclude", + "spacetimedb-unity-tests", "--", "--test-threads=2", "--skip", @@ -763,6 +791,10 @@ fn main() -> Result<()> { run_csharp_tests()?; } + Some(CiCmd::UnityTests { skip_dlls }) => { + run_unity_tests(skip_dlls)?; + } + Some(CiCmd::Docs) => { run_docs_build()?; } From 4376ad80434ec081e254adecf6697f244ca3ae6d Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 14:03:20 -0700 Subject: [PATCH 2/7] [bfops/cargo-unity-test]: fix --- crates/unity-tests/tests/unity.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs index 6a85da7d4ad..19dc45edae6 100644 --- a/crates/unity-tests/tests/unity.rs +++ b/crates/unity-tests/tests/unity.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_macros)] + use std::env; use std::ffi::OsString; use std::fs; From 98f589b6c3b9fc95f09fa743416a0bdf0446a818 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 14:43:51 -0700 Subject: [PATCH 3/7] [bfops/cargo-unity-test]: do thing --- .github/workflows/ci.yml | 5 +- crates/unity-tests/tests/unity.rs | 84 ++++++++----------- .../PlayModeTests/PlayModeExampleTest.cs | 2 +- .../Assets/Scripts/GameManager.cs | 5 +- demo/Blackholio/server-rust/publish.sh | 4 +- 5 files changed, 48 insertions(+), 52 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 056c650f628..a873c5a4c00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -659,7 +659,10 @@ jobs: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - name: Run Unity tests - run: cargo ci unity-tests --skip-dlls + run: | + # `cargo ci dlls` above has already hydrated the local Unity SDK DLLs for the meta check. + # Avoid doing that same setup twice before running the Rust Unity harness. + cargo ci unity-tests --skip-dlls env: UNITY_VERSION: 2022.3.32f1 UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs index 19dc45edae6..b4bb9ebb385 100644 --- a/crates/unity-tests/tests/unity.rs +++ b/crates/unity-tests/tests/unity.rs @@ -4,17 +4,17 @@ use std::env; use std::ffi::OsString; use std::fs; use std::io; -use std::net::{SocketAddr, TcpStream}; use std::path::{Path, PathBuf}; -use std::process::{Command, ExitStatus, Stdio}; -use std::time::{Duration, Instant}; +use std::process::{Command, ExitStatus}; use anyhow::{anyhow, bail, Context, Result}; use clap::Parser; use quick_xml::events::{BytesStart, Event}; use quick_xml::Reader; use serde_json::Value; -use spacetimedb_language_test_support::{print_results, target_dir, workspace_root, Outcome, TestCaseResult}; +use spacetimedb_language_test_support::{ + print_results, target_dir, workspace_root, Outcome, SpacetimeDbGuard, TestCaseResult, +}; use tempfile::TempDir; const UNITY_VERSION: &str = "2022.3.32f1"; @@ -61,6 +61,7 @@ fn run() -> Result<()> { "bash", &["./generate.sh".into(), "-y".into()], Some(spacetime_bin.path_env()), + &[], ) .context("failed to generate Blackholio Unity bindings")?; @@ -72,23 +73,26 @@ fn run() -> Result<()> { "demo/Blackholio/client-unity/Assets/Scripts/autogen".into(), ], None, + &[], ) .context("generated Blackholio Unity bindings differ from the checked-in files")?; - let _server = SpacetimeServer::start(&server_dir, spacetime_bin.path_env())?; + let server = SpacetimeDbGuard::spawn_in_temp_data_dir(); run_command( &server_dir, "spacetime", &["logout".into()], Some(spacetime_bin.path_env()), + &[], ) .context("failed to log out of local SpacetimeDB")?; run_command( &server_dir, "spacetime", - &["login".into(), "--server-issued-login".into(), "local".into()], + &["login".into(), "--server-issued-login".into(), server.host_url.clone()], Some(spacetime_bin.path_env()), + &[], ) .context("failed to log in to local SpacetimeDB")?; run_command( @@ -96,18 +100,26 @@ fn run() -> Result<()> { "bash", &["./publish.sh".into()], Some(spacetime_bin.path_env()), + &[("SPACETIMEDB_SERVER_URL", server.host_url.as_str())], ) .context("failed to publish the Blackholio module")?; run_unity_tests( &unity_path, &unity_project_dir, + &server.host_url, args.filter.as_deref(), &args.passthrough, ) } -fn run_unity_tests(unity_path: &Path, project_dir: &Path, filter: Option<&str>, passthrough: &[String]) -> Result<()> { +fn run_unity_tests( + unity_path: &Path, + project_dir: &Path, + server_url: &str, + filter: Option<&str>, + passthrough: &[String], +) -> Result<()> { let out_dir = target_dir().join("unity-tests"); fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; let results_path = out_dir.join("results.xml"); @@ -135,6 +147,7 @@ fn run_unity_tests(unity_path: &Path, project_dir: &Path, filter: Option<&str>, let status = Command::new(unity_path) .args(&args) + .env("SPACETIMEDB_SERVER_URL", server_url) .status() .with_context(|| format!("failed to run {}", unity_path.display()))?; @@ -169,7 +182,7 @@ fn find_unity(explicit_path: Option<&Path>) -> Result { } let version = env::var("UNITY_VERSION").unwrap_or_else(|_| UNITY_VERSION.to_string()); - for path in [ + let mut candidates = vec![ PathBuf::from(format!("/opt/unity/editors/{version}/Editor/Unity")), PathBuf::from(format!("/opt/Unity/Hub/Editor/{version}/Editor/Unity")), PathBuf::from("/opt/unity/Editor/Unity"), @@ -177,7 +190,12 @@ fn find_unity(explicit_path: Option<&Path>) -> Result { PathBuf::from(format!( "/Applications/Unity/Hub/Editor/{version}/Unity.app/Contents/MacOS/Unity" )), - ] { + ]; + if let Some(home) = env::var_os("HOME") { + candidates.push(PathBuf::from(home).join(format!("Unity/Hub/Editor/{version}/Editor/Unity"))); + } + + for path in candidates { if path.exists() { return Ok(path); } @@ -256,43 +274,6 @@ fn link_or_copy(src: &Path, dst: &Path) -> io::Result<()> { fs::copy(src, dst).map(|_| ()) } -struct SpacetimeServer { - child: std::process::Child, -} - -impl SpacetimeServer { - fn start(cwd: &Path, path_env: &OsString) -> Result { - let child = Command::new("spacetime") - .arg("start") - .current_dir(cwd) - .env("PATH", path_env) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .context("failed to start local SpacetimeDB server")?; - wait_for_port("127.0.0.1:3000".parse().unwrap(), Duration::from_secs(30))?; - Ok(Self { child }) - } -} - -impl Drop for SpacetimeServer { - fn drop(&mut self) { - let _ = self.child.kill(); - let _ = self.child.wait(); - } -} - -fn wait_for_port(addr: SocketAddr, timeout: Duration) -> Result<()> { - let deadline = Instant::now() + timeout; - while Instant::now() < deadline { - if TcpStream::connect_timeout(&addr, Duration::from_millis(250)).is_ok() { - return Ok(()); - } - std::thread::sleep(Duration::from_millis(250)); - } - bail!("SpacetimeDB did not start listening on {addr} within {timeout:?}") -} - struct FileRestore { path: PathBuf, original: String, @@ -352,12 +333,21 @@ fn patch_unity_package_manifest(path: &Path) -> Result { }) } -fn run_command(cwd: &Path, program: &str, args: &[String], path_env: Option<&OsString>) -> Result<()> { +fn run_command( + cwd: &Path, + program: &str, + args: &[String], + path_env: Option<&OsString>, + envs: &[(&str, &str)], +) -> Result<()> { let mut command = Command::new(program); command.args(args).current_dir(cwd); if let Some(path_env) = path_env { command.env("PATH", path_env); } + for (key, value) in envs { + command.env(key, value); + } let status = command .status() .with_context(|| format!("failed to run {}", shell_line(program, args)))?; diff --git a/demo/Blackholio/client-unity/Assets/PlayModeTests/PlayModeExampleTest.cs b/demo/Blackholio/client-unity/Assets/PlayModeTests/PlayModeExampleTest.cs index ea1216acfb1..2a95c803313 100644 --- a/demo/Blackholio/client-unity/Assets/PlayModeTests/PlayModeExampleTest.cs +++ b/demo/Blackholio/client-unity/Assets/PlayModeTests/PlayModeExampleTest.cs @@ -26,7 +26,7 @@ public IEnumerator SimpleConnectionTest() }).OnConnectError((_) => { Debug.Assert(false, "Connection failed!"); - }).WithUri("http://127.0.0.1:3000") + }).WithUri(GameManager.ServerUrl) .WithDatabaseName("blackholio").Build(); while (!connected) diff --git a/demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs b/demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs index 76f32372602..174128938a8 100644 --- a/demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs +++ b/demo/Blackholio/client-unity/Assets/Scripts/GameManager.cs @@ -8,8 +8,9 @@ public class GameManager : MonoBehaviour { - const string SERVER_URL = "http://127.0.0.1:3000"; + const string DEFAULT_SERVER_URL = "http://127.0.0.1:3000"; const string MODULE_NAME = "blackholio"; + public static string ServerUrl => Environment.GetEnvironmentVariable("SPACETIMEDB_SERVER_URL") ?? DEFAULT_SERVER_URL; public static event Action OnConnected; public static event Action OnSubscriptionApplied; @@ -44,7 +45,7 @@ private void Start() .OnConnect(HandleConnect) .OnConnectError(HandleConnectError) .OnDisconnect(HandleDisconnect) - .WithUri(SERVER_URL) + .WithUri(ServerUrl) .WithDatabaseName(MODULE_NAME); // If the user has a SpacetimeDB auth token stored in the Unity PlayerPrefs, diff --git a/demo/Blackholio/server-rust/publish.sh b/demo/Blackholio/server-rust/publish.sh index f3311d04923..d8defc69330 100755 --- a/demo/Blackholio/server-rust/publish.sh +++ b/demo/Blackholio/server-rust/publish.sh @@ -2,4 +2,6 @@ set -euo pipefail -spacetime publish -s local blackholio --delete-data -y +SPACETIMEDB_SERVER_URL="${SPACETIMEDB_SERVER_URL:-local}" + +spacetime publish -s "$SPACETIMEDB_SERVER_URL" blackholio --delete-data -y From 1c5a082b6492306590bf9ab18b8bef1a0e1285ca Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Mon, 4 May 2026 16:56:14 -0700 Subject: [PATCH 4/7] [bfops/cargo-unity-test]: try unity CI fix --- .github/workflows/ci.yml | 1 + crates/unity-tests/tests/unity.rs | 131 ++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a873c5a4c00..e605f8ba075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -665,6 +665,7 @@ jobs: cargo ci unity-tests --skip-dlls env: UNITY_VERSION: 2022.3.32f1 + UNITY_USE_DOCKER: "1" UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs index b4bb9ebb385..210ef83f972 100644 --- a/crates/unity-tests/tests/unity.rs +++ b/crates/unity-tests/tests/unity.rs @@ -51,7 +51,7 @@ fn run() -> Result<()> { return Ok(()); } - let unity_path = find_unity(args.unity_path.as_deref())?; + let unity = find_unity(args.unity_path.as_deref())?; let spacetime_bin = SpacetimeBin::prepare()?; let _server_cargo_restore = patch_blackholio_server_manifest(&server_dir.join("Cargo.toml"))?; let _unity_manifest_restore = patch_unity_package_manifest(&unity_project_dir.join("Packages/manifest.json"))?; @@ -105,7 +105,7 @@ fn run() -> Result<()> { .context("failed to publish the Blackholio module")?; run_unity_tests( - &unity_path, + &unity, &unity_project_dir, &server.host_url, args.filter.as_deref(), @@ -114,7 +114,7 @@ fn run() -> Result<()> { } fn run_unity_tests( - unity_path: &Path, + unity: &UnityRunner, project_dir: &Path, server_url: &str, filter: Option<&str>, @@ -145,11 +145,9 @@ fn run_unity_tests( } args.extend_from_slice(passthrough); - let status = Command::new(unity_path) - .args(&args) - .env("SPACETIMEDB_SERVER_URL", server_url) - .status() - .with_context(|| format!("failed to run {}", unity_path.display()))?; + let (status, command_line) = unity + .run(&args, server_url) + .context("failed to run Unity playmode tests")?; if results_path.exists() { let results = parse_unity_results(&results_path)?; @@ -158,30 +156,115 @@ fn run_unity_tests( print_log_excerpt(&log_path)?; } - ensure_success(status, &shell_line(unity_path, &args)) + ensure_success(status, &command_line) +} + +enum UnityRunner { + Native(PathBuf), + Docker { image: String }, +} + +impl UnityRunner { + fn run(&self, unity_args: &[String], server_url: &str) -> Result<(ExitStatus, String)> { + match self { + UnityRunner::Native(path) => { + let mut args = unity_args.to_vec(); + add_unity_license_args(&mut args); + let status = Command::new(path) + .args(&args) + .env("SPACETIMEDB_SERVER_URL", server_url) + .status() + .with_context(|| format!("failed to run {}", path.display()))?; + Ok((status, shell_line(path, &redact_unity_args(&args)))) + } + UnityRunner::Docker { image } => { + let workspace = workspace_root(); + let mut docker_args = vec![ + "run".to_string(), + "--rm".to_string(), + "--network".to_string(), + "host".to_string(), + "-e".to_string(), + format!("SPACETIMEDB_SERVER_URL={server_url}"), + ]; + for var in ["UNITY_EMAIL", "UNITY_PASSWORD", "UNITY_SERIAL", "UNITY_LICENSE"] { + if env::var_os(var).is_some() { + docker_args.push("-e".to_string()); + docker_args.push(var.to_string()); + } + } + docker_args.extend([ + "-v".to_string(), + format!("{}:{}", workspace.display(), workspace.display()), + "-w".to_string(), + workspace.display().to_string(), + image.clone(), + "/opt/unity/Editor/Unity".to_string(), + ]); + + let mut args = unity_args.to_vec(); + add_unity_license_args(&mut args); + docker_args.extend(args); + + let status = Command::new("docker") + .args(&docker_args) + .status() + .context("failed to run docker")?; + Ok((status, shell_line("docker", &redact_unity_args(&docker_args)))) + } + } + } +} + +fn add_unity_license_args(args: &mut Vec) { + if let (Ok(email), Ok(password), Ok(serial)) = ( + env::var("UNITY_EMAIL"), + env::var("UNITY_PASSWORD"), + env::var("UNITY_SERIAL"), + ) { + args.extend([ + "-username".to_string(), + email, + "-password".to_string(), + password, + "-serial".to_string(), + serial, + ]); + } +} + +fn redact_unity_args(args: &[String]) -> Vec { + let mut redacted = args.to_vec(); + for index in 1..redacted.len() { + if matches!(redacted[index - 1].as_str(), "-username" | "-password" | "-serial") { + redacted[index] = "".to_string(); + } + } + redacted } -fn find_unity(explicit_path: Option<&Path>) -> Result { +fn find_unity(explicit_path: Option<&Path>) -> Result { + let version = env::var("UNITY_VERSION").unwrap_or_else(|_| UNITY_VERSION.to_string()); + if let Some(path) = explicit_path { if path.exists() { - return Ok(path.to_path_buf()); + return Ok(UnityRunner::Native(path.to_path_buf())); } bail!("Unity executable does not exist: {}", path.display()); } for var in ["UNITY_PATH", "UNITY_EXECUTABLE"] { if let Some(path) = env::var_os(var).map(PathBuf::from).filter(|path| path.exists()) { - return Ok(path); + return Ok(UnityRunner::Native(path)); } } for name in ["unity", "Unity", "unity-editor"] { if let Some(path) = find_on_path(name) { - return Ok(path); + return Ok(UnityRunner::Native(path)); } } - let version = env::var("UNITY_VERSION").unwrap_or_else(|_| UNITY_VERSION.to_string()); let mut candidates = vec![ PathBuf::from(format!("/opt/unity/editors/{version}/Editor/Unity")), PathBuf::from(format!("/opt/Unity/Hub/Editor/{version}/Editor/Unity")), @@ -197,15 +280,31 @@ fn find_unity(explicit_path: Option<&Path>) -> Result { for path in candidates { if path.exists() { - return Ok(path); + return Ok(UnityRunner::Native(path)); } } + if let Some(image) = unity_docker_image(&version) { + return Ok(UnityRunner::Docker { image }); + } + bail!( - "could not find Unity. Pass --unity-path, set UNITY_PATH or UNITY_EXECUTABLE, or install Unity {version} in a standard GitHub runner path" + "could not find Unity. Pass --unity-path, set UNITY_PATH or UNITY_EXECUTABLE, install Unity {version} in a standard GitHub runner path, or set UNITY_USE_DOCKER=1" ) } +fn unity_docker_image(version: &str) -> Option { + if let Ok(image) = env::var("UNITY_DOCKER_IMAGE") + && !image.is_empty() + { + return Some(image); + } + if env::var_os("UNITY_USE_DOCKER").as_deref() == Some(std::ffi::OsStr::new("1")) { + return Some(format!("unityci/editor:ubuntu-{version}-base-3")); + } + None +} + fn find_on_path(name: &str) -> Option { let path = env::var_os("PATH")?; env::split_paths(&path) From 44740ebf3d23d62dbaf2c6a92045ebc6c4a42c95 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 11:24:54 -0700 Subject: [PATCH 5/7] [bfops/cargo-unity-test]: print results --- crates/unity-tests/tests/unity.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs index 210ef83f972..d8a8689d0bf 100644 --- a/crates/unity-tests/tests/unity.rs +++ b/crates/unity-tests/tests/unity.rs @@ -124,6 +124,8 @@ fn run_unity_tests( fs::create_dir_all(&out_dir).with_context(|| format!("failed to create {}", out_dir.display()))?; let results_path = out_dir.join("results.xml"); let log_path = out_dir.join("unity.log"); + remove_file_if_exists(&results_path)?; + remove_file_if_exists(&log_path)?; let mut args = vec![ "-batchmode".to_string(), @@ -152,13 +154,27 @@ fn run_unity_tests( if results_path.exists() { let results = parse_unity_results(&results_path)?; print_results("unity playmode", &results_path, &results)?; - } else if !status.success() && log_path.exists() { - print_log_excerpt(&log_path)?; + } else { + if log_path.exists() { + print_log_excerpt(&log_path)?; + } + bail!( + "Unity did not write a test result file at {}; cannot determine which tests ran", + results_path.display() + ); } ensure_success(status, &command_line) } +fn remove_file_if_exists(path: &Path) -> Result<()> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error).with_context(|| format!("failed to remove {}", path.display())), + } +} + enum UnityRunner { Native(PathBuf), Docker { image: String }, From ddd55358070df88d8702a3201e1954bb1ae10dc6 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 12:00:14 -0700 Subject: [PATCH 6/7] [bfops/cargo-unity-test]: unity updates --- crates/unity-tests/tests/unity.rs | 32 +++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs index d8a8689d0bf..eb893ec60d6 100644 --- a/crates/unity-tests/tests/unity.rs +++ b/crates/unity-tests/tests/unity.rs @@ -127,6 +127,12 @@ fn run_unity_tests( remove_file_if_exists(&results_path)?; remove_file_if_exists(&log_path)?; + let planned_tests = discover_unity_tests(project_dir)?; + println!("unity playmode: discovered {} tests before run", planned_tests.len()); + for test in &planned_tests { + println!("planned {test}"); + } + let mut args = vec![ "-batchmode".to_string(), "-nographics".to_string(), @@ -159,8 +165,8 @@ fn run_unity_tests( print_log_excerpt(&log_path)?; } bail!( - "Unity did not write a test result file at {}; cannot determine which tests ran", - results_path.display() + "Unity exited with {status} but did not write a test result file at {}; cannot determine which tests actually ran. Command: {command_line}", + results_path.display(), ); } @@ -551,26 +557,32 @@ fn attr(event: &BytesStart<'_>, key: &[u8]) -> Result> { fn print_log_excerpt(path: &Path) -> Result<()> { let log = fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + eprintln!( - "Unity did not write a test result file. Last log lines from {}:", + "Unity did not write a test result file. Full log from {}:", path.display() ); - let lines: Vec<_> = log.lines().rev().take(80).collect(); - for line in lines.into_iter().rev() { - eprintln!("{line}"); + eprint!("{log}"); + if !log.ends_with('\n') { + eprintln!(); } + Ok(()) } fn list_unity_tests(project_dir: &Path) -> Result<()> { + for test in discover_unity_tests(project_dir)? { + println!("{test}"); + } + Ok(()) +} + +fn discover_unity_tests(project_dir: &Path) -> Result> { let tests_dir = project_dir.join("Assets/PlayModeTests"); let mut tests = Vec::new(); collect_unity_tests(&tests_dir, &mut tests)?; tests.sort(); - for test in tests { - println!("{test}"); - } - Ok(()) + Ok(tests) } fn collect_unity_tests(dir: &Path, tests: &mut Vec) -> Result<()> { From 229462736cdf7681adada3350ea72f9fe90c58c4 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Tue, 5 May 2026 13:16:17 -0700 Subject: [PATCH 7/7] [bfops/cargo-unity-test]: try unity fix --- crates/unity-tests/tests/unity.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/unity-tests/tests/unity.rs b/crates/unity-tests/tests/unity.rs index eb893ec60d6..73aec463f6e 100644 --- a/crates/unity-tests/tests/unity.rs +++ b/crates/unity-tests/tests/unity.rs @@ -136,7 +136,6 @@ fn run_unity_tests( let mut args = vec![ "-batchmode".to_string(), "-nographics".to_string(), - "-quit".to_string(), "-projectPath".to_string(), project_dir.display().to_string(), "-runTests".to_string(),