Skip to content

Commit 0dec730

Browse files
committed
feat(compilation): improve compile error handling and protocol
- Automatically send compile errors after CompilationFinished message - Simplify compile error collection in UnityManager - Update documentation for new compilation protocol behavior - Refactor test assertions for compile error handling - Remove redundant compile error collection delay logic
1 parent 1fc2241 commit 0dec730

File tree

5 files changed

+68
-117
lines changed

5 files changed

+68
-117
lines changed

UnityProject/Packages/packages-lock.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"com.unity.nuget.newtonsoft-json": "3.2.1",
1010
"com.unity.nuget.mono-cecil": "1.11.4"
1111
},
12-
"hash": "9257ab7d15c0d166e59f2af8054a570a0cb5b58b"
12+
"hash": "41cea8e4958084e43e1bbb957a177dcc51698ac7"
1313
},
1414
"com.unity.burst": {
1515
"version": "1.8.21",

docs/UnityPackageMessagingProtocol.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ All available message types:
7272
| `RetrieveTestList` | 23 | Request to retrieve list of available tests | Test mode string ("EditMode" or "PlayMode") |
7373
| `ExecuteTests` | 24 | Request to execute specific tests | TestMode, TestMode:AssemblyName.dll, or TestMode:FullTestName / Response is empty string|
7474
| `ShowUsage` | 25 | Show usage information | JSON serialized FileUsage object |
75-
| `CompilationFinished` | 100 | Notification that compilation has finished | Empty string |
75+
| `CompilationFinished` | 100 | Notification that compilation has finished | Empty string (automatically followed by GetCompileErrors message) |
7676
| `PackageName` | 101 | Request/response for package name | Empty string (request) / Package name string (response) |
7777
| `Online` | 102 | Notifies clients that this package is online and ready to receive messages | Empty string |
7878
| `Offline` | 103 | Notifies clients that this package is offline and can not receive messages | Empty string |
7979
| `IsPlaying` | 104 | Notification of current play mode state | "true" (in play mode) / "false" (in edit mode) |
8080
| `CompilationStarted` | 105 | Notification that compilation has started | Empty string |
81-
| `GetCompileErrors` | 106 | Request/response for compile error information | Empty string (request) / JSON serialized LogContainer (response) |
81+
| `GetCompileErrors` | 106 | Auto-sent after CompilationFinished, or manual request/response for compile error information | Empty string (request) / JSON serialized LogContainer (response) |
8282

8383
Note:
8484
- Message value greater than or equal to 100 means it does not exist in the official package but was added in this package.
@@ -130,11 +130,26 @@ Detailed value formats for some of the types:
130130
- **Compilation Lifecycle**: This message is sent at the beginning of the compilation process, before any assembly compilation starts
131131
- **Relationship to CompilationFinished**: This message pairs with `CompilationFinished` (Value: 100) to provide complete compilation lifecycle notifications
132132

133+
#### CompilationFinished (Value: 100)
134+
- **Format**: Empty string
135+
- **Description**: Notification sent when Unity's compilation pipeline finishes compiling assemblies. This message is broadcast to all connected clients when the compilation process completes.
136+
- **Automatic Behavior**:
137+
- **GetCompileErrors Auto-Send**: Immediately after broadcasting this message, a `GetCompileErrors` message (Value: 106) is automatically sent to all connected clients with the collected compile errors from the compilation session
138+
- **Error Collection**: Compile errors are collected during the compilation process and automatically provided without requiring a separate request
139+
- **Manual Requests**: `GetCompileErrors` can still be requested manually
140+
- **Important Notes**:
141+
- **Compilation Lifecycle**: This message is sent at the end of the compilation process, after all assembly compilation finishes
142+
- **Relationship to CompilationStarted**: This message pairs with `CompilationStarted` (Value: 105) to provide complete compilation lifecycle notifications
143+
- **Client Integration**: Clients can expect to receive compile error information automatically after each compilation without needing to explicitly request it
144+
133145
#### GetCompileErrors (Value: 106)
134146
- **Format**:
135147
- Request: Empty string
136148
- Response: JSON serialized LogContainer object
137-
- **Description**: Requests the collected compile errors that occurred during Unity's compilation process. Unity collects compile errors within a 1-second window after compilation finishes.
149+
- **Description**: Provides the collected compile errors that occurred during Unity's compilation process. This message is automatically sent to all connected clients immediately after each `CompilationFinished` message, but can also be requested manually.
150+
- **Automatic Behavior**:
151+
- **Auto-Send**: Automatically broadcast to all clients after every `CompilationFinished` message
152+
- **Manual Request**: Can also be requested manually by sending an empty string request
138153

139154
- **C# Structure**:
140155

@@ -173,7 +188,7 @@ public class Log
173188
- **Error Filtering**: Only log messages containing "error CS" are collected
174189
- **Automatic Clearing**: Previous compile errors are cleared when compilation starts
175190
- **Response Format**: Returns JSON with LogContainer containing array of Log objects
176-
- **Usage**: Clients can request this to get structured compile error information for IDE integration, error highlighting, and debugging assistance.
191+
- **Usage**: Clients automatically receive structured compile error information after each compilation for IDE integration, error highlighting, and debugging assistance. Manual requests are also supported for on-demand error retrieval.
177192

178193
#### RetrieveTestList (Value: 23)
179194
- **Format**: Test mode string ("EditMode" or "PlayMode")
@@ -185,11 +200,20 @@ public class Log
185200
- `TestMode` - Execute all tests in the specified mode
186201
- `TestMode:AssemblyName.dll` - Execute all tests in the specified assembly
187202
- `TestMode:FullTestName` - Execute a specific test by its full name
203+
- `TestMode:PartialTestName?` - Execute tests using fuzzy matching (partial name matching), by ending with `?`
188204
- **Examples**:
189205
- `"EditMode"` - Run all edit mode tests
190206
- `"PlayMode:MyTests.dll"` - Run all tests in MyTests assembly
191207
- `"EditMode:MyNamespace.MyTestClass"` - Run all tests in MyTestClass
192-
- **Description**: Executes tests based on the specified filter. The filter can target all tests in a mode, all tests in an assembly, or a specific test by name.
208+
- `"EditMode:TestMethod?"` - Run all tests whose full name ends with "TestMethod"
209+
- `"PlayMode:Utils?"` - Run all tests whose full name ends with "Utils"
210+
- **Description**: Executes tests based on the specified filter. The filter can target all tests in a mode, all tests in an assembly, or a specific test by name. When the filter doesn't match any exact test names, fuzzy matching is performed to find tests whose full names end with the specified search term.
211+
- **Fuzzy Matching Behavior**:
212+
- If filter ends with `?`, the system performs fuzzy matching
213+
- Fuzzy matching finds all tests (including non-leaf nodes) whose `FullName` ends with the search term
214+
- Case-insensitive matching is used
215+
- Both leaf tests and test containers (classes, namespaces) can be matched
216+
- Multiple matches are supported - all matching tests will be executed
193217

194218
Response:
195219
- A response that is empty is sent to the original client to confirm that the message is received and already processed.

src/unity_manager.rs

Lines changed: 16 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -237,45 +237,22 @@ impl UnityManager {
237237
{
238238
*compilation_guard = Some(compilation_finish_time);
239239
}
240-
241-
// Wait 1 second for logs to arrive after compilation finish event
242-
let log_manager_clone = log_manager.clone();
243-
let last_compile_errors_clone = last_compile_errors.clone();
244-
tokio::spawn(async move {
245-
// give it a little bit more time, 3 seconds to make sure we don't miss any logs
246-
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
247-
248-
// Collect compile errors from logs
249-
if let Ok(log_manager_guard) = log_manager_clone.lock() {
250-
if let Ok(mut compile_errors_guard) = last_compile_errors_clone.lock()
251-
{
252-
// start a bit earlier than compilation finish time in case messages are not received in order
253-
let logs = log_manager_guard.get_recent_logs(compilation_finish_time - Duration::from_secs(1), Some(LogLevel::Error), Some("error CS"));
254-
for log_entry in logs {
255-
let main_message =
256-
extract_main_message(&log_entry.message);
257-
compile_errors_guard.push(main_message);
258-
}
259-
debug_log!("Compilation finished - collected {} compile errors: {:?}", compile_errors_guard.len(), compile_errors_guard);
260-
}
261-
}
262-
});
263240
}
264241
UnityEvent::CompileErrors(log_container) => {
265-
// Handle initial compile errors from Unity
266-
// If we never received compile finish events
267-
// then this is the initial compile errors before we connect to Unity
242+
// Handle compile errors from Unity
243+
// Update compile errors whenever we receive them
244+
if let Ok(mut compile_errors_guard) = last_compile_errors.lock() {
245+
compile_errors_guard.clear();
246+
for log in &log_container.logs {
247+
let main_message = extract_main_message(&log.message);
248+
compile_errors_guard.push(main_message);
249+
}
250+
debug_log!("Compile errors received from Unity: {} errors", compile_errors_guard.len());
251+
}
252+
253+
// If this is the first time we receive compile errors, set the compilation finished time
268254
if let Ok(mut last_compilation_finished_guard) = last_compilation_finished.lock() {
269255
if last_compilation_finished_guard.is_none() {
270-
if let Ok(mut compile_errors_guard) = last_compile_errors.lock() {
271-
compile_errors_guard.clear();
272-
for log in &log_container.logs {
273-
let main_message = extract_main_message(&log.message);
274-
compile_errors_guard.push(main_message);
275-
}
276-
debug_log!("Initial compile errors received from Unity: {} errors", compile_errors_guard.len());
277-
}
278-
279256
// compile didn't happen just now, but we can use now as a fake compile time, it's ok
280257
*last_compilation_finished_guard = Some(SystemTime::now());
281258
}
@@ -697,56 +674,13 @@ impl UnityManager {
697674
}
698675
}
699676

700-
// Wait additional time for compile errors to arrive after compilation finishes
677+
// Wait a fixed 100ms for compile errors to arrive after compilation finishes
701678
if refresh_task.is_successful() && refresh_task.has_compilation() {
702-
// Wait for either Unity to go offline (domain reload = compilation success)
703-
// or compile errors to arrive (compilation failed)
704-
let mut received_compile_errors = false;
705-
let wait_timeout = Duration::from_secs(3); // 3 second timeout for waiting
706-
let wait_start = std::time::Instant::now();
707-
708-
while wait_start.elapsed() < wait_timeout {
709-
match timeout(Duration::from_millis(100), event_receiver.recv()).await {
710-
Ok(Ok(event)) => {
711-
match event {
712-
UnityEvent::OnlineStateChanged(is_online) => {
713-
if !is_online {
714-
// Unity went offline -> domain reload happened -> compilation success!
715-
debug_log!("Unity went offline, domain reload detected - compilation successful");
716-
break;
717-
}
718-
}
719-
UnityEvent::LogMessage { level: LogLevel::Error, message } => {
720-
// Check if this is a compile error log -> compilation failed
721-
if message.contains("error CS") {
722-
debug_log!("Compile error detected in log message - compilation failed");
723-
received_compile_errors = true;
724-
break;
725-
}
726-
}
727-
_ => {
728-
// Ignore other events
729-
}
730-
}
731-
}
732-
Ok(Err(_)) => {
733-
// Event channel closed
734-
break;
735-
}
736-
Err(_) => {
737-
// Timeout, continue waiting
738-
}
739-
}
740-
}
741-
742-
// Give a small additional delay if we received compile errors to ensure all are collected
743-
if received_compile_errors {
744-
sleep(Duration::from_millis(100)).await;
745-
}
679+
sleep(Duration::from_millis(200)).await;
746680
}
747681

748-
let previous_compile_errors = self.get_last_compile_errors();
749-
let result = refresh_task.build_result(&self.log_manager, &previous_compile_errors);
682+
let compile_errors = self.get_last_compile_errors();
683+
let result = refresh_task.build_result(&self.log_manager, &compile_errors);
750684
Ok(result)
751685
} else {
752686
Err(MESSAGING_CLIENT_NOT_INIT_ERROR.into())

src/unity_manager_tests.rs

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async fn execute_unity_tests_with_validation(
137137
.map_err(|_| "Unity Editor not running or messaging failed")?;
138138

139139
// Wait for Unity to become online
140-
manager.wait_online(3).await
140+
manager.wait_online(10).await
141141
.map_err(|e| format!("Timeout waiting for Unity to become online: {}", e))?;
142142
println!("✓ Unity connectivity confirmed");
143143

@@ -294,24 +294,17 @@ async fn test_unity_manager_refresh_with_compilation_errors() {
294294
assert!(result.success, "Refresh should have completed successfully");
295295
assert!(result.refresh_error_message.is_none(), "Refresh should not have error message: {:?}", result.refresh_error_message);
296296

297-
// Verify we received compilation error logs
298-
assert!(!result.problems.is_empty(), "Should have received compilation error logs");
297+
// Check that we have compilation errors
298+
println!("Problems: {:?}", result.problems);
299+
assert!(!result.problems.is_empty(), "Should have compilation errors");
299300

300-
// Check that the error logs contain compilation-related errors
301-
let compilation_errors: Vec<_> = result.problems.iter().filter(|log| {
302-
let msg_lower = log.to_lowercase();
303-
msg_lower.contains("error") ||
304-
msg_lower.contains("compilation") ||
305-
msg_lower.contains("testcompilationerrors") ||
306-
msg_lower.contains("nonexistentnamespace") ||
307-
msg_lower.contains("undefinedvariable") ||
308-
msg_lower.contains("cs(") // C# error format usually contains "cs(line,col)"
309-
}).collect();
301+
// Should contain compilation-related errors
302+
let has_compilation_error = result.problems.iter().any(|problem| {
303+
problem.contains("error CS") || problem.contains("compilation")
304+
});
305+
assert!(has_compilation_error, "Should contain compilation-related errors");
310306

311-
assert!(!compilation_errors.is_empty(),
312-
"Should have received compilation-related error logs. Received logs: {:?}", result.problems);
313-
314-
println!("✓ Received {} compilation-related error logs", compilation_errors.len());
307+
println!("✓ Received {} compilation-related error logs", result.problems.len());
315308

316309
// Print some of the error logs for debugging
317310
for (i, log) in result.problems.iter().take(3).enumerate() {
@@ -336,10 +329,11 @@ async fn test_unity_manager_refresh_with_compilation_errors() {
336329
// Verify middle refresh completed successfully
337330
assert!(result.success, "Middle refresh should have completed successfully");
338331

339-
// Verify we still received compilation error logs from previous compile
340-
assert!(!result.problems.is_empty(), "Middle refresh should still receive compilation error logs from previous compile");
332+
// With the new protocol, compile errors come from dedicated messages.
333+
// The problems field contains previous compile errors, which should still be available.
334+
assert!(!result.problems.is_empty(), "Middle refresh should still receive compilation error logs from previous compile errors");
341335

342-
println!("✓ Middle refresh received {} compilation-related error logs from previous compile", result.problems.len());
336+
println!("✓ Middle refresh received {} compilation-related error logs from previous compile errors", result.problems.len());
343337
},
344338
Err(e) => {
345339
println!("⚠ Middle refresh method failed: {}", e);

src/unity_refresh_task.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,13 @@ impl UnityRefreshAssetDatabaseTask {
138138
pub fn build_result(
139139
&self,
140140
log_manager: &std::sync::Arc<std::sync::Mutex<UnityLogManager>>,
141-
previous_compile_errors: &[String],
141+
compile_errors: &[String],
142142
) -> RefreshResult {
143-
let mut logs = self.collect_refresh_logs(log_manager);
144-
145-
// If no compilation occurred during this refresh, include previous compile errors
146-
if !self.is_compile && !previous_compile_errors.is_empty() {
147-
logs.extend_from_slice(previous_compile_errors);
148-
}
149-
143+
let mut logs = Vec::new();
144+
logs.extend_from_slice(compile_errors);
145+
let other_logs = self.collect_refresh_logs(log_manager);
146+
logs.extend(other_logs);
147+
150148
let duration = self.operation_start.elapsed().as_secs_f64();
151149

152150
RefreshResult {
@@ -165,12 +163,13 @@ impl UnityRefreshAssetDatabaseTask {
165163
) -> Vec<String> {
166164
let mut logs: Vec<String> = Vec::new();
167165

168-
// Get errors and warnings during this refresh (excluding CS warnings because there can be too many)
166+
// Get errors and warnings during this refresh (excluding CS warnings and compile errors)
169167
if let Ok(log_manager_guard) = log_manager.lock() {
170168
let recent_logs = log_manager_guard.get_recent_logs(self.refresh_start_time, None, None);
171169

172170
for log in recent_logs {
173171
if !log.message.contains("warning CS")
172+
&& !log.message.contains("error CS")
174173
&& (log.level == LogLevel::Error || log.level == LogLevel::Warning)
175174
{
176175
logs.push(extract_main_message(log.message.as_str()));

0 commit comments

Comments
 (0)