chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:41:59 +08:00
commit e365e5df4d
508 changed files with 163373 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
[Message: BepInEx] BepInEx 5.4.23.5 - Solar Expanse (2026/4/19 11:22:45)
[Info :Solar Expanse Trainer] Pipe client connected (1).
[Info :Solar Expanse Trainer] Pipe client connected (2).
[Warning: HarmonyX] AccessTools.Method: Could not find method for type Game.UI.Screen.
[Error :Solar Expanse Trainer] Pipe server loop failed: System.Threading.ThreadAbortException: Thread was being aborted.
at SolarExpanseTrainer.SolarExpanseTrainerPlugin.HandleCommand (SolarExpanseTrainer.PipeCommand command) [0x000b8] in <414166dfdcae444f95d8ea05ca6715f0>:0
at SolarExpanseTrainer.SolarExpanseTrainerPlugin.PipeServerMain () [0x0007b] in <414166dfdcae444f95d8ea05ca6715f0>:0
[Error : BepInEx] Chainloader startup failed: System.MissingMethodException: Method not found: 'Void Demo.Run(Int32)'.
at BepInEx.Bootstrap.Chainloader.Start () [0x0007b] in <414166dfdcae444f95d8ea05ca6715f0>:0
+16
View File
@@ -0,0 +1,16 @@
Initialize engine version: 2022.3.22f1 (887be4894c44)
ArgumentNullException: Value cannot be null.
Parameter name: source
at Game.AI.ContractHasObjective.OnUpdate () [0x0000d] in <70471b9615aa4ecfa3ed1abb95b73832>:0
at UnityEngine.MonoBehaviour:Update()
ArgumentNullException: Value cannot be null.
Parameter name: source
at Game.AI.ContractHasObjective.OnUpdate () [0x0000d] in <70471b9615aa4ecfa3ed1abb95b73832>:0
at UnityEngine.MonoBehaviour:Update()
The referenced script on this Behaviour (Game Object 'CycleMissionEditWindow') is missing!
A formatter has been created for the UnityEngine.Object type Sprite - this is *strongly* discouraged.
Stacktrace: at Sirenix.Serialization.BaseFormatter`1[T]..cctor () [0x00000] in <c90407e701154d5ea8a975ee4e10d3a4>:0
at System.Object.__icall_wrapper_mono_generic_class_init (System.IntPtr) [0x00000] in <27124aa0e30a41659b903b822b959bc7>:0
Amplitude Event Sent Successfully
Amplitude Event Sent Successfully
+269
View File
@@ -0,0 +1,269 @@
//! Integration tests for the `unitydiag` command.
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use assert_cmd::Command;
use filetime::{FileTime, set_file_mtime};
use predicates::prelude::*;
fn cargo_command() -> Command {
Command::cargo_bin("unitydiag").expect("binary")
}
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("workspace root")
}
fn fixture(path: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(path)
}
fn create_temp_dir(label: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock")
.as_nanos();
let path =
std::env::temp_dir().join(format!("unitydiag-{label}-{}-{unique}", std::process::id()));
fs::create_dir_all(&path).expect("temp dir");
path
}
fn write_fixture(target: &Path, source_name: &str) {
let content = fs::read(fixture(source_name)).expect("read fixture");
fs::write(target, content).expect("write fixture");
}
fn set_mtime(target: &Path, unix_seconds: i64) {
set_file_mtime(target, FileTime::from_unix_time(unix_seconds, 0)).expect("set file mtime");
}
#[test]
fn emits_grouped_text_output_by_incident() {
let mut command = cargo_command();
command
.arg(fixture("player.log"))
.arg(fixture("LogOutput.log"))
.assert()
.success()
.stdout(predicate::str::contains("summary logs=2"))
.stdout(predicate::str::contains("domain=game"))
.stdout(predicate::str::contains("domain=mod"))
.stdout(predicate::str::contains("domain=bepinex"))
.stdout(predicate::str::contains("count=2"))
.stdout(predicate::str::contains(
"message=ArgumentNullException: Value cannot be null. | Parameter name: source",
))
.stdout(predicate::str::contains(
"frame=Game.AI.ContractHasObjective.OnUpdate",
));
}
#[test]
fn emits_stable_json_object_with_logs_incidents_and_summary() {
let mut command = cargo_command();
command
.arg("--json")
.arg("--include-warnings")
.arg("--include-info")
.arg("--stack")
.arg("top")
.arg(fixture("player.log"))
.arg(fixture("LogOutput.log"))
.assert()
.success()
.stdout(predicate::str::contains("\"logs\":["))
.stdout(predicate::str::contains("\"incidents\":["))
.stdout(predicate::str::contains("\"summary\":{"))
.stdout(predicate::str::contains("\"group_by\":\"incident\""))
.stdout(predicate::str::contains("\"domain\":\"telemetry\""))
.stdout(predicate::str::contains("\"domain\":\"harmony\""))
.stdout(predicate::str::contains("\"kind\":\"player\""))
.stdout(predicate::str::contains("\"kind\":\"bepinex\""));
}
#[test]
fn supports_powershell_pipeline_input() {
let binary = assert_cmd::cargo::cargo_bin("unitydiag");
let input = fixture("player.log");
let script = format!(
"Get-Content '{}' | & '{}' --include-warnings --json",
input.display(),
binary.display()
);
let mut command = Command::new("pwsh");
command
.current_dir(workspace_root())
.arg("-NoProfile")
.arg("-Command")
.arg(script)
.assert()
.success()
.stdout(predicate::str::contains("\"incidents\":["))
.stdout(predicate::str::contains("\"domain\":\"game\""));
}
#[test]
fn discovers_logs_from_game_root_and_latest() {
let root = create_temp_dir("discover");
let game_root = root.join("Solar Expanse");
let bepinex_dir = game_root.join("BepInEx");
let locallow_dir = root
.join("AppData")
.join("LocalLow")
.join("SpaceOps")
.join("Solar Expanse");
fs::create_dir_all(&bepinex_dir).expect("bepinex dir");
fs::create_dir_all(&locallow_dir).expect("locallow dir");
write_fixture(&locallow_dir.join("Player.log"), "player.log");
write_fixture(&bepinex_dir.join("LogOutput.log"), "LogOutput.log");
set_mtime(&locallow_dir.join("Player.log"), 1_700_000_000);
set_mtime(&bepinex_dir.join("LogOutput.log"), 1_700_000_002);
let mut command = cargo_command();
command
.env("USERPROFILE", &root)
.arg("--json")
.arg("--include-info")
.arg("--game-root")
.arg(&game_root)
.arg("--latest")
.assert()
.success()
.stdout(predicate::str::contains("\"logs\":["))
.stdout(predicate::str::contains("LogOutput.log"))
.stdout(predicate::str::contains("\"log_count\":1"));
fs::remove_dir_all(&root).expect("cleanup");
}
#[test]
fn game_root_without_latest_includes_bepinex_and_matching_player_log() {
let root = create_temp_dir("discover-both");
let game_root = root.join("Solar Expanse");
let bepinex_dir = game_root.join("BepInEx");
let locallow_dir = root
.join("AppData")
.join("LocalLow")
.join("SpaceOps")
.join("Solar Expanse");
fs::create_dir_all(&bepinex_dir).expect("bepinex dir");
fs::create_dir_all(&locallow_dir).expect("locallow dir");
write_fixture(&locallow_dir.join("Player.log"), "player.log");
write_fixture(&bepinex_dir.join("LogOutput.log"), "LogOutput.log");
let mut command = cargo_command();
command
.env("USERPROFILE", &root)
.arg("--json")
.arg("--include-info")
.arg("--game-root")
.arg(&game_root)
.assert()
.success()
.stdout(predicate::str::contains("\"log_count\":2"))
.stdout(predicate::str::contains("LogOutput.log"))
.stdout(predicate::str::contains("Player.log"));
fs::remove_dir_all(&root).expect("cleanup");
}
#[test]
fn discovers_logs_from_relative_dot_game_root() {
let root = create_temp_dir("discover-dot");
let game_root = root.join("Solar Expanse");
let locallow_root = root.join("AppData").join("LocalLow");
let target_locallow = locallow_root.join("SpaceOps").join("Solar Expanse");
let unrelated_locallow = locallow_root.join("OtherStudio").join("Another Game");
fs::create_dir_all(&game_root).expect("game root");
fs::create_dir_all(&target_locallow).expect("target locallow");
fs::create_dir_all(&unrelated_locallow).expect("unrelated locallow");
write_fixture(&target_locallow.join("Player.log"), "player.log");
write_fixture(&unrelated_locallow.join("Player.log"), "player.log");
set_mtime(&target_locallow.join("Player.log"), 1_700_000_000);
set_mtime(&unrelated_locallow.join("Player.log"), 1_700_000_002);
let mut command = cargo_command();
command
.current_dir(&game_root)
.env("USERPROFILE", &root)
.arg("--json")
.arg("--include-info")
.arg("--game-root")
.arg(".")
.arg("--latest")
.assert()
.success()
.stdout(predicate::str::contains("\"logs\":["))
.stdout(predicate::str::contains("SpaceOps"))
.stdout(predicate::str::contains("Solar Expanse"))
.stdout(predicate::str::contains("\"log_count\":1"))
.stdout(predicate::str::contains("\"kind\":\"player\""));
fs::remove_dir_all(&root).expect("cleanup");
}
#[test]
fn help_includes_examples_and_grouping_flags() {
let mut command = cargo_command();
command
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("--group-by"))
.stdout(predicate::str::contains("--latest"))
.stdout(predicate::str::contains("ConvertFrom-Json"))
.stdout(predicate::str::contains(
"unitydiag --game-root 'C:\\game' --latest",
));
}
#[test]
fn utf8_bom_log_file_is_parsed_without_losing_incidents() {
let root = create_temp_dir("bom");
let log_path = root.join("Player.log");
let mut bytes = vec![0xEF, 0xBB, 0xBF];
bytes.extend(fs::read(fixture("player.log")).expect("fixture"));
fs::write(&log_path, bytes).expect("bom fixture");
let mut command = cargo_command();
command
.arg("--json")
.arg(&log_path)
.assert()
.success()
.stdout(predicate::str::contains("\"log_count\":1"))
.stdout(predicate::str::contains("\"domain\":\"game\""));
fs::remove_dir_all(&root).expect("cleanup");
}
#[test]
fn invalid_utf8_log_file_reports_read_error_instead_of_summarizing() {
let root = create_temp_dir("invalid-utf8");
let log_path = root.join("Player.log");
fs::write(&log_path, b"Initialize engine version\n\xff\xfe\xfd\n").expect("invalid fixture");
let mut command = cargo_command();
command
.arg(&log_path)
.assert()
.failure()
.stderr(predicate::str::contains("failed to read"))
.stderr(predicate::str::contains("valid UTF-8"));
fs::remove_dir_all(&root).expect("cleanup");
}