-
Notifications
You must be signed in to change notification settings - Fork 58
Add unit tests for GeneralUpdate.Bowl crash monitoring component #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
JusterZhu
merged 5 commits into
master
from
copilot/add-unit-tests-for-generalupdate-bowl
Jan 28, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
bae0739
Initial plan
Copilot 5d74a52
Add comprehensive unit tests for GeneralUpdate.Bowl component
Copilot 5805966
Add README documentation for BowlTest project
Copilot 9372319
Move BowlTest to tests directory as requested
Copilot f9cd0d4
Merge branch 'master' into copilot/add-unit-tests-for-generalupdate-bowl
JusterZhu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <IsPackable>false</IsPackable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="coverlet.collector" Version="6.0.4"/> | ||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/> | ||
| <PackageReference Include="xunit" Version="2.9.3"/> | ||
| <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/> | ||
| <PackageReference Include="Moq" Version="4.20.72"/> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Using Include="Xunit"/> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\c#\GeneralUpdate.Bowl\GeneralUpdate.Bowl.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,241 @@ | ||||||||||||||||||||||
| using System; | ||||||||||||||||||||||
| using System.Runtime.InteropServices; | ||||||||||||||||||||||
| using GeneralUpdate.Bowl; | ||||||||||||||||||||||
| using GeneralUpdate.Bowl.Strategys; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| namespace BowlTest | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Contains test cases for the Bowl class. | ||||||||||||||||||||||
| /// Tests the main entry point and parameter creation logic. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| public class BowlTests | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that Launch with valid MonitorParameter doesn't throw on Windows. | ||||||||||||||||||||||
| /// This test can only run on Windows as Linux is not fully implemented. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void Launch_WithValidParameter_DoesNotThrow_OnWindows() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // This test validates the code path but requires Windows platform | ||||||||||||||||||||||
| // and valid file paths to fully execute. We verify it doesn't throw | ||||||||||||||||||||||
| // during initialization phase. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Skip on non-Windows platforms | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var tempPath = System.IO.Path.GetTempPath(); | ||||||||||||||||||||||
| var testPath = System.IO.Path.Combine(tempPath, $"BowlTest_{Guid.NewGuid()}"); | ||||||||||||||||||||||
| System.IO.Directory.CreateDirectory(testPath); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| var parameter = new MonitorParameter | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| TargetPath = testPath, | ||||||||||||||||||||||
| FailDirectory = System.IO.Path.Combine(testPath, "fail"), | ||||||||||||||||||||||
| BackupDirectory = System.IO.Path.Combine(testPath, "backup"), | ||||||||||||||||||||||
| ProcessNameOrId = "notepad.exe", | ||||||||||||||||||||||
| DumpFileName = "test.dmp", | ||||||||||||||||||||||
| FailFileName = "test.json", | ||||||||||||||||||||||
| WorkModel = "Normal", | ||||||||||||||||||||||
| ExtendedField = "1.0.0" | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| // Note: This will fail when it tries to launch procdump as it won't exist | ||||||||||||||||||||||
| // But we're testing that the initialization doesn't throw | ||||||||||||||||||||||
| var exception = Record.Exception(() => Bowl.Launch(parameter)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // We expect some kind of exception since procdump won't exist | ||||||||||||||||||||||
| // but not ArgumentNullException or PlatformNotSupportedException | ||||||||||||||||||||||
| Assert.True(exception == null || | ||||||||||||||||||||||
| (exception is not ArgumentNullException && | ||||||||||||||||||||||
| exception is not PlatformNotSupportedException)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Cleanup | ||||||||||||||||||||||
| if (System.IO.Directory.Exists(testPath)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| System.IO.Directory.Delete(testPath, true); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that Launch throws PlatformNotSupportedException on unsupported platforms. | ||||||||||||||||||||||
| /// This test verifies the platform detection logic. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void Launch_OnUnsupportedPlatform_ThrowsPlatformNotSupportedException() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // This test verifies platform detection | ||||||||||||||||||||||
| // On Windows, it should work; on other platforms, it should throw | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var parameter = new MonitorParameter | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| TargetPath = "/tmp/test", | ||||||||||||||||||||||
| FailDirectory = "/tmp/test/fail", | ||||||||||||||||||||||
| BackupDirectory = "/tmp/test/backup", | ||||||||||||||||||||||
| ProcessNameOrId = "test", | ||||||||||||||||||||||
| DumpFileName = "test.dmp", | ||||||||||||||||||||||
| FailFileName = "test.json" | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| Assert.Throws<PlatformNotSupportedException>(() => Bowl.Launch(parameter)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that CreateParameter throws when ProcessInfo environment variable is null. | ||||||||||||||||||||||
| /// This test uses reflection to test the private CreateParameter method. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void CreateParameter_WithNullEnvironmentVariable_ThrowsArgumentNullException() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var originalValue = Environment.GetEnvironmentVariable("ProcessInfo"); | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", null); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| var exception = Assert.Throws<ArgumentNullException>(() => Bowl.Launch()); | ||||||||||||||||||||||
| Assert.Contains("ProcessInfo", exception.Message); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Restore original value | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", originalValue); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that CreateParameter throws when ProcessInfo environment variable is empty. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void CreateParameter_WithEmptyEnvironmentVariable_ThrowsArgumentNullException() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var originalValue = Environment.GetEnvironmentVariable("ProcessInfo"); | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", ""); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| var exception = Assert.Throws<ArgumentNullException>(() => Bowl.Launch()); | ||||||||||||||||||||||
| Assert.Contains("ProcessInfo", exception.Message); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Restore original value | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", originalValue); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that CreateParameter throws when ProcessInfo JSON is invalid. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void CreateParameter_WithInvalidJson_ThrowsException() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var originalValue = Environment.GetEnvironmentVariable("ProcessInfo"); | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", "{ invalid json }"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| Assert.ThrowsAny<Exception>(() => Bowl.Launch()); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Restore original value | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", originalValue); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that CreateParameter throws when ProcessInfo deserializes to null. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void CreateParameter_WithNullDeserialization_ThrowsArgumentNullException() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var originalValue = Environment.GetEnvironmentVariable("ProcessInfo"); | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", "null"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| var exception = Assert.Throws<ArgumentNullException>(() => Bowl.Launch()); | ||||||||||||||||||||||
| Assert.Contains("ProcessInfo", exception.Message); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Restore original value | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", originalValue); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Tests that Launch with valid ProcessInfo environment variable creates correct parameter. | ||||||||||||||||||||||
| /// This test verifies the environment variable parsing logic. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| [Fact] | ||||||||||||||||||||||
| public void Launch_WithValidProcessInfoEnvironment_ParsesCorrectly() | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Skip on non-Windows as platform check will fail first | ||||||||||||||||||||||
| if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Arrange | ||||||||||||||||||||||
| var originalValue = Environment.GetEnvironmentVariable("ProcessInfo"); | ||||||||||||||||||||||
| var tempPath = System.IO.Path.GetTempPath(); | ||||||||||||||||||||||
| var testPath = System.IO.Path.Combine(tempPath, $"BowlTest_{Guid.NewGuid()}"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| var processInfoJson = @"{ | ||||||||||||||||||||||
| ""AppName"": ""TestApp.exe"", | ||||||||||||||||||||||
| ""InstallPath"": """ + testPath.Replace("\\", "\\\\") + @""", | ||||||||||||||||||||||
| ""LastVersion"": ""1.2.3"" | ||||||||||||||||||||||
| }"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", processInfoJson); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| System.IO.Directory.CreateDirectory(testPath); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Act & Assert | ||||||||||||||||||||||
| // Should not throw ArgumentNullException for ProcessInfo | ||||||||||||||||||||||
| var exception = Record.Exception(() => Bowl.Launch()); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Should not be ArgumentNullException related to ProcessInfo | ||||||||||||||||||||||
| Assert.True(exception == null || | ||||||||||||||||||||||
| exception is not ArgumentNullException || | ||||||||||||||||||||||
| !exception.Message.Contains("ProcessInfo")); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| finally | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // Cleanup | ||||||||||||||||||||||
| Environment.SetEnvironmentVariable("ProcessInfo", originalValue); | ||||||||||||||||||||||
| if (System.IO.Directory.Exists(testPath)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| try { System.IO.Directory.Delete(testPath, true); } catch { } | ||||||||||||||||||||||
|
||||||||||||||||||||||
| try { System.IO.Directory.Delete(testPath, true); } catch { } | |
| try | |
| { | |
| System.IO.Directory.Delete(testPath, true); | |
| } | |
| catch (Exception ex) | |
| { | |
| // Best-effort cleanup: do not fail the test on delete errors, but log for diagnostics | |
| Console.WriteLine($"[BowlTests] Failed to delete test directory '{testPath}': {ex}"); | |
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The solution file shows that the BowlTest project is replacing ClientCoreTest project (GUID 18E96D5E-9D34-4047-B75E-7D832A055FD2 is being removed and GUID 46165893-12E1-4131-8D2A-BD18676B1CEE is being added). However, the ClientCoreTest directory still exists in the tests folder. This appears to be an error - the change should add BowlTest without removing ClientCoreTest. The ClientCoreTest project should remain in the solution alongside the new BowlTest project.