use editor_adapter_api::EditorAdapter; use serde_json::json; use std::path::PathBuf; use std::time::{Duration, Instant}; use unity_editor_bridge::{ configure_helper_candidates, disconnect_unity_editor, execute_unity_editor_code_for_project, UnityEditorAdapter, }; const MAX_MESSAGE_BYTES: usize = unity_editor_bridge::MAX_MESSAGE_BYTES; #[path = "../src/transport.rs"] mod transport; fn fixture() -> (tempfile::TempDir, PathBuf) { let directory = tempfile::tempdir().unwrap(); let executable = directory .path() .join("dotnet/publish/win-x64/Agc.Unity.Attach.exe"); std::fs::create_dir_all(executable.parent().unwrap()).unwrap(); std::fs::copy( env!("CARGO_BIN_EXE_unity-bridge-protocol-fixture"), &executable, ) .unwrap(); (directory, executable) } fn project() -> tempfile::TempDir { let directory = tempfile::tempdir().unwrap(); for path in ["Assets", "Packages", "ProjectSettings"] { std::fs::create_dir(directory.path().join(path)).unwrap(); } std::fs::write( directory.path().join("ProjectSettings/ProjectVersion.txt"), "m_EditorVersion: 6000.0.0f1\n", ) .unwrap(); directory } #[test] #[cfg(all(windows, target_arch = "x86_64"))] fn shared_service_executes_serially_and_retains_uncertain_state_across_instances() { let (_fixture, executable) = fixture(); let project = project(); let project_path = project.path().to_string_lossy().into_owned(); configure_helper_candidates(vec![executable.clone()]).unwrap(); std::env::set_var("AGC_TEST_PROVIDER_SECRET", "must-not-reach-helper"); let first = execute_unity_editor_code_for_project(&project_path, "success", 1000).unwrap(); std::env::remove_var("AGC_TEST_PROVIDER_SECRET"); assert_eq!(first["status"], "completed"); assert_eq!(first["result"]["secretInherited"], false); for code in ["known_failure", "not_dispatched"] { assert_eq!( execute_unity_editor_code_for_project(&project_path, code, 1000).unwrap()["status"], "failed" ); } let adapter = UnityEditorAdapter::new(vec![executable.clone()]); assert_eq!( adapter .rpc( "editor.execute", json!({"projectPath":project_path,"code":"success","timeoutMs":1000}) ) .unwrap()["status"], "completed" ); let path = project_path.clone(); let executing = std::thread::spawn(move || execute_unity_editor_code_for_project(&path, "delay", 1000)); std::thread::sleep(Duration::from_millis(50)); let start = Instant::now(); assert!( execute_unity_editor_code_for_project(&project_path, "success", 1000).unwrap()["error"] ["message"] .as_str() .unwrap() .contains("正在执行") ); assert!(start.elapsed() < Duration::from_millis(150)); disconnect_unity_editor(); assert_eq!(executing.join().unwrap().unwrap()["status"], "completed"); let next = execute_unity_editor_code_for_project(&project_path, "success", 1000).unwrap(); assert_ne!(first["result"]["fixturePid"], next["result"]["fixturePid"]); let invalid = execute_unity_editor_code_for_project(&project_path, "wrong_id", 1000).unwrap(); assert_eq!(invalid["status"], "needs-reconciliation"); disconnect_unity_editor(); configure_helper_candidates(vec![executable.clone()]).unwrap(); let recreated = UnityEditorAdapter::new(vec![executable]); assert_eq!( recreated .rpc( "execute", json!({"projectPath":project_path,"code":"success"}) ) .unwrap()["status"], "needs-reconciliation" ); } #[test] fn helper_timeout_eof_oversized_frame_and_blocked_write_are_bounded() { let (_fixture, executable) = fixture(); for code in ["eof", "overflow", "no_response"] { let mut process = transport::HelperProcess::spawn(&executable).unwrap(); let result = process.exchange( &json!({"id":1,"method":"execute","params":{"code":code}}), Instant::now() + Duration::from_millis(300), ); assert!(result.is_err()); let start = Instant::now(); drop(process); assert!(start.elapsed() < Duration::from_secs(3)); } let mut process = transport::HelperProcess::spawn(&executable).unwrap(); process .exchange( &json!({"id":1,"method":"execute","params":{"code":"stop_reading"}}), Instant::now() + Duration::from_secs(2), ) .unwrap(); let start = Instant::now(); assert!(process .exchange( &json!({"payload":"x".repeat(1024*1024)}), Instant::now() + Duration::from_millis(100) ) .is_err()); drop(process); assert!(start.elapsed() < Duration::from_secs(3)); }