270 lines
8.8 KiB
Rust
270 lines
8.8 KiB
Rust
//! 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");
|
|
}
|