diff --git a/.editorconfig b/.editorconfig
index 1cc80b05..f42c94e8 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,6 +4,7 @@ root = true
dotnet_sort_system_directives_first = false
# XML documentation
+dotnet_diagnostic.CS1574.severity = warning
dotnet_diagnostic.CS1591.severity = warning
# ASP.NET
@@ -11,13 +12,16 @@ dotnet_diagnostic.ASP0015.severity = warning
# Code Analysis (CA)
dotnet_diagnostic.CA1050.severity = warning
+dotnet_diagnostic.CA1416.severity = warning
dotnet_diagnostic.CA1401.severity = warning
dotnet_diagnostic.CA1806.severity = warning
dotnet_diagnostic.CA1816.severity = warning
dotnet_diagnostic.CA1822.severity = warning
dotnet_diagnostic.CA1826.severity = warning
+dotnet_diagnostic.CA1834.severity = warning
dotnet_diagnostic.CA1847.severity = warning
dotnet_diagnostic.CA1859.severity = warning
+dotnet_diagnostic.CA1861.severity = warning
dotnet_diagnostic.CA1869.severity = warning
dotnet_diagnostic.CA2016.severity = warning
@@ -30,11 +34,14 @@ dotnet_diagnostic.IDE0059.severity = warning
dotnet_diagnostic.IDE0060.severity = warning
dotnet_diagnostic.IDE0074.severity = warning
dotnet_diagnostic.IDE0180.severity = warning
+dotnet_diagnostic.IDE0230.severity = warning
dotnet_diagnostic.IDE0270.severity = warning
dotnet_diagnostic.IDE0290.severity = warning
+dotnet_diagnostic.IDE0300.severity = warning
dotnet_diagnostic.IDE0301.severity = warning
dotnet_diagnostic.IDE0305.severity = warning
dotnet_diagnostic.IDE0330.severity = warning
+dotnet_diagnostic.IDE1006.severity = warning
# System library
dotnet_diagnostic.SYSLIB1054.severity = warning
diff --git a/.gitattributes b/.gitattributes
index b9e03f9d..c185d0b0 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -4,3 +4,4 @@
*.bats text eol=lf
*.bash text eol=lf
scripts/*.cs text eol=lf
+scripts/**/*.cs text eol=lf
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d70a4a3e..216b1807 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,14 +32,51 @@ jobs:
run: dotnet build --no-restore
- name: Test
- run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
+ timeout-minutes: 30
+ shell: bash
+ run: |
+ set -euo pipefail
- - name: Upload coverage
+ for project in \
+ tests/Dotsider.Tests/Dotsider.Tests.csproj \
+ tests/Dotsider.Mcp.Tests/Dotsider.Mcp.Tests.csproj \
+ tests/Dotsider.Website.Tests/Dotsider.Website.Tests.csproj
+ do
+ project_name="$(basename "$(dirname "$project")")"
+ result_dir="TestResults/${{ matrix.os }}/${project_name}"
+ mkdir -p "$result_dir"
+ export DOTSIDER_R2R_DIAGNOSTICS=1
+ export DOTSIDER_R2R_DIAGNOSTICS_PATH="$result_dir/r2r-diagnostics.log"
+ : > "$DOTSIDER_R2R_DIAGNOSTICS_PATH"
+ set +e
+ dotnet test "$project" \
+ --no-build \
+ --report-trx \
+ --results-directory "$result_dir" \
+ --coverage \
+ --coverage-output-format cobertura \
+ --diagnostic \
+ --diagnostic-verbosity Trace \
+ --diagnostic-output-directory "$result_dir" \
+ --diagnostic-file-prefix "$project_name" \
+ 2>&1 | tee "$result_dir/dotnet-test.log"
+ status=${PIPESTATUS[0]}
+ set -e
+ if [ "$status" -ne 0 ]; then
+ exit "$status"
+ fi
+ done
+
+ - name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v7
with:
- name: coverage-${{ matrix.os }}
- path: '**/TestResults/**/coverage.cobertura.xml'
+ name: test-results-${{ matrix.os }}
+ path: |
+ TestResults/**/*.trx
+ TestResults/**/*.cobertura.xml
+ TestResults/**/*.diag
+ TestResults/**/*.log
deploy-tests:
runs-on: ubuntu-latest
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 00000000..c4459fac
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+ latest
+ true
+ true
+
+
diff --git a/Dotsider.slnx b/Dotsider.slnx
index 59a760ff..2ebf7a6e 100644
--- a/Dotsider.slnx
+++ b/Dotsider.slnx
@@ -1,4 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -40,12 +51,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Dotsider.Benchmarks/Dotsider.Benchmarks.csproj b/benchmarks/Dotsider.Benchmarks/Dotsider.Benchmarks.csproj
index 23c4cc51..1c825d49 100644
--- a/benchmarks/Dotsider.Benchmarks/Dotsider.Benchmarks.csproj
+++ b/benchmarks/Dotsider.Benchmarks/Dotsider.Benchmarks.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..7c02fe68
--- /dev/null
+++ b/global.json
@@ -0,0 +1,8 @@
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ },
+ "msbuild-sdks": {
+ "MSTest.Sdk": "4.3.0"
+ }
+}
diff --git a/samples/AppLocalRollForward/AppLocalRollForward.csproj b/samples/AppLocalRollForward/AppLocalRollForward.csproj
index 1927ce7b..a5743a1b 100644
--- a/samples/AppLocalRollForward/AppLocalRollForward.csproj
+++ b/samples/AppLocalRollForward/AppLocalRollForward.csproj
@@ -24,7 +24,7 @@
build of the same package, so the deployed app-local DLL has the same simple
name and PKT but a strictly greater assembly version. That divergence is the
scenario the resolver fix exists to handle. -->
-
+
diff --git a/samples/NativeAotArtifactsConsole/Directory.Build.props b/samples/NativeAotArtifactsConsole/Directory.Build.props
index b38e4240..365f9f68 100644
--- a/samples/NativeAotArtifactsConsole/Directory.Build.props
+++ b/samples/NativeAotArtifactsConsole/Directory.Build.props
@@ -1,7 +1,9 @@
+ project file). Scoped to this sample. -->
+
+
true
diff --git a/samples/NetFxBindingRedirects.Clr2/Program.cs b/samples/NetFxBindingRedirects.Clr2/Program.cs
index 52e61c7b..d72c0210 100644
--- a/samples/NetFxBindingRedirects.Clr2/Program.cs
+++ b/samples/NetFxBindingRedirects.Clr2/Program.cs
@@ -167,7 +167,7 @@ private static string ToJson(IDictionary map)
sb.Append(" \"fullName\": ").Append(JsonString(kv.Value.FullName)).Append(",\n");
sb.Append(" \"location\": ").Append(JsonString(kv.Value.Location)).Append(",\n");
sb.Append(" \"loaded\": ").Append(kv.Value.Loaded ? "true" : "false").Append(",\n");
- sb.Append(" \"error\": ").Append(JsonString(kv.Value.Error)).Append("\n");
+ sb.Append(" \"error\": ").Append(JsonString(kv.Value.Error)).Append('\n');
sb.Append(" }");
}
sb.Append("\n}\n");
diff --git a/samples/NetFxBindingRedirects.NewDep/NetFxBindingRedirects.NewDep.csproj b/samples/NetFxBindingRedirects.NewDep/NetFxBindingRedirects.NewDep.csproj
index bc5bec17..0360f266 100644
--- a/samples/NetFxBindingRedirects.NewDep/NetFxBindingRedirects.NewDep.csproj
+++ b/samples/NetFxBindingRedirects.NewDep/NetFxBindingRedirects.NewDep.csproj
@@ -12,7 +12,7 @@
-
-
+
+
diff --git a/samples/NetFxBindingRedirects.OldDep/NetFxBindingRedirects.OldDep.csproj b/samples/NetFxBindingRedirects.OldDep/NetFxBindingRedirects.OldDep.csproj
index 334b4544..de6b9fe2 100644
--- a/samples/NetFxBindingRedirects.OldDep/NetFxBindingRedirects.OldDep.csproj
+++ b/samples/NetFxBindingRedirects.OldDep/NetFxBindingRedirects.OldDep.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/samples/NetFxBindingRedirects/NetFxBindingRedirects.csproj b/samples/NetFxBindingRedirects/NetFxBindingRedirects.csproj
index 4dc3c4cd..b12c7ff1 100644
--- a/samples/NetFxBindingRedirects/NetFxBindingRedirects.csproj
+++ b/samples/NetFxBindingRedirects/NetFxBindingRedirects.csproj
@@ -1,7 +1,7 @@
+
@@ -44,27 +45,17 @@
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/samples/NetFxBindingRedirects/Program.cs b/samples/NetFxBindingRedirects/Program.cs
index cfe5e96c..e809d7db 100644
--- a/samples/NetFxBindingRedirects/Program.cs
+++ b/samples/NetFxBindingRedirects/Program.cs
@@ -132,7 +132,7 @@ private static string ToJson(IDictionary map)
sb.Append(" \"fullName\": ").Append(JsonString(kv.Value.FullName)).Append(",\n");
sb.Append(" \"location\": ").Append(JsonString(kv.Value.Location)).Append(",\n");
sb.Append(" \"loaded\": ").Append(kv.Value.Loaded ? "true" : "false").Append(",\n");
- sb.Append(" \"error\": ").Append(JsonString(kv.Value.Error)).Append("\n");
+ sb.Append(" \"error\": ").Append(JsonString(kv.Value.Error)).Append('\n');
sb.Append(" }");
}
sb.Append("\n}\n");
diff --git a/scripts/Directory.Build.props b/scripts/Directory.Build.props
index dfa77527..f7382f00 100644
--- a/scripts/Directory.Build.props
+++ b/scripts/Directory.Build.props
@@ -1,14 +1,16 @@
-
- latest
- enable
- enable
- true
- latest
- true
- true
- true
- false
- false
-
+
+
+
+ latest
+ enable
+ enable
+ true
+ latest
+ true
+ true
+ true
+ false
+ false
+
diff --git a/scripts/README.md b/scripts/README.md
index e6020f5d..9db9a9f7 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,125 +1,30 @@
# Scripts
dotsider repository utilities are .NET file-based apps. They require the .NET 10
-SDK.
-
-The implementation follows the Microsoft file-based app guidance:
+SDK and follow the Microsoft file-based app guidance:
https://learn.microsoft.com/en-us/dotnet/core/sdk/file-based-apps
-Run a utility with:
+Run a utility with `dotnet run --file`:
```powershell
+dotnet run --file ./scripts/Run-Tests.cs
dotnet run --file ./scripts/Capture-DisasmOracle.cs -- -Architecture riscv64 -Fixture path/to/blob.bin -OraclePath llvm-objdump -OutputDirectory artifacts/oracles/disasm -- -D -b binary -m riscv:rv64 path/to/blob.bin
```
-Large disassembly tools can emit very large streams. Use
-`-MaxOutputCharacters` to cap retained stdout/stderr while still draining the
-process to completion, `-TimeoutSeconds` to kill long-running oracle tools, and
-`-AllowOracleFailure` when the capture should upload diagnostics from a failing
-oracle tool instead of failing before artifacts are saved.
-
-For automation or repeated invocations, build first and run without rebuilding:
-
-```powershell
-dotnet build ./scripts/Capture-DisasmOracle.cs --nologo --verbosity quiet
-dotnet run --file ./scripts/Capture-DisasmOracle.cs --no-build -- -Architecture riscv64 -Fixture path/to/blob.bin -OraclePath llvm-objdump -OutputDirectory artifacts/oracles/disasm -- -D -b binary -m riscv:rv64 path/to/blob.bin
-```
-
-The build-first pattern avoids file-based app cache contention when multiple
-processes might run the same utility at the same time.
-
-If the SDK cache gets stale or contested, clear all file-based app cache output:
-
-```powershell
-dotnet clean file-based-apps
-```
-
-To force a clean build for one utility:
-
-```powershell
-dotnet clean ./scripts/Capture-DisasmOracle.cs
-dotnet build ./scripts/Capture-DisasmOracle.cs
-```
-
-The executable utilities have a Unix shebang:
-
-```text
-#!/usr/bin/env -S dotnet --
-```
-
-The repository uses LF line endings through `.gitattributes`, which is required
-for direct Unix execution. On Unix-like systems, executable files can also be run
-directly after checkout when the executable bit is present:
-
-```bash
-./scripts/Capture-DisasmOracle.cs -Architecture riscv64 -Fixture path/to/blob.bin -OraclePath llvm-objdump -OutputDirectory artifacts/oracles/disasm -- -D -b binary -m riscv:rv64 path/to/blob.bin
-```
-
Keep file-based apps under `scripts/`, outside project directories. The local
`scripts/Directory.Build.props` intentionally isolates utility app settings from
-package metadata and project settings used by the shipped dotsider projects.
-
-Repository tests build every executable utility app with `dotnet build` and run
-stable fake-input coverage for the disassembly oracle capture app. When adding a
-utility, keep its top-level launcher thin and put behavior in a documented app
-class or documented helper methods so XML documentation and convention tests
-cover hoverable code.
+package metadata and project settings used by shipped dotsider projects.
-dotsider decoder tests prefer real sample assemblies over hand-written byte
-fixtures. The shared test fixture publishes cross-RID ReadyToRun samples for
-architecture-specific coverage when the SDK provides the RID packs. Use
-`Capture-DisasmOracle.cs` for architectures that require runtime-built inputs or
-external oracle captures, then review and promote the normalized artifacts into
-committed fixtures.
-
-Normal `dotnet publish` cannot always produce every decoder target on every
-machine. Current public SDK feeds may not include runtime/host packs for
-`linux-riscv64` or `linux-loongarch64`; those ReadyToRun fixture paths are null
-when restore cannot resolve the packs. Browser and WASI Wasm do not support
-ReadyToRun publishing; the browser-wasm fixture publishes with
-`PublishReadyToRun=false` and decodes the real SDK-produced
-`dotnet.native.wasm` module instead. If a runtime-built input or an external
-oracle is needed, capture it with the file-based utility and commit the reviewed
-fixture metadata, including the runtime source files used as ground truth.
-The .NET runtime repo validates RISC-V64 and LoongArch64 through its
-cross-target runtime pipeline and SuperPMI/crossgen2 collections, so dotsider CI
-does not try to build private runtime packs as part of normal test execution.
-
-The `Native architecture oracles` workflow is the outer-loop capture pipeline.
-It can be run manually from GitHub Actions and also refreshes SDK-backed oracle
-captures on a weekly schedule. The default path installs `wasm-tools`, publishes
-the browser-wasm fixture, opportunistically tries SDK ReadyToRun publishes for
-`linux-riscv64` and `linux-loongarch64`, captures any available oracle output
-with `Capture-DisasmOracle.cs`, and uploads the unreviewed artifacts.
-
-When public SDK packs are unavailable, run the workflow manually with
-`run-runtime-cross-target=true`. That opt-in path checks out `dotnet/runtime` at
-the requested ref and uses the same pinned Azure Linux cross-build containers
-and rootfs paths that runtime uses for RISC-V64 and LoongArch64. The resulting
-runtime logs and artifact locations are uploaded for review; promoted fixtures
-should still flow through `Capture-DisasmOracle.cs` so every committed oracle
-records the producer ref, fixture hash, and command line.
-
-For local development, the same shape applies:
-
-```powershell
-$env:DOTSIDER_RUNTIME_ROOT = "D:\SRC\runtime"
-dotnet build ./scripts/Capture-DisasmOracle.cs --nologo --verbosity quiet
-dotnet run --file ./scripts/Capture-DisasmOracle.cs --no-build -- `
- -Architecture loongarch64 `
- -Fixture artifacts/oracles/input/loongarch64-smoke.bin `
- -OraclePath llvm-objdump `
- -RuntimeRoot $env:DOTSIDER_RUNTIME_ROOT `
- -OutputDirectory artifacts/oracles/disasm `
- -- -d artifacts/oracles/input/loongarch64-smoke.bin
-```
+The executable utilities start with `#!/usr/bin/env -S dotnet --`, and the
+repository enforces LF line endings for script files so direct Unix execution
+works when the executable bit is present.
Current utilities:
| App | Purpose |
| --- | --- |
| `Capture-DisasmOracle.cs` | Capture external native-disassembly oracle output and metadata. |
+| `Run-Tests.cs` | Run `dotnet test` once or repeatedly with forwarded test arguments. |
-Oracle captures stay under ignored `artifacts/` paths until they have been
-reviewed and normalized into committed test fixtures.
+Use each script's XML documentation and command-line help for option details.
diff --git a/scripts/Run-Tests.cs b/scripts/Run-Tests.cs
new file mode 100644
index 00000000..fa3696a8
--- /dev/null
+++ b/scripts/Run-Tests.cs
@@ -0,0 +1,397 @@
+#!/usr/bin/env -S dotnet --
+#:property TargetFramework=net10.0
+#:property PackAsTool=false
+#:package System.CommandLine@2.0.9
+#:include ScriptSupport.cs
+
+using System.CommandLine;
+using System.Diagnostics;
+
+try
+{
+ return TestRunApp.Run(args);
+}
+catch (Exception ex) when (ex is ArgumentException or DirectoryNotFoundException or FileNotFoundException or InvalidOperationException)
+{
+ Console.Error.WriteLine(ex.Message);
+ return 1;
+}
+
+///
+/// Runs the repository test suite through dotnet test.
+/// The app can repeat the same command to expose parallelization flakes.
+/// Extra arguments are forwarded unchanged after the script argument separator.
+/// System.CommandLine provides option parsing and help output.
+/// Child dotnet test processes disable MSBuild node reuse so repeat runs
+/// do not leave idle build worker processes behind on developer machines.
+///
+///
+/// With no arguments, dotnet run --file ./scripts/Run-Tests.cs runs
+/// dotnet test once for the full solution. Use -Count to repeat
+/// the same command, -Target to select a project or solution, and
+/// -- to forward native dotnet test arguments such as
+/// --filter "FullyQualifiedName~SomeTest".
+///
+internal static class TestRunApp
+{
+ private const string DisableMsBuildNodeReuseVariable = "MSBUILDDISABLENODEREUSE";
+
+ ///
+ /// Parses script arguments and executes one or more dotnet test attempts.
+ /// The first failing exit code is returned after all attempts finish.
+ /// Use -StopOnFailure when an early failing attempt is enough evidence.
+ ///
+ /// The command-line arguments.
+ /// The process exit code.
+ internal static int Run(string[] args)
+ {
+ Option countOption = new("--count", "-Count")
+ {
+ HelpName = "N",
+ Description = "Number of test attempts. Defaults to 1.",
+ };
+ Option repeatOption = new("--repeat", "-Repeat")
+ {
+ HelpName = "N",
+ Description = "Alias for -Count.",
+ };
+ Option targetOption = new("--target", "-Target")
+ {
+ HelpName = "PATH",
+ Description = "Optional solution, project, or directory passed to dotnet test.",
+ };
+ Option workingDirectoryOption = new("--working-directory", "-WorkingDirectory")
+ {
+ HelpName = "DIR",
+ Description = "Directory for dotnet test. Defaults to the repository root.",
+ };
+ Option timeoutSecondsOption = new("--timeout-seconds", "-TimeoutSeconds")
+ {
+ HelpName = "SECONDS",
+ Description = "Per-attempt process timeout in seconds.",
+ };
+ Option logDirectoryOption = new("--log-directory", "-LogDirectory")
+ {
+ HelpName = "DIR",
+ Description = "Write one combined stdout/stderr log per attempt.",
+ };
+ Option maxOutputCharactersOption = new("--max-output-characters", "-MaxOutputCharacters")
+ {
+ HelpName = "CHARS",
+ Description = "Maximum stdout/stderr characters retained per stream when logging.",
+ DefaultValueFactory = _ => 4_000_000,
+ };
+ Option stopOnFailureOption = new("--stop-on-failure", "-StopOnFailure")
+ {
+ Description = "Stop after the first failing attempt.",
+ };
+ Argument dotnetTestArgument = new("dotnet-test-arguments")
+ {
+ HelpName = "ARGS",
+ Description = "Arguments forwarded to dotnet test after --.",
+ Arity = ArgumentArity.ZeroOrMore,
+ };
+ RootCommand rootCommand = new("Runs dotnet test once or repeatedly.")
+ {
+ countOption,
+ repeatOption,
+ targetOption,
+ workingDirectoryOption,
+ timeoutSecondsOption,
+ logDirectoryOption,
+ maxOutputCharactersOption,
+ stopOnFailureOption,
+ dotnetTestArgument,
+ };
+ rootCommand.SetAction(parseResult =>
+ {
+ string repositoryRoot = ScriptSupport.FindRepositoryRoot();
+ string workingDirectory = ResolveWorkingDirectory(parseResult.GetValue(workingDirectoryOption), repositoryRoot);
+ string target = ResolveTarget(parseResult.GetValue(targetOption), workingDirectory);
+ int count = ResolveCount(parseResult.GetValue(countOption), parseResult.GetValue(repeatOption));
+ TimeSpan? timeout = ResolveTimeout(parseResult.GetValue(timeoutSecondsOption));
+ string logDirectory = ResolveLogDirectory(parseResult.GetValue(logDirectoryOption), workingDirectory);
+ int maxOutputCharacters = ValidatePositive(parseResult.GetValue(maxOutputCharactersOption), "MaxOutputCharacters");
+ bool stopOnFailure = parseResult.GetValue(stopOnFailureOption);
+ string[] dotnetTestArguments = parseResult.GetValue(dotnetTestArgument) ?? [];
+
+ return RunAttempts(
+ workingDirectory,
+ target,
+ count,
+ timeout,
+ logDirectory,
+ maxOutputCharacters,
+ stopOnFailure,
+ dotnetTestArguments);
+ });
+
+ return rootCommand.Parse(NormalizeLegacyHelpAliases(args)).Invoke();
+ }
+
+ private static int RunAttempts(
+ string workingDirectory,
+ string target,
+ int count,
+ TimeSpan? timeout,
+ string logDirectory,
+ int maxOutputCharacters,
+ bool stopOnFailure,
+ string[] dotnetTestArguments)
+ {
+ var commandArguments = new List { "test" };
+ if (!string.IsNullOrWhiteSpace(target))
+ {
+ commandArguments.Add(target);
+ }
+
+ commandArguments.AddRange(dotnetTestArguments);
+
+ var failureCount = 0;
+ var firstFailureExitCode = 0;
+ var attemptsRun = 0;
+ var total = Stopwatch.StartNew();
+
+ for (var attempt = 1; attempt <= count; attempt++)
+ {
+ attemptsRun++;
+ Console.WriteLine();
+ Console.WriteLine($"dotnet test attempt {attempt}/{count}");
+ Console.WriteLine($"dotnet {FormatCommand(commandArguments)}");
+
+ TestAttemptResult result = string.IsNullOrWhiteSpace(logDirectory)
+ ? RunDotnetTestStreaming(commandArguments, workingDirectory, timeout)
+ : RunDotnetTestCaptured(commandArguments, workingDirectory, timeout, logDirectory, maxOutputCharacters, attempt);
+
+ string outcome = result.ExitCode == 0 ? "passed" : result.TimedOut ? "timed out" : "failed";
+ Console.WriteLine($"attempt {attempt}/{count} {outcome} in {result.Elapsed:c} (exit code {result.ExitCode})");
+ if (!string.IsNullOrWhiteSpace(result.LogPath))
+ {
+ Console.WriteLine(result.LogPath);
+ }
+
+ if (result.ExitCode != 0)
+ {
+ failureCount++;
+ firstFailureExitCode = firstFailureExitCode == 0 ? result.ExitCode : firstFailureExitCode;
+ if (stopOnFailure)
+ {
+ break;
+ }
+ }
+ }
+
+ Console.WriteLine();
+ Console.WriteLine($"test attempts completed: {attemptsRun} requested={count} failed={failureCount} elapsed={total.Elapsed:c}");
+ return failureCount == 0 ? 0 : firstFailureExitCode;
+ }
+
+ private static TestAttemptResult RunDotnetTestStreaming(
+ List commandArguments,
+ string workingDirectory,
+ TimeSpan? timeout)
+ {
+ var startInfo = new ProcessStartInfo("dotnet")
+ {
+ UseShellExecute = false,
+ WorkingDirectory = workingDirectory,
+ };
+ ConfigureDotnetTestEnvironment(startInfo.Environment);
+ foreach (string argument in commandArguments)
+ {
+ startInfo.ArgumentList.Add(argument);
+ }
+
+ var stopwatch = Stopwatch.StartNew();
+ using Process process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start dotnet.");
+ bool timedOut = WaitForExit(process, timeout);
+ return new TestAttemptResult(timedOut ? 124 : process.ExitCode, stopwatch.Elapsed, timedOut, "");
+ }
+
+ private static TestAttemptResult RunDotnetTestCaptured(
+ List commandArguments,
+ string workingDirectory,
+ TimeSpan? timeout,
+ string logDirectory,
+ int maxOutputCharacters,
+ int attempt)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ (int exitCode, string stdout, string stderr, bool stdoutTruncated, bool stderrTruncated, bool timedOut) = ScriptSupport.RunProcess(
+ "dotnet",
+ commandArguments,
+ workingDirectory,
+ maxOutputCharacters,
+ timeout,
+ CreateDotnetTestEnvironment());
+
+ string logPath = Path.Combine(logDirectory, $"dotnet-test-{attempt:0000}.log");
+ ScriptSupport.WriteTextFile(
+ logPath,
+ string.Join(
+ Environment.NewLine,
+ $"WorkingDirectory: {workingDirectory}",
+ $"Command: dotnet {FormatCommand(commandArguments)}",
+ $"ExitCode: {(timedOut ? 124 : exitCode)}",
+ $"TimedOut: {timedOut}",
+ $"StdoutTruncated: {stdoutTruncated}",
+ $"StderrTruncated: {stderrTruncated}",
+ "",
+ "stdout:",
+ stdout,
+ "",
+ "stderr:",
+ stderr));
+
+ Console.Write(stdout);
+ if (!string.IsNullOrWhiteSpace(stderr))
+ {
+ Console.Error.Write(stderr);
+ }
+
+ return new TestAttemptResult(timedOut ? 124 : exitCode, stopwatch.Elapsed, timedOut, logPath);
+ }
+
+ private static Dictionary CreateDotnetTestEnvironment() =>
+ new(StringComparer.OrdinalIgnoreCase)
+ {
+ [DisableMsBuildNodeReuseVariable] = "1",
+ };
+
+ private static void ConfigureDotnetTestEnvironment(IDictionary environment)
+ {
+ environment[DisableMsBuildNodeReuseVariable] = "1";
+ }
+
+ private static bool WaitForExit(Process process, TimeSpan? timeout)
+ {
+ if (timeout is null)
+ {
+ process.WaitForExit();
+ return false;
+ }
+
+ int milliseconds = checked((int)Math.Min(timeout.Value.TotalMilliseconds, int.MaxValue));
+ if (milliseconds <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be positive.");
+ }
+
+ if (process.WaitForExit(milliseconds))
+ {
+ return false;
+ }
+
+ try
+ {
+ process.Kill(entireProcessTree: true);
+ }
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+
+ process.WaitForExit();
+ return true;
+ }
+
+ private static string ResolveWorkingDirectory(string? value, string repositoryRoot)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return repositoryRoot;
+ }
+
+ return ScriptSupport.ResolveWorkingDirectory(value);
+ }
+
+ private static string ResolveTarget(string? value, string workingDirectory)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return "";
+ }
+
+ string candidate = Path.IsPathFullyQualified(value)
+ ? value
+ : Path.Combine(workingDirectory, value);
+ if (File.Exists(candidate) || Directory.Exists(candidate))
+ {
+ return Path.GetFullPath(candidate);
+ }
+
+ throw new FileNotFoundException($"Target '{value}' does not exist.");
+ }
+
+ private static int ResolveCount(int? count, int? repeat) =>
+ ValidatePositive(count ?? repeat ?? 1, "Count");
+
+ private static TimeSpan? ResolveTimeout(int? timeoutSeconds) =>
+ timeoutSeconds is null
+ ? null
+ : TimeSpan.FromSeconds(ValidatePositive(timeoutSeconds.Value, "TimeoutSeconds"));
+
+ private static string ResolveLogDirectory(string? value, string workingDirectory)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return "";
+ }
+
+ string resolved = Path.IsPathFullyQualified(value)
+ ? value
+ : Path.Combine(workingDirectory, value);
+ Directory.CreateDirectory(resolved);
+ return Path.GetFullPath(resolved);
+ }
+
+ private static int ValidatePositive(int value, string optionName)
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentException($"-{optionName} must be a positive integer.");
+ }
+
+ return value;
+ }
+
+ private static string[] NormalizeLegacyHelpAliases(string[] args)
+ {
+ string[] normalized = [.. args];
+ for (var i = 0; i < normalized.Length; i++)
+ {
+ if (normalized[i].Equals("--", StringComparison.Ordinal))
+ {
+ break;
+ }
+
+ if (IsLegacyHelpAlias(normalized[i]))
+ {
+ normalized[i] = "--help";
+ }
+ }
+
+ return normalized;
+ }
+
+ private static bool IsLegacyHelpAlias(string token) =>
+ token.Equals("-help", StringComparison.OrdinalIgnoreCase)
+ || token.Equals("--help", StringComparison.OrdinalIgnoreCase)
+ || token.Equals("-?", StringComparison.OrdinalIgnoreCase)
+ || token.Equals("/?", StringComparison.OrdinalIgnoreCase);
+
+ private static string FormatCommand(IEnumerable arguments) =>
+ string.Join(' ', arguments.Select(QuoteArgument));
+
+ private static string QuoteArgument(string argument)
+ {
+ if (argument.Length == 0)
+ {
+ return "\"\"";
+ }
+
+ return argument.Any(char.IsWhiteSpace) ? $"\"{argument.Replace("\"", "\\\"", StringComparison.Ordinal)}\"" : argument;
+ }
+
+ private readonly record struct TestAttemptResult(int ExitCode, TimeSpan Elapsed, bool TimedOut, string LogPath);
+}
diff --git a/scripts/ScriptSupport.cs b/scripts/ScriptSupport.cs
index 16955660..a0f41ac3 100644
--- a/scripts/ScriptSupport.cs
+++ b/scripts/ScriptSupport.cs
@@ -306,13 +306,15 @@ internal static string ResolveCommandPath(string commandPath, string description
/// The process working directory.
/// The maximum characters to retain per stream.
/// The optional process timeout.
+ /// Optional environment variable overrides for the process.
/// The exit code, stdout, stderr, truncation state, and timeout state.
internal static (int ExitCode, string Stdout, string Stderr, bool StdoutTruncated, bool StderrTruncated, bool TimedOut) RunProcess(
string filePath,
IEnumerable arguments,
string workingDirectory,
int maxOutputCharacters = int.MaxValue,
- TimeSpan? timeout = null)
+ TimeSpan? timeout = null,
+ IReadOnlyDictionary? environment = null)
{
var startInfo = new ProcessStartInfo(filePath)
{
@@ -327,6 +329,20 @@ internal static (int ExitCode, string Stdout, string Stderr, bool StdoutTruncate
startInfo.ArgumentList.Add(argument);
}
+ if (environment is not null)
+ {
+ foreach ((string key, string? value) in environment)
+ {
+ if (value is null)
+ {
+ startInfo.Environment.Remove(key);
+ continue;
+ }
+
+ startInfo.Environment[key] = value;
+ }
+ }
+
using Process process = Process.Start(startInfo) ?? throw new InvalidOperationException($"Failed to start '{filePath}'.");
Task<(string Text, bool Truncated)> stdoutTask = Task.Run(() => ReadBoundedToEnd(process.StandardOutput, maxOutputCharacters));
Task<(string Text, bool Truncated)> stderrTask = Task.Run(() => ReadBoundedToEnd(process.StandardError, maxOutputCharacters));
diff --git a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunDiagnostics.cs b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunDiagnostics.cs
new file mode 100644
index 00000000..37939280
--- /dev/null
+++ b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunDiagnostics.cs
@@ -0,0 +1,93 @@
+using System.Diagnostics;
+
+namespace Dotsider.Core.Analysis.ReadyToRun;
+
+internal static class ReadyToRunDiagnostics
+{
+ private const string EnabledVariable = "DOTSIDER_R2R_DIAGNOSTICS";
+ private const string PathVariable = "DOTSIDER_R2R_DIAGNOSTICS_PATH";
+ private const string MaxLinesVariable = "DOTSIDER_R2R_DIAGNOSTICS_MAX_LINES";
+ private const int DefaultMaxLines = 50_000;
+
+ private static readonly Lock Gate = new();
+ private static readonly bool Enabled = IsEnabled(Environment.GetEnvironmentVariable(EnabledVariable));
+ private static readonly string? LogPath = Enabled ? NormalizePath(Environment.GetEnvironmentVariable(PathVariable)) : null;
+ private static readonly int MaxLines = ResolveMaxLines(Environment.GetEnvironmentVariable(MaxLinesVariable));
+ private static readonly Lazy Writer = new(CreateWriter);
+ private static int s_linesWritten;
+ private static int s_truncated;
+
+ public static void Write(string message)
+ {
+ if (!Enabled)
+ return;
+
+ if (MaxLines > 0)
+ {
+ var lineNumber = Interlocked.Increment(ref s_linesWritten);
+ if (lineNumber > MaxLines)
+ {
+ if (Interlocked.Exchange(ref s_truncated, 1) == 0)
+ WriteCore($"diagnostics-truncated max-lines={MaxLines}");
+ return;
+ }
+ }
+
+ WriteCore(message);
+ }
+
+ private static void WriteCore(string message)
+ {
+ var line = $"{DateTimeOffset.UtcNow:O} pid={Environment.ProcessId} tid={Environment.CurrentManagedThreadId} {message}";
+ try
+ {
+ if (LogPath is { Length: > 0 })
+ {
+ lock (Gate)
+ {
+ Writer.Value?.WriteLine(line);
+ }
+ }
+ else
+ {
+ Trace.WriteLine(line);
+ }
+ }
+ catch (Exception)
+ {
+ // Diagnostics must never change ReadyToRun parsing behavior.
+ }
+ }
+
+ private static bool IsEnabled(string? value) =>
+ value is not null
+ && !string.Equals(value, "0", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(value, "false", StringComparison.OrdinalIgnoreCase);
+
+ private static string? NormalizePath(string? value) =>
+ string.IsNullOrWhiteSpace(value) ? null : Path.GetFullPath(value);
+
+ private static int ResolveMaxLines(string? value) =>
+ int.TryParse(value, out var maxLines) && maxLines >= 0 ? maxLines : DefaultMaxLines;
+
+ private static StreamWriter? CreateWriter()
+ {
+ if (LogPath is not { Length: > 0 })
+ return null;
+
+ try
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(LogPath) ?? ".");
+ var writer = new StreamWriter(new FileStream(LogPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
+ {
+ AutoFlush = true
+ };
+ writer.WriteLine($"{DateTimeOffset.UtcNow:O} pid={Environment.ProcessId} diagnostics-start");
+ return writer;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImageReader.cs b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImageReader.cs
index f98b05fd..2e1b9591 100644
--- a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImageReader.cs
+++ b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImageReader.cs
@@ -193,7 +193,7 @@ private static ReadyToRunModel BuildComponent(AssemblyAnalyzer analyzer, ReadyTo
? new ReadyToRunMethodMapReader.GlobalInstanceSource(
io, instance.Size, manifestReader, owner.AssemblyName ?? "", Guid.Empty, [])
: (ReadyToRunMethodMapReader.GlobalInstanceSource?)null;
- allMethods = SafeBuild(tables, sources, global, moduleContext);
+ allMethods = SafeBuild(tables, sources, global, moduleContext, mvid);
}
finally
{
@@ -247,13 +247,14 @@ private static List SafeBuild(
Tables tables,
IReadOnlyList sources,
ReadyToRunMethodMapReader.GlobalInstanceSource? global,
- ReadyToRunModuleContext? moduleContext = null)
+ ReadyToRunModuleContext? moduleContext = null,
+ Guid? targetMvid = null)
{
try
{
return [.. ReadyToRunMethodMapReader.Build(
tables.Reader, tables.RuntimeFunctions, tables.HotColdMap,
- tables.ImageBase, tables.AddressSpace, sources, global, moduleContext)];
+ tables.ImageBase, tables.AddressSpace, sources, global, moduleContext, targetMvid)];
}
catch (Exception ex) when (ex is BadImageFormatException or IndexOutOfRangeException or ArgumentOutOfRangeException)
{
diff --git a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImportReader.cs b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImportReader.cs
index c3670254..b6ea7fc6 100644
--- a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImportReader.cs
+++ b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunImportReader.cs
@@ -220,6 +220,8 @@ private static void ReadRecord(
R2RNativeReader reader, int offset, MetadataReader? metadata, IReadOnlyList methodDefs,
ReadyToRunModuleContext? moduleContext)
{
+ Func? resolveMetadata =
+ moduleContext is null ? null : moduleContext.ResolveMetadata;
uint fixup = reader.ReadByte(ref offset);
string? modulePrefix = null;
if ((fixup & FixupModuleOverride) != 0)
@@ -245,7 +247,8 @@ private static void ReadRecord(
{
case FixupMethodEntry or FixupMethodHandle or FixupVirtualEntry:
{
- var sig = ReadyToRunSignatureWalker.ParseMethod(reader, offset, metadata);
+ var sig = ReadyToRunSignatureWalker.ParseMethod(
+ reader, offset, metadata, resolveMetadata);
return DecorateMethod(sig, metadata, methodDefs, modulePrefix);
}
@@ -257,7 +260,8 @@ private static void ReadRecord(
case FixupPInvokeTarget or FixupIndirectPInvokeTarget:
{
- var sig = ReadyToRunSignatureWalker.ParseMethod(reader, offset, metadata);
+ var sig = ReadyToRunSignatureWalker.ParseMethod(
+ reader, offset, metadata, resolveMetadata);
var name = DecorateMethod(sig, metadata, methodDefs, modulePrefix);
return name is null ? null : $"{name} (pinvoke)";
}
diff --git a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunMethodMapReader.cs b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunMethodMapReader.cs
index 733073ba..1396c8ad 100644
--- a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunMethodMapReader.cs
+++ b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunMethodMapReader.cs
@@ -50,7 +50,8 @@ public static IReadOnlyList Build(
NativeAddressSpace addressSpace,
IReadOnlyList sources,
GlobalInstanceSource? globalInstance,
- ReadyToRunModuleContext? moduleContext = null)
+ ReadyToRunModuleContext? moduleContext = null,
+ Guid? targetMvid = null)
{
var isEntryPoint = new bool[runtimeFunctions.Count];
var pending = new List();
@@ -59,9 +60,9 @@ public static IReadOnlyList Build(
// methods and the image's instantiated generics — before counting, so funclet grouping
// never runs past the map.
foreach (var source in sources)
- MarkMethodDefEntryPoints(reader, source, isEntryPoint, pending);
+ MarkMethodDefEntryPoints(reader, source, isEntryPoint, pending, targetMvid);
if (globalInstance is { } instance)
- MarkInstanceMethodEntryPoints(reader, instance, moduleContext, isEntryPoint, pending);
+ MarkInstanceMethodEntryPoints(reader, instance, moduleContext, isEntryPoint, pending, targetMvid);
// Pass 2/3: count each method's runtime functions, then materialize its code ranges.
var methodsByToken = new Dictionary<(string, int), MethodDefInfo>();
@@ -85,7 +86,8 @@ public static IReadOnlyList Build(
}
private static void MarkMethodDefEntryPoints(
- R2RNativeReader reader, MethodMapSource source, bool[] isEntryPoint, List pending)
+ R2RNativeReader reader, MethodMapSource source, bool[] isEntryPoint,
+ List pending, Guid? targetMvid)
{
var array = new R2RNativeArray(reader, source.EntryPointsFileOffset);
for (uint rid = 1; rid <= array.Count; rid++)
@@ -98,6 +100,9 @@ private static void MarkMethodDefEntryPoints(
continue;
isEntryPoint[entryId] = true;
+ if (!ShouldMaterialize(targetMvid, source.Mvid))
+ continue;
+
pending.Add(new PendingEntry(
source.AssemblyName, source.Mvid, source, (int)(0x0600_0000 | rid),
entryId, IsGeneric: false, Instantiation: null));
@@ -106,16 +111,22 @@ private static void MarkMethodDefEntryPoints(
private static void MarkInstanceMethodEntryPoints(
R2RNativeReader reader, GlobalInstanceSource instance, ReadyToRunModuleContext? moduleContext,
- bool[] isEntryPoint, List pending)
+ bool[] isEntryPoint, List pending, Guid? targetMvid)
{
if (instance.Size <= 0)
return;
var table = new R2RNativeHashtable(reader, instance.Offset, instance.Offset + instance.Size);
+ Func? resolveMetadata =
+ moduleContext is null ? null : moduleContext.ResolveMetadata;
foreach (var entryOffset in table.AllEntryOffsets())
{
// The payload is a method signature followed by the runtime-function index.
- var sig = ReadyToRunSignatureWalker.ParseMethod(reader, entryOffset, instance.Metadata);
+ var metadata = targetMvid is null ? instance.Metadata : null;
+ Func? metadataResolver =
+ targetMvid is null ? resolveMetadata : null;
+ var sig = ReadyToRunSignatureWalker.ParseMethod(
+ reader, entryOffset, metadata, metadataResolver);
var entryId = DecodeRuntimeFunctionIndex(reader, sig.Offset);
if (entryId < 0 || entryId >= isEntryPoint.Length)
continue;
@@ -124,21 +135,37 @@ private static void MarkInstanceMethodEntryPoints(
// A module override attributes the instantiation to a component (composite); resolve it
// there so its token, name, and owner identity are recovered rather than left unnamed.
- if (sig.ModuleIndex >= 0 && moduleContext?.Resolve(sig.ModuleIndex) is { } module)
+ if (sig.ModuleIndex >= 0)
{
- var reparsed = module.Provider is { } p
- ? ReadyToRunSignatureWalker.ParseMethod(reader, entryOffset, p.GetMetadataReader())
- : sig;
- var crossToken = (reparsed.MethodToken & 0xFF00_0000) == 0x0600_0000 ? reparsed.MethodToken : 0;
- var source = module.Provider is { } provider
- ? new MethodMapSource(module.AssemblyName, module.Mvid, 0, provider.MethodDefs, provider.GetMetadataReader())
- : (MethodMapSource?)null;
- pending.Add(new PendingEntry(
- module.AssemblyName, module.Mvid, source, crossToken,
- entryId, IsGeneric: true, reparsed.InstantiationDisplay));
- continue;
+ if (moduleContext?.Resolve(sig.ModuleIndex) is not { } module)
+ {
+ if (targetMvid is not null)
+ continue;
+ }
+ else
+ {
+ if (!ShouldMaterialize(targetMvid, module.Mvid))
+ continue;
+
+ var resolvedMetadata = module.Provider?.GetMetadataReader();
+ var reparsed = resolvedMetadata is not null
+ ? ReadyToRunSignatureWalker.ParseMethod(
+ reader, entryOffset, resolvedMetadata, resolveMetadata)
+ : sig;
+ var crossToken = (reparsed.MethodToken & 0xFF00_0000) == 0x0600_0000 ? reparsed.MethodToken : 0;
+ var source = module.Provider is { } provider
+ ? new MethodMapSource(module.AssemblyName, module.Mvid, 0, provider.MethodDefs, resolvedMetadata)
+ : (MethodMapSource?)null;
+ pending.Add(new PendingEntry(
+ module.AssemblyName, module.Mvid, source, crossToken,
+ entryId, IsGeneric: true, reparsed.InstantiationDisplay));
+ continue;
+ }
}
+ if (!ShouldMaterialize(targetMvid, instance.Mvid))
+ continue;
+
// Same-module instantiation — its token is a MethodDef in the instance table's own module;
// give it that module's source so its declaring type and name resolve (a cross-module token
// we could not resolve stays unnamed).
@@ -154,6 +181,9 @@ private static void MarkInstanceMethodEntryPoints(
}
}
+ private static bool ShouldMaterialize(Guid? targetMvid, Guid entryMvid) =>
+ targetMvid is null || targetMvid.Value == entryMvid;
+
private static int DecodeRuntimeFunctionIndex(R2RNativeReader reader, int elementOffset)
{
var offset = reader.DecodeUnsigned(elementOffset, out var id);
diff --git a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunModuleContext.cs b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunModuleContext.cs
index ad7e83f9..5538f6c6 100644
--- a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunModuleContext.cs
+++ b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunModuleContext.cs
@@ -1,4 +1,5 @@
using Dotsider.Core.Analysis.Models;
+using System.Reflection.Metadata;
namespace Dotsider.Core.Analysis.ReadyToRun;
@@ -57,6 +58,10 @@ private ReadyToRunModuleContext(
return new ModuleRef(component.AssemblyName, component.Mvid, _providerFor(component.Mvid));
}
+ /// Resolves an override index directly to component metadata, or null when unavailable.
+ public MetadataReader? ResolveMetadata(int moduleIndex) =>
+ Resolve(moduleIndex)?.Provider?.GetMetadataReader();
+
// Component assembly indices start at two from R2R major 6.3 (readytorun.h version history); the
// manifest reserves index 1 for itself, so a component is index (position + offset).
private static int IndexOffset(ReadyToRunInfo info) =>
diff --git a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunSignatureWalker.cs b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunSignatureWalker.cs
index 7404b639..f13415f2 100644
--- a/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunSignatureWalker.cs
+++ b/src/Dotsider.Core/Analysis/ReadyToRun/ReadyToRunSignatureWalker.cs
@@ -21,6 +21,8 @@ internal static class ReadyToRunSignatureWalker
private const uint SigConstrained = 0x20;
private const uint SigOwnerType = 0x40;
private const uint SigUpdateContext = 0x80;
+ private const uint MaxSignatureItemCount = 1024;
+ private const uint MaxArrayRank = 256;
/// The result of walking one method signature.
/// The file offset immediately after the signature (where the runtime-function index begins).
@@ -35,18 +37,30 @@ public readonly record struct MethodSignature(
/// The image reader.
/// The file offset of the signature.
/// The metadata reader for resolving token names, or null.
- public static MethodSignature ParseMethod(R2RNativeReader reader, int offset, MetadataReader? metadata)
+ /// Resolves a ReadyToRun module override index to metadata, or null when unavailable.
+ public static MethodSignature ParseMethod(
+ R2RNativeReader reader,
+ int offset,
+ MetadataReader? metadata,
+ Func? moduleMetadata = null)
{
- var walker = new Walker(reader, offset, metadata);
+ var walker = new Walker(reader, offset, metadata, moduleMetadata);
walker.ParseMethod();
return new MethodSignature(
walker.Offset, walker.MethodToken, walker.RenderInstantiation(), walker.CrossModule, walker.ModuleIndex);
}
- private sealed class Walker(R2RNativeReader reader, int offset, MetadataReader? metadata)
+ private sealed class Walker(
+ R2RNativeReader reader,
+ int offset,
+ MetadataReader? metadata,
+ Func? moduleMetadata)
{
private readonly List _instantiation = [];
+ private readonly MetadataReader? _outerMetadata = metadata;
+ private readonly int _startOffset = offset;
private int _offset = offset;
+ private MetadataReader? _metadata = metadata;
private string? _ownerDisplay;
private int _topLevelModuleIndex = -1;
@@ -70,13 +84,27 @@ private sealed class Walker(R2RNativeReader reader, int offset, MetadataReader?
public void ParseMethod()
{
var flags = ReadUInt();
+ ReadyToRunDiagnostics.Write($"method-start offset=0x{_startOffset:X} flags=0x{flags:X}");
if ((flags & SigUpdateContext) != 0)
{
_topLevelModuleIndex = (int)ReadUInt(); // the token resolves in the module at this index
CrossModule = true;
flags &= ~SigUpdateContext;
+ ReadyToRunDiagnostics.Write(
+ $"method-update-context start=0x{_startOffset:X} module={_topLevelModuleIndex} next=0x{_offset:X}");
+ WithMetadata(moduleMetadata?.Invoke(_topLevelModuleIndex), () => ParseMethodBody(flags));
+ ReadyToRunDiagnostics.Write(
+ $"method-end start=0x{_startOffset:X} end=0x{_offset:X} token=0x{MethodToken:X8} module={ModuleIndex}");
+ return;
}
+ ParseMethodBody(flags);
+ ReadyToRunDiagnostics.Write(
+ $"method-end start=0x{_startOffset:X} end=0x{_offset:X} token=0x{MethodToken:X8} module={ModuleIndex}");
+ }
+
+ private void ParseMethodBody(uint flags)
+ {
if ((flags & SigOwnerType) != 0)
{
_ownerDisplay = SkipType();
@@ -101,6 +129,7 @@ public void ParseMethod()
if ((flags & SigMethodInstantiation) != 0)
{
var argCount = ReadUInt();
+ EnsureBounded(argCount, MaxSignatureItemCount, "method generic argument count");
for (var i = 0; i < argCount; i++)
_instantiation.Add(SkipType());
flags &= ~SigMethodInstantiation;
@@ -117,6 +146,8 @@ public void ParseMethod()
private string SkipType()
{
var elementType = reader.ReadByte(ref _offset) & 0x7F;
+ ReadyToRunDiagnostics.Write(
+ $"type start=0x{_startOffset:X} offset=0x{_offset - 1:X} element=0x{elementType:X2}");
switch (elementType)
{
case 0x01: return "void";
@@ -151,7 +182,9 @@ private string SkipType()
// one seen is the owner's; capture it so the method token attributes correctly.
var moduleIndex = (int)ReadUInt();
if (_ownerModuleIndex < 0) { _ownerModuleIndex = moduleIndex; CrossModule = true; }
- return SkipType();
+ ReadyToRunDiagnostics.Write(
+ $"type-module start=0x{_startOffset:X} module={moduleIndex} next=0x{_offset:X}");
+ return WithMetadata(moduleMetadata?.Invoke(moduleIndex), SkipType);
}
case 0x3b: ReadUInt(); return "var"; // VAR_ZAPSIG
case 0x3d: return SkipType(); // NATIVE_VALUETYPE_ZAPSIG
@@ -162,10 +195,13 @@ private string SkipType()
{
var element = SkipType();
var rank = ReadUInt();
+ EnsureBounded(rank, MaxArrayRank, "array rank");
if (rank == 0) return element + "[]";
var sizes = ReadUInt();
+ EnsureBounded(sizes, MaxArrayRank, "array size count");
for (var i = 0; i < sizes; i++) ReadUInt();
var lowerBounds = ReadUInt();
+ EnsureBounded(lowerBounds, MaxArrayRank, "array lower-bound count");
for (var i = 0; i < lowerBounds; i++) ReadInt();
return $"{element}[{new string(',', (int)rank - 1)}]";
}
@@ -173,8 +209,14 @@ private string SkipType()
{
var generic = SkipType();
var argCount = ReadUInt();
+ EnsureBounded(argCount, MaxSignatureItemCount, "type generic argument count");
+ ReadyToRunDiagnostics.Write(
+ $"type-genericinst start=0x{_startOffset:X} args={argCount} next=0x{_offset:X}");
var args = new string[argCount];
- for (var i = 0; i < argCount; i++) args[i] = SkipType();
+ WithMetadata(_outerMetadata, () =>
+ {
+ for (var i = 0; i < argCount; i++) args[i] = SkipType();
+ });
return $"{generic}<{string.Join(", ", args)}>";
}
case 0x1b: // FNPTR
@@ -182,6 +224,7 @@ private string SkipType()
var header = reader.ReadByte(ref _offset);
if ((header & 0x10) != 0) ReadUInt(); // generic param count
var paramCount = ReadUInt();
+ EnsureBounded(paramCount, MaxSignatureItemCount, "function pointer parameter count");
SkipType(); // return
for (var i = 0; i < paramCount; i++)
{
@@ -198,25 +241,37 @@ private string SkipType()
private string TypeTokenName(int token)
{
- if (metadata is null) return "Type";
+ if (_metadata is null) return "Type";
try
{
var handle = MetadataTokens.EntityHandle(token);
+ var row = MetadataTokens.GetRowNumber(handle);
+ ReadyToRunDiagnostics.Write(
+ $"type-token offset=0x{_offset:X} token=0x{token:X8} kind={handle.Kind} row={row}");
return handle.Kind switch
{
- HandleKind.TypeDefinition => metadata.GetString(
- metadata.GetTypeDefinition((TypeDefinitionHandle)handle).Name),
- HandleKind.TypeReference => metadata.GetString(
- metadata.GetTypeReference((TypeReferenceHandle)handle).Name),
+ HandleKind.TypeDefinition when IsValidRow(row, _metadata.TypeDefinitions.Count) => _metadata.GetString(
+ _metadata.GetTypeDefinition((TypeDefinitionHandle)handle).Name),
+ HandleKind.TypeReference when IsValidRow(row, _metadata.TypeReferences.Count) => _metadata.GetString(
+ _metadata.GetTypeReference((TypeReferenceHandle)handle).Name),
_ => "Type",
};
}
- catch (Exception ex) when (ex is BadImageFormatException or ArgumentException)
+ catch (Exception ex) when (ex is BadImageFormatException or ArgumentException or InvalidOperationException)
{
return "Type";
}
}
+ private static void EnsureBounded(uint value, uint max, string name)
+ {
+ if (value > max)
+ throw new BadImageFormatException($"ReadyToRun signature {name} {value} exceeds supported maximum {max}.");
+ }
+
+ private static bool IsValidRow(int row, int count) =>
+ row > 0 && row <= count;
+
// Reads a signature token: an ECMA compressed value whose low 2 bits pick the table.
private int ReadToken()
{
@@ -234,5 +289,33 @@ private int ReadToken()
private uint ReadUInt() => reader.ReadCompressedUInt(ref _offset);
private int ReadInt() => reader.ReadCompressedInt(ref _offset);
+
+ private void WithMetadata(MetadataReader? next, Action action)
+ {
+ var previous = _metadata;
+ _metadata = next;
+ try
+ {
+ action();
+ }
+ finally
+ {
+ _metadata = previous;
+ }
+ }
+
+ private string WithMetadata(MetadataReader? next, Func action)
+ {
+ var previous = _metadata;
+ _metadata = next;
+ try
+ {
+ return action();
+ }
+ finally
+ {
+ _metadata = previous;
+ }
+ }
}
}
diff --git a/src/Dotsider.Core/Analysis/RuntimeTracer.cs b/src/Dotsider.Core/Analysis/RuntimeTracer.cs
index 7c4738ce..44db5065 100644
--- a/src/Dotsider.Core/Analysis/RuntimeTracer.cs
+++ b/src/Dotsider.Core/Analysis/RuntimeTracer.cs
@@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Globalization;
+using System.Runtime.InteropServices;
using TraceEventCategory = Dotsider.Core.Analysis.Models.TraceEventCategory;
using TraceEventEntry = Dotsider.Core.Analysis.Models.TraceEventEntry;
@@ -19,18 +20,20 @@ public sealed class RuntimeTracer(string assemblyPath, string arguments, Action
{
private const int MaxEvents = 10_000;
private const int MaxOutputLines = 5_000;
- private const int MaxConnectRetries = 25; // 25 × 200ms = 5s max wait
- private const int ConnectRetryDelayMs = 200;
+ private static readonly TimeSpan ConnectTimeout = TimeSpan.FromSeconds(15);
+ private static readonly TimeSpan ConnectRetryDelay = TimeSpan.FromMilliseconds(200);
private readonly bool _isExe = assemblyPath.EndsWith(".exe", StringComparison.OrdinalIgnoreCase);
private Process? _process;
+ private DiagnosticsClientConnector? _connector;
private EventPipeSession? _session;
private EventPipeEventSource? _eventSource;
private Task? _processingTask;
private CancellationTokenSource? _cts;
private Stopwatch? _stopwatch;
private Timer? _invalidateTimer;
+ private string? _diagnosticPort;
// Ring buffer for events — lock protects both read and write
private readonly TraceEventEntry[] _eventRing = new TraceEventEntry[MaxEvents];
@@ -54,6 +57,7 @@ public sealed class RuntimeTracer(string assemblyPath, string arguments, Action
// Dirty flag for throttled invalidation
private int _dirty;
+ private int _invalidateTimerCallbackActive;
// Process output
private readonly ConcurrentQueue _outputQueue = new();
@@ -149,6 +153,10 @@ public void Start()
{
_cts = new CancellationTokenSource();
_stopwatch = Stopwatch.StartNew();
+ _diagnosticPort = CreateDiagnosticPort();
+ var connectCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ connectCts.CancelAfter(ConnectTimeout);
+ var connectorTask = DiagnosticsClientConnector.FromDiagnosticPort(_diagnosticPort, connectCts.Token);
// Launch with diagnostic port suspend so we can attach EventPipe
// before Main() runs — this captures events even for short-lived processes
@@ -160,7 +168,13 @@ public void Start()
RedirectStandardOutput = true,
RedirectStandardError = true,
};
- psi.Environment["DOTNET_DefaultDiagnosticPortSuspend"] = "1";
+ psi.Environment["DOTNET_DiagnosticPorts"] = _diagnosticPort;
+ psi.Environment.Remove("DOTNET_DefaultDiagnosticPortSuspend");
+ if (!psi.Environment.ContainsKey("ASPNETCORE_URLS")
+ && !psi.Environment.ContainsKey("DOTNET_URLS"))
+ {
+ psi.Environment["ASPNETCORE_URLS"] = "http://127.0.0.1:0";
+ }
_process = Process.Start(psi);
if (_process is null)
@@ -171,40 +185,29 @@ public void Start()
_processState = TraceProcessState.Error;
}
+ connectCts.Cancel();
+ connectCts.Dispose();
MarkDirty();
return;
}
// Capture stdout/stderr on background threads
var startTime = _stopwatch;
- Task.Run(() => ReadOutput(_process.StandardOutput, false, startTime));
- Task.Run(() => ReadOutput(_process.StandardError, true, startTime));
+ RunOnDedicatedThread(() => ReadOutput(_process.StandardOutput, false, startTime));
+ RunOnDedicatedThread(() => ReadOutput(_process.StandardError, true, startTime));
// Handle process exit
_process.EnableRaisingEvents = true;
_process.Exited += (_, _) =>
{
- bool transitioned;
+ int? exitCode = TryReadExitCode();
lock (_stateLock)
{
- transitioned = _processState == TraceProcessState.Running;
- if (transitioned)
- {
- _exitCode = _process.HasExited ? _process.ExitCode : null;
- _processState = TraceProcessState.Exited;
- }
+ _exitCode = exitCode;
}
- if (transitioned)
- invalidate();
- if (transitioned)
- {
- _stopwatch?.Stop();
- // StopProcessing unblocks source.Process() synchronously.
- // session.Stop() can deadlock on Windows when the pipe is
- // already broken, so we only use the TraceEventSource path.
- _eventSource?.StopProcessing();
- }
+ _stopwatch?.Stop();
+ invalidate();
};
lock (_stateLock) _processState = TraceProcessState.Starting;
@@ -215,51 +218,41 @@ public void Start()
// Otherwise, only invalidate when the dirty flag is set.
_invalidateTimer = new Timer(_ =>
{
- if (Interlocked.Exchange(ref _dirty, 0) == 1
- || ProcessState == TraceProcessState.Running)
- invalidate();
- }, null, 0, 100);
+ if (Interlocked.Exchange(ref _invalidateTimerCallbackActive, 1) == 1)
+ {
+ return;
+ }
- // Connection + event processing on background task.
- // The process is suspended (DOTNET_DefaultDiagnosticPortSuspend=1)
- // so it won't exit while we're connecting.
- var providers = BuildProviders();
- var pid = _process.Id;
- _processingTask = Task.Run(async () =>
- {
try
{
- // Retry connecting — the diagnostic IPC endpoint may not be
- // available immediately after process start
- DiagnosticsClient? client = null;
- EventPipeSession? session = null;
-
- for (var attempt = 0; attempt < MaxConnectRetries; attempt++)
+ if (Interlocked.Exchange(ref _dirty, 0) == 1
+ || IsProcessRunning())
{
- _cts.Token.ThrowIfCancellationRequested();
-
- try
- {
- client = new DiagnosticsClient(pid);
- session = client.StartEventPipeSession(providers, requestRundown: false);
- break; // connected
- }
- catch (ServerNotAvailableException)
- {
- // Runtime diagnostic pipe not ready yet — wait and retry
- await Task.Delay(ConnectRetryDelayMs, _cts.Token);
- }
- catch (EndOfStreamException)
- {
- await Task.Delay(ConnectRetryDelayMs, _cts.Token);
- }
+ invalidate();
}
+ }
+ finally
+ {
+ Volatile.Write(ref _invalidateTimerCallbackActive, 0);
+ }
+ }, null, 0, 100);
+ // Connection + event processing on background task. The process is
+ // suspended by DOTNET_DiagnosticPorts until we attach EventPipe and
+ // send ResumeRuntime.
+ var providers = BuildProviders();
+ _processingTask = Task.Factory.StartNew(async () =>
+ {
+ try
+ {
+ _connector = await connectorTask.ConfigureAwait(false);
+ var client = _connector.Instance;
+ var session = await TryStartEventPipeSessionAsync(client, providers);
if (session is null)
{
lock (_stateLock)
{
- _errorMessage = "Timed out connecting to runtime diagnostics (5s). Is this a valid .NET assembly?";
+ _errorMessage = $"Timed out starting runtime diagnostics after {ConnectTimeout.TotalSeconds.ToString("0", CultureInfo.InvariantCulture)}s. Is this a valid .NET assembly?";
_processState = TraceProcessState.Error;
}
@@ -273,23 +266,27 @@ public void Start()
MarkDirty();
// Resume the suspended runtime now that EventPipe is attached
- client!.ResumeRuntime();
+ client.ResumeRuntime();
// Process events — blocks until session ends
ProcessEventsLoop(session);
+ MarkExitedIfStarted();
}
- catch (OperationCanceledException) { /* user cancelled */ }
- catch (Exception ex) when (ex is EndOfStreamException or IOException or ObjectDisposedException)
+ catch (OperationCanceledException) when (_cts.IsCancellationRequested) { /* user cancelled */ }
+ catch (OperationCanceledException)
{
- // Expected: process exited (pipe broke) or user cancelled
lock (_stateLock)
{
- if (_processState is TraceProcessState.Running or TraceProcessState.Starting)
- {
- _exitCode = _process?.HasExited == true ? _process.ExitCode : null;
- _processState = TraceProcessState.Exited;
- }
+ _errorMessage = $"Timed out connecting to runtime diagnostics after {ConnectTimeout.TotalSeconds.ToString("0", CultureInfo.InvariantCulture)}s. Is this a valid .NET assembly?";
+ _processState = TraceProcessState.Error;
}
+
+ try { _process.Kill(); } catch { }
+ }
+ catch (Exception ex) when (ex is EndOfStreamException or IOException or ObjectDisposedException)
+ {
+ // Expected: process exited (pipe broke) or user cancelled
+ MarkExitedIfStarted();
}
catch (Exception ex)
{
@@ -301,10 +298,13 @@ public void Start()
}
finally
{
+ connectCts.Dispose();
+ DisposeDiagnosticConnector();
+ CleanupDiagnosticPort();
_stopwatch?.Stop();
invalidate();
}
- });
+ }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
}
/// Stops the traced process and event collection.
@@ -325,15 +325,18 @@ public void Stop()
_invalidateTimer?.Dispose();
_invalidateTimer = null;
+ DisposeDiagnosticConnector();
+ CleanupDiagnosticPort();
_stopwatch?.Stop();
+ int? exitCode = TryReadExitCode();
bool stopped;
lock (_stateLock)
{
stopped = _processState is TraceProcessState.Running or TraceProcessState.Starting;
if (stopped)
{
- _exitCode = _process?.HasExited == true ? _process.ExitCode : null;
+ _exitCode = exitCode;
_processState = TraceProcessState.Exited;
}
}
@@ -353,6 +356,122 @@ public void Dispose()
private void MarkDirty() => Volatile.Write(ref _dirty, 1);
+ private static string CreateDiagnosticPort()
+ {
+ var name = $"dotsider-{Environment.ProcessId}-{Guid.NewGuid():N}";
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? name
+ : Path.Combine("/tmp", $"{name}.socket");
+ }
+
+ private void DisposeDiagnosticConnector()
+ {
+ var connector = Interlocked.Exchange(ref _connector, null);
+ if (connector is null)
+ return;
+
+ try { connector.DisposeAsync().AsTask().GetAwaiter().GetResult(); } catch { }
+ }
+
+ private void CleanupDiagnosticPort()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ var diagnosticPort = _diagnosticPort;
+ if (string.IsNullOrEmpty(diagnosticPort))
+ return;
+
+ try { File.Delete(diagnosticPort); } catch { }
+ }
+
+ private void MarkExitedIfStarted()
+ {
+ int? exitCode = TryReadExitCode();
+ lock (_stateLock)
+ {
+ if (_processState is TraceProcessState.Running or TraceProcessState.Starting)
+ {
+ _exitCode = exitCode;
+ _processState = TraceProcessState.Exited;
+ }
+ }
+ }
+
+ private bool IsProcessRunning()
+ {
+ lock (_stateLock)
+ {
+ return _processState == TraceProcessState.Running;
+ }
+ }
+
+ private int? TryReadExitCode()
+ {
+ var process = _process;
+ if (process is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return process.HasExited ? process.ExitCode : null;
+ }
+ catch (InvalidOperationException)
+ {
+ return null;
+ }
+ }
+
+ private async Task TryStartEventPipeSessionAsync(
+ DiagnosticsClient client,
+ IReadOnlyCollection providers)
+ {
+ var connectTimer = Stopwatch.StartNew();
+
+ while (connectTimer.Elapsed < ConnectTimeout)
+ {
+ _cts!.Token.ThrowIfCancellationRequested();
+
+ try
+ {
+ using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token);
+ connectCts.CancelAfter(ConnectTimeout - connectTimer.Elapsed);
+ return await client.StartEventPipeSessionAsync(
+ providers,
+ requestRundown: false,
+ circularBufferMB: 256,
+ token: connectCts.Token);
+ }
+ catch (OperationCanceledException) when (!_cts.IsCancellationRequested)
+ {
+ return null;
+ }
+ catch (Exception ex) when (IsTransientDiagnosticsConnectException(ex))
+ {
+ var remaining = ConnectTimeout - connectTimer.Elapsed;
+ if (remaining <= TimeSpan.Zero)
+ return null;
+
+ var delay = remaining < ConnectRetryDelay ? remaining : ConnectRetryDelay;
+ await Task.Delay(delay, _cts.Token);
+ }
+ }
+
+ return null;
+ }
+
+ private static bool IsTransientDiagnosticsConnectException(Exception ex) =>
+ ex is ServerNotAvailableException or EndOfStreamException;
+
+ private static void RunOnDedicatedThread(Action action) =>
+ Task.Factory.StartNew(
+ action,
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
+
private static List BuildProviders() =>
[
// Verbose level is required for MethodJittingStarted events (which
diff --git a/src/Dotsider.Core/Dotsider.Core.csproj b/src/Dotsider.Core/Dotsider.Core.csproj
index 24056e36..b0109c4b 100644
--- a/src/Dotsider.Core/Dotsider.Core.csproj
+++ b/src/Dotsider.Core/Dotsider.Core.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/src/Dotsider.Mcp/Dotsider.Mcp.csproj b/src/Dotsider.Mcp/Dotsider.Mcp.csproj
index d41a283e..39b54153 100644
--- a/src/Dotsider.Mcp/Dotsider.Mcp.csproj
+++ b/src/Dotsider.Mcp/Dotsider.Mcp.csproj
@@ -37,8 +37,8 @@
-
-
+
+
diff --git a/src/Dotsider/Dotsider.csproj b/src/Dotsider/Dotsider.csproj
index 22ecf33b..d75e2c2f 100644
--- a/src/Dotsider/Dotsider.csproj
+++ b/src/Dotsider/Dotsider.csproj
@@ -44,7 +44,7 @@
-
+
diff --git a/src/Dotsider/DotsiderApp.cs b/src/Dotsider/DotsiderApp.cs
index a661c223..dc6e7e63 100644
--- a/src/Dotsider/DotsiderApp.cs
+++ b/src/Dotsider/DotsiderApp.cs
@@ -103,6 +103,12 @@ .. _state.Analyzer.PreIlcCompanions is { } companions
])
.OnSelectionChanged(e =>
{
+ if (IsDetailPopupOpen())
+ {
+ _state.App.Invalidate();
+ return;
+ }
+
SelectTab(e.SelectedIndex);
SeedFocusedRowIfNeeded();
RequestContentFocus();
@@ -137,38 +143,42 @@ Action VimReset(Action act
// Number keys 1-8, s, q suppressed during search editing, jump dialog,
// or hex insert mode to let EditorNode/TextBox receive character input
var hexInsertMode = _state.CurrentTab == TabId.HexDump && _state.HexMode == HexEditMode.Insert;
+ var detailPopupOpen = IsDetailPopupOpen();
if (!isSearchEditing && !_state.HexJumpDialogOpen && !hexInsertMode
&& !_state.ModalDialogOpen)
{
- for (var i = 0; i < 8; i++)
+ if (!detailPopupOpen)
{
- var tabIndex = i;
- var key = (Hex1bKey)((int)Hex1bKey.D1 + i);
- bindings.Key(key).Global().Action(VimReset(_ =>
+ for (var i = 0; i < 8; i++)
{
- SelectTab(tabIndex);
- SeedFocusedRowIfNeeded();
- RequestContentFocus();
- _state.App.Invalidate();
- }), $"Tab {tabIndex + 1}");
- }
+ var tabIndex = i;
+ var key = (Hex1bKey)((int)Hex1bKey.D1 + i);
+ bindings.Key(key).Global().Action(VimReset(_ =>
+ {
+ SelectTab(tabIndex);
+ SeedFocusedRowIfNeeded();
+ RequestContentFocus();
+ _state.App.Invalidate();
+ }), $"Tab {tabIndex + 1}");
+ }
- // Suppress size toggle on Dynamic Events sub-tab (S = Socket filter)
- // and hex tab in insert mode (S = byte input)
- var suppressSizeToggle = (_state.CurrentTab == TabId.Dynamic && _state.DynamicSubTab == DynamicSubTabId.Events)
- || (_state.CurrentTab == TabId.HexDump && _state.HexMode == HexEditMode.Insert);
- if (!suppressSizeToggle)
- {
- bindings.Key(Hex1bKey.S).Global().Action(VimReset(_ =>
+ // Suppress size toggle on Dynamic Events sub-tab (S = Socket filter)
+ // and hex tab in insert mode (S = byte input)
+ var suppressSizeToggle = (_state.CurrentTab == TabId.Dynamic && _state.DynamicSubTab == DynamicSubTabId.Events)
+ || (_state.CurrentTab == TabId.HexDump && _state.HexMode == HexEditMode.Insert);
+ if (!suppressSizeToggle)
{
- _state.HumanReadableSizes = !_state.HumanReadableSizes;
- _state.App.Invalidate();
- }), "Toggle size format");
- }
+ bindings.Key(Hex1bKey.S).Global().Action(VimReset(_ =>
+ {
+ _state.HumanReadableSizes = !_state.HumanReadableSizes;
+ _state.App.Invalidate();
+ }), "Toggle size format");
+ }
- // Suppress Q quit in hex insert mode — let editor receive it as byte input
- if (!(_state.CurrentTab == TabId.HexDump && _state.HexMode == HexEditMode.Insert))
- bindings.Key(Hex1bKey.Q).Global().Action(VimReset(ctx => ctx.RequestStop()), "Quit");
+ // Suppress Q quit in hex insert mode — let editor receive it as byte input
+ if (!(_state.CurrentTab == TabId.HexDump && _state.HexMode == HexEditMode.Insert))
+ bindings.Key(Hex1bKey.Q).Global().Action(VimReset(ctx => ctx.RequestStop()), "Quit");
+ }
// Universal yank — works on all tabs with neovim-style behavior
bindings.Key(Hex1bKey.Y).Global().Action(ctx =>
@@ -250,7 +260,6 @@ void SearchToggle()
_state.App.RequestFocus(node => node is TextBoxNode);
_state.App.Invalidate();
}
- var detailPopupOpen = _state.PeDetailContent is not null || _state.StringsDetailContent is not null;
if (!_state.HexJumpDialogOpen && !detailPopupOpen && !_state.ModalDialogOpen)
{
bindings.Key(Hex1bKey.OemQuestion).Global().OverridesCapture().Action(VimReset(_ => SearchToggle()), "Search");
@@ -306,6 +315,19 @@ void SearchToggle()
}), "Attach pre-ILC sidecars");
}
+ if (_state.CurrentTab == TabId.General
+ && !isSearchEditing
+ && !_state.ApphostDialogOpen
+ && !_state.PreIlcDialogOpen
+ && !_state.ModalDialogOpen)
+ {
+ bindings.Key(Hex1bKey.Enter).Global().OverridesCapture().Action(VimReset(_ =>
+ {
+ if (_state.App.FocusedNode is EditorNode) return;
+ TryDrillFocusedGeneralReference();
+ }), "Drill into reference");
+ }
+
// Hex + IL Inspector keybindings (shared with NuGetApp).
// Suppressed while a companion dialog is open to prevent background shortcuts.
if (!isSearchEditing && !_state.ModalDialogOpen)
@@ -546,12 +568,44 @@ private void SelectTab(int tabIndex)
_state.NavigateToTab(tabIndex);
}
+ private bool IsDetailPopupOpen() =>
+ _state.PeDetailContent is not null || _state.StringsDetailContent is not null;
+
///
/// Requests focus on the appropriate content node for the current tab.
/// IL tab targets the ListNode tree; all other tabs target any content node including TableNode.
///
private void RequestContentFocus() => _state.RequestContentFocus();
+ private bool TryDrillFocusedGeneralReference()
+ {
+ var refs = _state.MetadataAnalyzer.AssemblyRefs;
+ var focusedName = _state.GeneralFocusedDep as string
+ ?? (refs.Count > 0 ? refs[0].Name : null);
+ if (focusedName is null) return false;
+
+ var asmRef = refs.FirstOrDefault(
+ r => string.Equals(r.Name, focusedName, StringComparison.OrdinalIgnoreCase));
+ if (asmRef is null) return false;
+
+ var resolution = AssemblyAnalyzer.ResolveAssemblyByIdentity(
+ _state.MetadataAnalyzer.FilePath,
+ asmRef,
+ _state.MetadataAnalyzer.TargetFramework,
+ _state.MetadataAnalyzer.PreferredRuntimePack,
+ _state.MetadataAnalyzer.SourceBundlePath,
+ _state.RootNetFxBindingContext);
+ if (resolution.Resolved is null) return false;
+
+ if (!_state.PushAssembly(resolution.Resolved)) return false;
+
+ SeedFocusedRowIfNeeded();
+ RequestContentFocus();
+ _state.App.Invalidate();
+ _state.RequestExtraFrame();
+ return true;
+ }
+
///
/// Seeds the focused row key for table-backed tabs so Enter works immediately
/// without requiring DownArrow first.
diff --git a/src/Dotsider/DotsiderState.cs b/src/Dotsider/DotsiderState.cs
index 2087cc7c..973f5e23 100644
--- a/src/Dotsider/DotsiderState.cs
+++ b/src/Dotsider/DotsiderState.cs
@@ -970,7 +970,7 @@ public void EnsureManagedNativeIndexAsync()
if (capturedAnalyzer.PreIlcCompanions is null) return;
PreIlcIndexBuildInProgress = true;
- _ = Task.Run(() =>
+ _ = QueueDedicatedBackgroundWork(() =>
{
try
{
@@ -993,6 +993,7 @@ public void EnsureManagedNativeIndexAsync()
{
PreIlcIndexBuildInProgress = false;
App.Invalidate();
+ RequestExtraFrame();
}
});
}
@@ -1216,8 +1217,10 @@ public void NavigateToIlMethod(MethodDefInfo method)
NavigateToTab(TabId.IlInspector);
var ilSearch = Search[TabId.IlInspector];
ilSearch.Reset();
+ IlFocusedPane = IlPane.Tree;
App.RequestFocus(node => node is ScrollPanelNode);
App.Invalidate();
+ RequestExtraFrame();
}
///
@@ -1245,6 +1248,7 @@ public void NavigateToHexOffset(int rva)
NavigateToTab(TabId.HexDump);
App.RequestFocus(node => node is EditorNode);
App.Invalidate();
+ RequestExtraFrame();
}
/// Navigates to the Hex Dump tab, jumping directly to a file offset (native mode).
@@ -1268,6 +1272,7 @@ public void NavigateToHexFileOffset(long fileOffset)
NavigateToTab(TabId.HexDump);
App.RequestFocus(node => node is EditorNode);
App.Invalidate();
+ RequestExtraFrame();
}
///
@@ -1943,6 +1948,7 @@ public void NavigateBack()
// single source of truth — IL → ScrollPanelNode, Hex → EditorNode, etc.
RequestContentFocus();
App.Invalidate();
+ RequestExtraFrame();
}
///
@@ -2322,7 +2328,7 @@ public void EnsureCachedGraphAsync()
GraphBuildInProgress = true;
var capturedAnalyzer = Analyzer;
- _ = Task.Run(() =>
+ _ = QueueDedicatedBackgroundWork(() =>
{
try
{
@@ -2338,7 +2344,15 @@ public void EnsureCachedGraphAsync()
{
GraphBuildInProgress = false;
App.Invalidate();
+ RequestExtraFrame();
}
});
}
+
+ private static Task QueueDedicatedBackgroundWork(Action work) =>
+ Task.Factory.StartNew(
+ work,
+ CancellationToken.None,
+ TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning,
+ TaskScheduler.Default);
}
diff --git a/src/Dotsider/Infrastructure/CursorColorHelper.cs b/src/Dotsider/Infrastructure/CursorColorHelper.cs
index 36eb461a..300dc973 100644
--- a/src/Dotsider/Infrastructure/CursorColorHelper.cs
+++ b/src/Dotsider/Infrastructure/CursorColorHelper.cs
@@ -14,10 +14,30 @@ public static class CursorColorHelper
///
/// Writes the OSC 12 sequence to set the cursor color to the dotsider theme teal.
///
- public static void SetThemeCursorColor() => Console.Write(SetTealSequence);
+ public static void SetThemeCursorColor() => SetThemeCursorColor(Console.Out);
+
+ ///
+ /// Writes the OSC 12 sequence to set the cursor color to the dotsider theme teal.
+ ///
+ /// The writer that receives the escape sequence.
+ public static void SetThemeCursorColor(TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.Write(SetTealSequence);
+ }
+
+ ///
+ /// Writes the OSC 112 sequence to reset the cursor color to the terminal default.
+ ///
+ public static void ResetCursorColor() => ResetCursorColor(Console.Out);
///
/// Writes the OSC 112 sequence to reset the cursor color to the terminal default.
///
- public static void ResetCursorColor() => Console.Write(ResetSequence);
+ /// The writer that receives the escape sequence.
+ public static void ResetCursorColor(TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+ writer.Write(ResetSequence);
+ }
}
diff --git a/src/Dotsider/NuGetApp.cs b/src/Dotsider/NuGetApp.cs
index 96937d60..2d8da20c 100644
--- a/src/Dotsider/NuGetApp.cs
+++ b/src/Dotsider/NuGetApp.cs
@@ -286,7 +286,7 @@ void SearchToggle()
if (!_state.IsBrowsingPackage && _state.SelectedDllState is not null)
{
- if (!isSearchEditing)
+ if (!isSearchEditing && !IsDetailPopupOpen(_state.SelectedDllState))
{
for (var i = 0; i < 5; i++)
{
@@ -525,10 +525,19 @@ private Hex1bWidget BuildDllInspector(WidgetContext outer)
])
.OnSelectionChanged(e =>
{
+ if (IsDetailPopupOpen(dllState))
+ {
+ _state.App.Invalidate();
+ return;
+ }
+
dllState.CurrentTab = e.SelectedIndex;
_state.App.Invalidate();
})
.Full()
.Fill();
}
+
+ private static bool IsDetailPopupOpen(DotsiderState state) =>
+ state.PeDetailContent is not null || state.StringsDetailContent is not null;
}
diff --git a/src/Dotsider/Views/DiffMethodsView.cs b/src/Dotsider/Views/DiffMethodsView.cs
index 4e946714..16a5a483 100644
--- a/src/Dotsider/Views/DiffMethodsView.cs
+++ b/src/Dotsider/Views/DiffMethodsView.cs
@@ -45,23 +45,8 @@ public static Hex1bWidget Build(WidgetContext ctx, DiffState state
// Set up match navigation — cycle through filtered rows
if (filtered.Count > 0 && !string.IsNullOrEmpty(query))
{
- var keys = filtered.Select(e =>
- {
- var m = e.Right ?? e.Left!;
- return e.Kind.ToString() + ":" + m.DeclaringType + "::" + m.Name + m.Signature;
- }).ToList();
- state.NavigateNextMatch = () =>
- {
- var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
- idx = (idx + 1) % keys.Count;
- state.DiffFocusedKey = keys[idx];
- };
- state.NavigatePrevMatch = () =>
- {
- var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
- idx = idx <= 0 ? keys.Count - 1 : idx - 1;
- state.DiffFocusedKey = keys[idx];
- };
+ state.NavigateNextMatch = () => NavigateMatch(state, forward: true);
+ state.NavigatePrevMatch = () => NavigateMatch(state, forward: false);
}
else
{
@@ -78,8 +63,7 @@ public static Hex1bWidget Build(WidgetContext ctx, DiffState state
// Table
widgets.Add(outer.Table(filtered)
- .RowKey(r => r.Kind.ToString() + ":" + (r.Left?.DeclaringType ?? r.Right?.DeclaringType ?? "") + "::" +
- (r.Left?.Name ?? r.Right?.Name ?? "") + (r.Left?.Signature ?? r.Right?.Signature ?? ""))
+ .RowKey(KeyFor)
.Header(h =>
[
h.Cell("").Width(SizeHint.Fixed(3)),
@@ -144,6 +128,42 @@ private static IReadOnlyList> FilterEntries(
_ => entries
};
+ private static List GetMatchingKeys(DiffState state)
+ {
+ var search = state.Search[2];
+ var query = search.Query;
+ if (string.IsNullOrEmpty(query)) return [];
+
+ return [.. FilterEntries(state.DiffResult.MethodDiffs, state.FilterMode)
+ .Where(e =>
+ {
+ var method = e.Right ?? e.Left!;
+ return method.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ method.Signature.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ method.DeclaringType.Contains(query, StringComparison.OrdinalIgnoreCase);
+ })
+ .Select(KeyFor)];
+ }
+
+ private static void NavigateMatch(DiffState state, bool forward)
+ {
+ var keys = GetMatchingKeys(state);
+ if (keys.Count == 0) return;
+
+ var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
+ idx = forward
+ ? (idx + 1) % keys.Count
+ : idx <= 0 ? keys.Count - 1 : idx - 1;
+ state.DiffFocusedKey = keys[idx];
+ }
+
+ private static string KeyFor(DiffEntry entry) =>
+ entry.Kind + ":"
+ + (entry.Left?.DeclaringType ?? entry.Right?.DeclaringType ?? "")
+ + "::"
+ + (entry.Left?.Name ?? entry.Right?.Name ?? "")
+ + (entry.Left?.Signature ?? entry.Right?.Signature ?? "");
+
private static (string Prefix, Hex1bColor Color) GetDiffStyle(DiffKind kind) => kind switch
{
DiffKind.Added => ("+", Green),
diff --git a/src/Dotsider/Views/DiffRefsView.cs b/src/Dotsider/Views/DiffRefsView.cs
index bc3eb7ce..6c0c0020 100644
--- a/src/Dotsider/Views/DiffRefsView.cs
+++ b/src/Dotsider/Views/DiffRefsView.cs
@@ -46,20 +46,8 @@ public static Hex1bWidget Build(WidgetContext ctx, DiffState state
// Set up match navigation — cycle through filtered rows
if (filtered.Count > 0 && !string.IsNullOrEmpty(query))
{
- var keys = filtered.Select(e =>
- e.Kind.ToString() + ":" + (e.Left?.Name ?? e.Right?.Name ?? "")).ToList();
- state.NavigateNextMatch = () =>
- {
- var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
- idx = (idx + 1) % keys.Count;
- state.DiffFocusedKey = keys[idx];
- };
- state.NavigatePrevMatch = () =>
- {
- var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
- idx = idx <= 0 ? keys.Count - 1 : idx - 1;
- state.DiffFocusedKey = keys[idx];
- };
+ state.NavigateNextMatch = () => NavigateMatch(state, forward: true);
+ state.NavigatePrevMatch = () => NavigateMatch(state, forward: false);
}
else
{
@@ -76,7 +64,7 @@ public static Hex1bWidget Build(WidgetContext ctx, DiffState state
// Table
widgets.Add(outer.Table(filtered)
- .RowKey(r => r.Kind.ToString() + ":" + (r.Left?.Name ?? r.Right?.Name ?? ""))
+ .RowKey(KeyFor)
.Header(h =>
[
h.Cell("").Width(SizeHint.Fixed(3)),
@@ -141,6 +129,39 @@ private static IReadOnlyList> FilterEntries(
_ => entries
};
+ private static List GetMatchingKeys(DiffState state)
+ {
+ var search = state.Search[3];
+ var query = search.Query;
+ if (string.IsNullOrEmpty(query)) return [];
+
+ return [.. FilterEntries(state.DiffResult.AssemblyRefDiffs, state.FilterMode)
+ .Where(e =>
+ {
+ var name = e.Right?.Name ?? e.Left?.Name ?? "";
+ var leftVer = e.Left?.Version ?? "";
+ var rightVer = e.Right?.Version ?? "";
+ return $"{name} {leftVer} {rightVer}"
+ .Contains(query, StringComparison.OrdinalIgnoreCase);
+ })
+ .Select(KeyFor)];
+ }
+
+ private static void NavigateMatch(DiffState state, bool forward)
+ {
+ var keys = GetMatchingKeys(state);
+ if (keys.Count == 0) return;
+
+ var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
+ idx = forward
+ ? (idx + 1) % keys.Count
+ : idx <= 0 ? keys.Count - 1 : idx - 1;
+ state.DiffFocusedKey = keys[idx];
+ }
+
+ private static string KeyFor(DiffEntry entry) =>
+ entry.Kind + ":" + (entry.Left?.Name ?? entry.Right?.Name ?? "");
+
private static (string Prefix, Hex1bColor Color) GetDiffStyle(DiffKind kind) => kind switch
{
DiffKind.Added => ("+", Green),
diff --git a/src/Dotsider/Views/DiffTypesView.cs b/src/Dotsider/Views/DiffTypesView.cs
index 370bf4a5..6dfb5845 100644
--- a/src/Dotsider/Views/DiffTypesView.cs
+++ b/src/Dotsider/Views/DiffTypesView.cs
@@ -43,20 +43,8 @@ public static Hex1bWidget Build(WidgetContext ctx, DiffState state
// Set up match navigation — cycle through filtered rows
if (filtered.Count > 0 && !string.IsNullOrEmpty(query))
{
- var keys = filtered.Select(e =>
- e.Kind.ToString() + ":" + (e.Left?.FullName ?? e.Right?.FullName ?? "")).ToList();
- state.NavigateNextMatch = () =>
- {
- var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
- idx = (idx + 1) % keys.Count;
- state.DiffFocusedKey = keys[idx];
- };
- state.NavigatePrevMatch = () =>
- {
- var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
- idx = idx <= 0 ? keys.Count - 1 : idx - 1;
- state.DiffFocusedKey = keys[idx];
- };
+ state.NavigateNextMatch = () => NavigateMatch(state, forward: true);
+ state.NavigatePrevMatch = () => NavigateMatch(state, forward: false);
}
else
{
@@ -73,7 +61,7 @@ public static Hex1bWidget Build(WidgetContext ctx, DiffState state
// Table
widgets.Add(outer.Table(filtered)
- .RowKey(r => r.Kind.ToString() + ":" + (r.Left?.FullName ?? r.Right?.FullName ?? ""))
+ .RowKey(KeyFor)
.Header(h =>
[
h.Cell("").Width(SizeHint.Fixed(3)),
@@ -138,6 +126,36 @@ private static IReadOnlyList> FilterEntries(
_ => entries
};
+ private static List GetMatchingKeys(DiffState state)
+ {
+ var search = state.Search[1];
+ var query = search.Query;
+ if (string.IsNullOrEmpty(query)) return [];
+
+ return [.. FilterEntries(state.DiffResult.TypeDiffs, state.FilterMode)
+ .Where(e =>
+ {
+ var type = e.Right ?? e.Left!;
+ return type.FullName.Contains(query, StringComparison.OrdinalIgnoreCase);
+ })
+ .Select(KeyFor)];
+ }
+
+ private static void NavigateMatch(DiffState state, bool forward)
+ {
+ var keys = GetMatchingKeys(state);
+ if (keys.Count == 0) return;
+
+ var idx = keys.IndexOf(state.DiffFocusedKey as string ?? "");
+ idx = forward
+ ? (idx + 1) % keys.Count
+ : idx <= 0 ? keys.Count - 1 : idx - 1;
+ state.DiffFocusedKey = keys[idx];
+ }
+
+ private static string KeyFor(DiffEntry entry) =>
+ entry.Kind + ":" + (entry.Left?.FullName ?? entry.Right?.FullName ?? "");
+
private static (string Prefix, Hex1bColor Color) GetDiffStyle(DiffKind kind) => kind switch
{
DiffKind.Added => ("+", Green),
diff --git a/src/Dotsider/Views/GeneralView.cs b/src/Dotsider/Views/GeneralView.cs
index 4bd98c21..4297f7fe 100644
--- a/src/Dotsider/Views/GeneralView.cs
+++ b/src/Dotsider/Views/GeneralView.cs
@@ -303,7 +303,9 @@ Hex1bWidget BuildReferencesPanel(WidgetContext outer)
if (resolution.Resolved is not null)
{
state.PushAssembly(resolution.Resolved);
+ state.RequestContentFocus();
state.App.Invalidate();
+ state.RequestExtraFrame();
}
})
.Compact()
@@ -330,7 +332,9 @@ Hex1bWidget BuildReferencesPanel(WidgetContext outer)
if (resolution.Resolved is not null)
{
state.PushAssembly(resolution.Resolved);
+ state.RequestContentFocus();
state.App.Invalidate();
+ state.RequestExtraFrame();
}
}, "Drill into reference");
})
diff --git a/src/Dotsider/Views/HexDumpView.cs b/src/Dotsider/Views/HexDumpView.cs
index 4b44e222..66108d04 100644
--- a/src/Dotsider/Views/HexDumpView.cs
+++ b/src/Dotsider/Views/HexDumpView.cs
@@ -124,6 +124,7 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s
state.HexMatchPatternLength = 0;
state.HexLastSearchQuery = null;
state.HexLiveSearchTooSlow = false;
+ state.RequestContentFocus();
state.App.Invalidate();
}
}, "Esc");
diff --git a/src/Dotsider/Views/IlInspectorView.cs b/src/Dotsider/Views/IlInspectorView.cs
index 852ec9b7..180db0df 100644
--- a/src/Dotsider/Views/IlInspectorView.cs
+++ b/src/Dotsider/Views/IlInspectorView.cs
@@ -199,57 +199,46 @@ public static Hex1bWidget Build(WidgetContext ctx, DotsiderState s
IlTreeList.Build(
treeRows,
formattedRows,
+ getRows: () => IsNativeTreeMode(state) ? BuildNativeTreeRows(state) : BuildTreeRows(state),
state,
- selectionChanged: index =>
+ selectionChanged: row =>
{
- if (index >= 0 && index < treeRows.Count)
+ // Direct assignment on keyboard/click moves does NOT arm
+ // IlScrollSelectionIntoViewPending. The keyboard handler
+ // calls EnsureSelectionVisible inline, so the pending
+ // path is reserved for external setters.
+ state.IlFocusedTreeKey = row.Key;
+ if (row is { Kind: IlTreeRowKind.Method, Method: not null })
{
- var row = treeRows[index];
- // Direct assignment on keyboard/click moves — does NOT arm
- // IlScrollSelectionIntoViewPending. The keyboard handler
- // calls EnsureSelectionVisible inline, so the pending
- // path is reserved for external setters.
- state.IlFocusedTreeKey = row.Key;
- if (row is { Kind: IlTreeRowKind.Method, Method: not null })
- {
- state.IlSelectedMethod = row.Method;
- state.IlSelectedMethodOwner = row.Owner;
- }
- else if (row is { Kind: IlTreeRowKind.Method, Symbol: not null })
- {
- state.IlSelectedNativeSymbol = row.Symbol;
- }
+ state.IlSelectedMethod = row.Method;
+ state.IlSelectedMethodOwner = row.Owner;
}
+ else if (row is { Kind: IlTreeRowKind.Method, Symbol: not null })
+ {
+ state.IlSelectedNativeSymbol = row.Symbol;
+ }
+
state.App.Invalidate();
},
- itemActivated: index =>
+ itemActivated: row =>
{
- if (index >= 0 && index < treeRows.Count)
- ActivateTreeRow(treeRows[index], state);
+ ActivateTreeRow(row, state);
state.App.Invalidate();
},
- expandRow: index =>
+ expandRow: row =>
{
- if (index >= 0 && index < treeRows.Count)
+ if (row is { CanExpand: true, IsExpanded: false })
{
- var row = treeRows[index];
- if (row is { CanExpand: true, IsExpanded: false })
- {
- state.IlTreeExpansionState[row.ExpansionKey] = true;
- state.App.Invalidate();
- }
+ state.IlTreeExpansionState[row.ExpansionKey] = true;
+ state.App.Invalidate();
}
},
- collapseRow: index =>
+ collapseRow: row =>
{
- if (index >= 0 && index < treeRows.Count)
+ if (row is { CanExpand: true, IsExpanded: true })
{
- var row = treeRows[index];
- if (row is { CanExpand: true, IsExpanded: true })
- {
- state.IlTreeExpansionState[row.ExpansionKey] = false;
- state.App.Invalidate();
- }
+ state.IlTreeExpansionState[row.ExpansionKey] = false;
+ state.App.Invalidate();
}
})
],
diff --git a/src/Dotsider/Views/IlTreeList.cs b/src/Dotsider/Views/IlTreeList.cs
index 0ec2530a..d974bbb6 100644
--- a/src/Dotsider/Views/IlTreeList.cs
+++ b/src/Dotsider/Views/IlTreeList.cs
@@ -34,10 +34,14 @@ internal static class IlTreeList
///
/// The flattened tree rows.
/// Pre-formatted display strings (one per row).
+ /// Returns the current flattened tree rows for input
+ /// dispatch. This can differ from when a queued key
+ /// arrives after search, expansion, or mode state changed but before the
+ /// next rendered tree replaces this binding closure.
/// The shared application state. Read live by every binding so
/// coalesced input batches operate on post-mutation selection.
/// Invoked by keyboard navigation and row clicks
- /// after the new index is computed. The caller is expected to assign
+ /// after the new row is computed. The caller is expected to assign
/// state.IlFocusedTreeKey directly (no ),
/// because the keyboard path manages scroll-into-view itself and must not arm
/// .
@@ -51,11 +55,12 @@ internal static class IlTreeList
internal static Hex1bWidget Build(
IReadOnlyList rows,
IReadOnlyList formattedRows,
+ Func> getRows,
DotsiderState state,
- Action? selectionChanged,
- Action? itemActivated,
- Action? expandRow,
- Action? collapseRow)
+ Action? selectionChanged,
+ Action? itemActivated,
+ Action? expandRow,
+ Action? collapseRow)
{
var selectedIndex = ResolveEffectiveIndex(rows, state.IlFocusedTreeKey);
@@ -122,79 +127,86 @@ internal static Hex1bWidget Build(
bindings.Remove(ScrollPanelWidget.MouseScrollDownAction);
bindings.Key(Hex1bKey.UpArrow).Action(e =>
- MoveSelection(e, rows, state, selectionChanged, -1), "Move up");
+ MoveSelection(e, getRows(), state, selectionChanged, -1), "Move up");
bindings.Key(Hex1bKey.DownArrow).Action(e =>
- MoveSelection(e, rows, state, selectionChanged, +1), "Move down");
+ MoveSelection(e, getRows(), state, selectionChanged, +1), "Move down");
bindings.Key(Hex1bKey.Home).Action(e =>
- SetSelection(e, rows, state, selectionChanged, 0), "Top");
+ SetSelection(e, getRows(), state, selectionChanged, 0), "Top");
bindings.Key(Hex1bKey.End).Action(e =>
- SetSelection(e, rows, state, selectionChanged, rows.Count - 1), "Bottom");
+ {
+ var currentRows = getRows();
+ SetSelection(e, currentRows, state, selectionChanged, currentRows.Count - 1);
+ }, "Bottom");
bindings.Key(Hex1bKey.PageUp).Action(e =>
{
if (e.FocusedNode is not ScrollPanelNode sp) return;
+ var currentRows = getRows();
var step = Math.Max(1, sp.ViewportSize - 1);
- MoveSelection(e, rows, state, selectionChanged, -step);
+ MoveSelection(e, currentRows, state, selectionChanged, -step);
}, "Page up");
bindings.Key(Hex1bKey.PageDown).Action(e =>
{
if (e.FocusedNode is not ScrollPanelNode sp) return;
+ var currentRows = getRows();
var step = Math.Max(1, sp.ViewportSize - 1);
- MoveSelection(e, rows, state, selectionChanged, +step);
+ MoveSelection(e, currentRows, state, selectionChanged, +step);
}, "Page down");
bindings.Key(Hex1bKey.Enter).Action(_ =>
- ActivateCurrent(rows, state, itemActivated), "Activate");
+ ActivateCurrent(getRows(), state, itemActivated), "Activate");
bindings.Key(Hex1bKey.Spacebar).Action(_ =>
- ActivateCurrent(rows, state, itemActivated), "Activate");
+ ActivateCurrent(getRows(), state, itemActivated), "Activate");
bindings.Key(Hex1bKey.LeftArrow).Action(e =>
- HandleLeft(e, rows, state, selectionChanged, collapseRow), "Collapse / parent");
+ HandleLeft(e, getRows(), state, selectionChanged, collapseRow), "Collapse / parent");
bindings.Key(Hex1bKey.RightArrow).Action(e =>
- HandleRight(e, rows, state, selectionChanged, expandRow), "Expand / child");
+ HandleRight(e, getRows(), state, selectionChanged, expandRow), "Expand / child");
// Wheel scrolls the viewport only — the selection stays put, even
// offscreen, mirroring the old panel's decoupled wheel behavior.
bindings.Mouse(MouseButton.ScrollUp).Action(_ =>
- ScrollViewport(state, rows.Count, -3), "Scroll up");
+ ScrollViewport(state, getRows().Count, -3), "Scroll up");
bindings.Mouse(MouseButton.ScrollDown).Action(_ =>
- ScrollViewport(state, rows.Count, +3), "Scroll down");
+ ScrollViewport(state, getRows().Count, +3), "Scroll down");
// Gutter presses: thumb drag or track-click paging. Row-area presses
// return an empty handler, which Hex1b treats as a rejected drag and
// falls through to the Left-click row selection below.
bindings.Drag(MouseButton.Left).Action((localX, localY) =>
{
+ var currentRows = getRows();
var sp = state.IlScrollPanelNode;
- if (sp is null || rows.Count <= sp.ViewportSize) return new DragHandler();
+ if (sp is null || currentRows.Count <= sp.ViewportSize) return new DragHandler();
if (localX < sp.Bounds.Width - 1) return new DragHandler();
- return IlTreeScrollbar.HandleDrag(state, rows.Count, sp.ViewportSize, localY);
+ return IlTreeScrollbar.HandleDrag(state, currentRows.Count, sp.ViewportSize, localY);
}, "Drag scrollbar");
bindings.Mouse(MouseButton.Left).Action(e =>
{
+ var currentRows = getRows();
if (e.FocusedNode is not ScrollPanelNode sp) return;
- if (rows.Count == 0) return;
+ if (currentRows.Count == 0) return;
// The rightmost column is the scrollbar gutter only when the tree
// actually overflows; when content fits, that column is normal row
// area and a click there must still select the row.
var localX = e.MouseX - sp.Bounds.X;
if (localX < 0 || localX >= sp.Bounds.Width) return;
- if (rows.Count > sp.ViewportSize && localX >= sp.Bounds.Width - 1) return;
+ if (currentRows.Count > sp.ViewportSize && localX >= sp.Bounds.Width - 1) return;
var rowIndex = (e.MouseY - sp.Bounds.Y) + state.IlTreeScrollOffset;
- if (rowIndex < 0 || rowIndex >= rows.Count) return;
+ if (rowIndex < 0 || rowIndex >= currentRows.Count) return;
- selectionChanged?.Invoke(rowIndex);
- itemActivated?.Invoke(rowIndex);
+ selectionChanged?.Invoke(currentRows[rowIndex]);
+ itemActivated?.Invoke(currentRows[rowIndex]);
}, "Select row");
})
.Fill();
@@ -261,7 +273,7 @@ private static void MoveSelection(
InputBindingActionContext e,
IReadOnlyList rows,
DotsiderState state,
- Action? selectionChanged,
+ Action? selectionChanged,
int delta)
{
if (e.FocusedNode is not ScrollPanelNode sp) return;
@@ -280,7 +292,7 @@ private static void MoveSelection(
return;
}
- selectionChanged?.Invoke(newIndex);
+ selectionChanged?.Invoke(rows[newIndex]);
IlInspectorView.EnsureSelectionVisible(state, sp, newIndex, rows.Count);
state.App.Invalidate();
}
@@ -289,7 +301,7 @@ private static void SetSelection(
InputBindingActionContext e,
IReadOnlyList rows,
DotsiderState state,
- Action? selectionChanged,
+ Action? selectionChanged,
int target)
{
if (e.FocusedNode is not ScrollPanelNode sp) return;
@@ -305,7 +317,7 @@ private static void SetSelection(
return;
}
- selectionChanged?.Invoke(newIndex);
+ selectionChanged?.Invoke(rows[newIndex]);
IlInspectorView.EnsureSelectionVisible(state, sp, newIndex, rows.Count);
state.App.Invalidate();
}
@@ -313,20 +325,20 @@ private static void SetSelection(
private static void ActivateCurrent(
IReadOnlyList rows,
DotsiderState state,
- Action? itemActivated)
+ Action? itemActivated)
{
if (rows.Count == 0) return;
var idx = ResolveEffectiveIndex(rows, state.IlFocusedTreeKey);
if (idx >= 0 && idx < rows.Count)
- itemActivated?.Invoke(idx);
+ itemActivated?.Invoke(rows[idx]);
}
private static void HandleLeft(
InputBindingActionContext e,
IReadOnlyList rows,
DotsiderState state,
- Action? selectionChanged,
- Action? collapseRow)
+ Action? selectionChanged,
+ Action? collapseRow)
{
if (e.FocusedNode is not ScrollPanelNode sp) return;
if (rows.Count == 0) return;
@@ -336,7 +348,7 @@ private static void HandleLeft(
var row = rows[idx];
if (row is { CanExpand: true, IsExpanded: true })
{
- collapseRow?.Invoke(idx);
+ collapseRow?.Invoke(row);
return;
}
@@ -346,7 +358,7 @@ private static void HandleLeft(
{
if (rows[i].Depth < row.Depth)
{
- selectionChanged?.Invoke(i);
+ selectionChanged?.Invoke(rows[i]);
IlInspectorView.EnsureSelectionVisible(state, sp, i, rows.Count);
state.App.Invalidate();
return;
@@ -358,8 +370,8 @@ private static void HandleRight(
InputBindingActionContext e,
IReadOnlyList rows,
DotsiderState state,
- Action? selectionChanged,
- Action? expandRow)
+ Action? selectionChanged,
+ Action? expandRow)
{
if (e.FocusedNode is not ScrollPanelNode sp) return;
if (rows.Count == 0) return;
@@ -369,7 +381,7 @@ private static void HandleRight(
var row = rows[idx];
if (row is { CanExpand: true, IsExpanded: false })
{
- expandRow?.Invoke(idx);
+ expandRow?.Invoke(row);
return;
}
@@ -377,7 +389,7 @@ private static void HandleRight(
if (row.IsExpanded && idx + 1 < rows.Count && rows[idx + 1].Depth == row.Depth + 1)
{
var childIdx = idx + 1;
- selectionChanged?.Invoke(childIdx);
+ selectionChanged?.Invoke(rows[childIdx]);
IlInspectorView.EnsureSelectionVisible(state, sp, childIdx, rows.Count);
state.App.Invalidate();
}
diff --git a/src/Dotsider/Views/StringsView.cs b/src/Dotsider/Views/StringsView.cs
index 54759c1e..51b3f0b8 100644
--- a/src/Dotsider/Views/StringsView.cs
+++ b/src/Dotsider/Views/StringsView.cs
@@ -95,6 +95,12 @@ [.. SourceTabs.Select((name, i) =>
)
.OnSelectionChanged(e =>
{
+ if (state.StringsDetailContent is not null)
+ {
+ state.App.Invalidate();
+ return;
+ }
+
state.StringsSourceTab = e.SelectedIndex;
search.Reset();
state.StringsFocusedKey = null;
@@ -144,8 +150,9 @@ [.. SourceTabs.Select((name, i) =>
{
var isSearchEditing = search.IsActive && !search.IsConfirmed;
- // Left/Right arrows to switch sub-tabs (suppressed during search editing)
- if (!isSearchEditing)
+ // Left/Right arrows to switch sub-tabs. Detail popups behave modally,
+ // so their editor/click-away surface owns navigation while open.
+ if (!isSearchEditing && state.StringsDetailContent is null)
{
if (state.App.FocusedNode is not EditorNode)
{
diff --git a/src/Dotsider/Views/TextObjectHelper.cs b/src/Dotsider/Views/TextObjectHelper.cs
index e55e09a0..a09a0b32 100644
--- a/src/Dotsider/Views/TextObjectHelper.cs
+++ b/src/Dotsider/Views/TextObjectHelper.cs
@@ -167,20 +167,34 @@ public static void ConfigureReadOnlyEditorBindings(
invalidate();
}, "");
- // --- Triple-click override: select line content only (no trailing newline) ---
- // The default EditorWidget triple-click uses SelectLineAt which positions the
- // cursor at the start of the NEXT line (exclusive end convention). PerformEditorYank
- // adds +1 to cursor.Position (inclusive/neovim convention for iw/iW). These two
- // conventions clash, causing yank to grab the newline plus the first character of
- // the next line. Fix: replace the default handler with one that positions the
- // cursor on the last visible character of the line (inclusive end).
+ // --- Double/triple-click overrides ---
+ // Hex1b click-count matching allows a double-click binding to match triple-clicks,
+ // so register both handlers here with the line-selection handler first.
+ bindings.Remove(EditorWidget.DoubleClick);
bindings.Remove(EditorWidget.TripleClick);
+
bindings.Mouse(MouseButton.Left).TripleClick().Action(_ =>
{
+ ResetToIdle();
SelectLine(thisEditorState);
invalidate();
}, "Triple-click to select line");
+ bindings.Mouse(MouseButton.Left).DoubleClick().Action(_ =>
+ {
+ ResetToIdle();
+ thisEditorState.SelectWordAt(thisEditorState.Cursor.Position);
+ invalidate();
+ }, "Double-click to select word");
+
+ // --- Triple-click behavior: select line content only (no trailing newline) ---
+ // The default EditorWidget triple-click uses SelectLineAt which positions the
+ // cursor at the start of the NEXT line (exclusive end convention). PerformEditorYank
+ // adds +1 to cursor.Position (inclusive/neovim convention for iw/iW). These two
+ // conventions clash, causing yank to grab the newline plus the first character of
+ // the next line. Fix: replace the default handler with one that positions the
+ // cursor on the last visible character of the line (inclusive end).
+
// --- Shift+V: visual line select (vim V) ---
bindings.Shift().Key(Hex1bKey.V).Action(_ =>
{
@@ -265,8 +279,6 @@ public static void ConfigureReadOnlyEditorBindings(
// --- Cancellation: mouse bindings ---
bindings.Mouse(MouseButton.Left).Action(() => ResetToIdle(), "");
bindings.Mouse(MouseButton.Left).Ctrl().Action(() => ResetToIdle(), "");
- bindings.Mouse(MouseButton.Left).DoubleClick().Action(() => ResetToIdle(), "");
- bindings.Mouse(MouseButton.Left).TripleClick().Action(() => ResetToIdle(), "");
bindings.Drag(MouseButton.Left).Action((_, _) =>
{
ResetToIdle();
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
new file mode 100644
index 00000000..16e2e44f
--- /dev/null
+++ b/tests/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+
+ All
+
+
+
+
+
+
diff --git a/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs b/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs
index da2820b0..aabde407 100644
--- a/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/AssemblyToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class AssemblyToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class AssemblyToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Verifies get_assembly_info returns populated metadata for a simple console executable.
///
- [Fact]
+ [TestMethod]
public async Task GetAssemblyInfo_HelloWorld_ReturnsAssemblyMetadata()
{
await StartServerAsync();
@@ -22,22 +24,22 @@ public async Task GetAssemblyInfo_HelloWorld_ReturnsAssemblyMetadata()
var result = await client.CallToolAsync(
"get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.HelloWorldDll },
+ new Dictionary { ["assemblyPath"] = Samples.HelloWorldDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("HelloWorld", json.GetProperty("assemblyName").GetString());
- Assert.True(json.GetProperty("hasMetadata").GetBoolean());
- Assert.True(json.GetProperty("typeCount").GetInt32() > 0);
- Assert.True(json.GetProperty("methodCount").GetInt32() > 0);
+ Assert.AreEqual("HelloWorld", json.GetProperty("assemblyName").GetString());
+ Assert.IsTrue(json.GetProperty("hasMetadata").GetBoolean());
+ Assert.IsGreaterThan(0, json.GetProperty("typeCount").GetInt32());
+ Assert.IsGreaterThan(0, json.GetProperty("methodCount").GetInt32());
}
///
/// Confirms get_assembly_info surfaces version data and external references for a richer library.
///
- [Fact]
+ [TestMethod]
public async Task GetAssemblyInfo_RichLibrary_IncludesVersion()
{
await StartServerAsync();
@@ -45,20 +47,20 @@ public async Task GetAssemblyInfo_RichLibrary_IncludesVersion()
var result = await client.CallToolAsync(
"get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("RichLibrary", json.GetProperty("assemblyName").GetString());
- Assert.True(json.GetProperty("assemblyRefCount").GetInt32() > 0);
+ Assert.AreEqual("RichLibrary", json.GetProperty("assemblyName").GetString());
+ Assert.IsGreaterThan(0, json.GetProperty("assemblyRefCount").GetInt32());
}
///
/// Invoking get_assembly_info without required arguments yields a descriptive error payload.
///
- [Fact]
+ [TestMethod]
public async Task GetAssemblyInfo_NoParams_ReturnsError()
{
await StartServerAsync();
@@ -70,14 +72,14 @@ public async Task GetAssemblyInfo_NoParams_ReturnsError()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("Error", text);
}
///
/// list_types enumerates defined types from a basic assembly.
///
- [Fact]
+ [TestMethod]
public async Task ListTypes_HelloWorld_ReturnsTypes()
{
await StartServerAsync();
@@ -85,19 +87,19 @@ public async Task ListTypes_HelloWorld_ReturnsTypes()
var result = await client.CallToolAsync(
"list_types",
- new Dictionary { ["assemblyPath"] = samples.HelloWorldDll },
+ new Dictionary { ["assemblyPath"] = Samples.HelloWorldDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var types = JsonSerializer.Deserialize(text);
- Assert.True(types.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, types.GetArrayLength());
}
///
/// A query filter narrows list_types output to name-matching results only.
///
- [Fact]
+ [TestMethod]
public async Task ListTypes_WithQuery_FiltersResults()
{
await StartServerAsync();
@@ -107,17 +109,17 @@ public async Task ListTypes_WithQuery_FiltersResults()
"list_types",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["query"] = "UserService"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var types = JsonSerializer.Deserialize(text);
foreach (var type in types.EnumerateArray())
{
- Assert.Contains("UserService", type.GetProperty("fullName").GetString(),
+ Assert.Contains("UserService", type.GetProperty("fullName").GetString()!,
StringComparison.OrdinalIgnoreCase);
}
}
@@ -125,7 +127,7 @@ public async Task ListTypes_WithQuery_FiltersResults()
///
/// maxResults caps list_types output to protect clients from oversized payloads.
///
- [Fact]
+ [TestMethod]
public async Task ListTypes_WithMaxResults_LimitsOutput()
{
await StartServerAsync();
@@ -135,21 +137,21 @@ public async Task ListTypes_WithMaxResults_LimitsOutput()
"list_types",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["maxResults"] = 3
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var types = JsonSerializer.Deserialize(text);
- Assert.True(types.GetArrayLength() <= 3);
+ Assert.IsLessThanOrEqualTo(3, types.GetArrayLength());
}
///
/// list_methods returns defined methods for a trivial assembly.
///
- [Fact]
+ [TestMethod]
public async Task ListMethods_HelloWorld_ReturnsMethods()
{
await StartServerAsync();
@@ -157,19 +159,19 @@ public async Task ListMethods_HelloWorld_ReturnsMethods()
var result = await client.CallToolAsync(
"list_methods",
- new Dictionary { ["assemblyPath"] = samples.HelloWorldDll },
+ new Dictionary { ["assemblyPath"] = Samples.HelloWorldDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var methods = JsonSerializer.Deserialize(text);
- Assert.True(methods.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, methods.GetArrayLength());
}
///
/// typeName filter restricts list_methods to methods of the specified declaring type.
///
- [Fact]
+ [TestMethod]
public async Task ListMethods_FilterByTypeName_ReturnsFilteredMethods()
{
await StartServerAsync();
@@ -179,18 +181,18 @@ public async Task ListMethods_FilterByTypeName_ReturnsFilteredMethods()
"list_methods",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["typeName"] = "UserService"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var methods = JsonSerializer.Deserialize(text);
- Assert.True(methods.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, methods.GetArrayLength());
foreach (var method in methods.EnumerateArray())
{
- Assert.Contains("UserService", method.GetProperty("declaringType").GetString(),
+ Assert.Contains("UserService", method.GetProperty("declaringType").GetString()!,
StringComparison.OrdinalIgnoreCase);
}
}
@@ -198,7 +200,7 @@ public async Task ListMethods_FilterByTypeName_ReturnsFilteredMethods()
///
/// find_members returns a grouped payload of types and methods matching the query.
///
- [Fact]
+ [TestMethod]
public async Task FindMembers_SearchQuery_ReturnsMatchingMembers()
{
await StartServerAsync();
@@ -208,21 +210,21 @@ public async Task FindMembers_SearchQuery_ReturnsMatchingMembers()
"find_members",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["query"] = "User"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("types", out _) || json.TryGetProperty("methods", out _));
+ Assert.IsTrue(json.TryGetProperty("types", out _) || json.TryGetProperty("methods", out _));
}
///
/// A missing file surfaces as an IsError result with a clear not-found message.
///
- [Fact]
+ [TestMethod]
public async Task GetAssemblyInfo_NonexistentFile_ReturnsFileNotFoundError()
{
await StartServerAsync();
@@ -233,9 +235,9 @@ public async Task GetAssemblyInfo_NonexistentFile_ReturnsFileNotFoundError()
new Dictionary { ["assemblyPath"] = "/nonexistent/path.dll" },
cancellationToken: TestCancellationToken);
- Assert.True(result.IsError);
+ Assert.IsTrue(result.IsError);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("File not found", text);
Assert.Contains("/nonexistent/path.dll", text);
}
@@ -243,7 +245,7 @@ public async Task GetAssemblyInfo_NonexistentFile_ReturnsFileNotFoundError()
///
/// An effectively empty assembly still returns a valid JSON array from list_types.
///
- [Fact]
+ [TestMethod]
public async Task ListTypes_EmptyLib_ReturnsMinimalTypes()
{
await StartServerAsync();
@@ -251,22 +253,23 @@ public async Task ListTypes_EmptyLib_ReturnsMinimalTypes()
var result = await client.CallToolAsync(
"list_types",
- new Dictionary { ["assemblyPath"] = samples.EmptyLibDll },
+ new Dictionary { ["assemblyPath"] = Samples.EmptyLibDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var types = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, types.ValueKind);
+ Assert.AreEqual(JsonValueKind.Array, types.ValueKind);
}
///
/// list_types unwraps Webcil browser app assemblies and returns their managed type definitions.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListTypes_WebcilWasm_ReturnsManagedTypes()
{
- Assert.SkipWhen(samples.WasmConsoleWebcilWasm is null,
+ TestSkip.When(Samples.WasmConsoleWebcilWasm is null,
"browser-wasm publish did not produce the Webcil app assembly on this leg.");
await StartServerAsync();
@@ -274,70 +277,73 @@ public async Task ListTypes_WebcilWasm_ReturnsManagedTypes()
var result = await client.CallToolAsync(
"list_types",
- new Dictionary { ["assemblyPath"] = samples.WasmConsoleWebcilWasm },
+ new Dictionary { ["assemblyPath"] = Samples.WasmConsoleWebcilWasm },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var types = JsonSerializer.Deserialize(text);
- Assert.Contains(types.EnumerateArray(), static type =>
- type.GetProperty("fullName").GetString() == "WasmCalculator");
+ Assert.Contains(static type =>
+ type.GetProperty("fullName").GetString() == "WasmCalculator", types.EnumerateArray());
}
///
/// get_assembly_info exposes displayName, bundle flags, and preferred runtime pack.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_IncludesNewProperties()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.NotNull(json.GetProperty("displayName").GetString());
- Assert.False(json.GetProperty("isBundleBacked").GetBoolean());
- Assert.True(json.GetProperty("canSaveInPlace").GetBoolean());
- Assert.Equal("Microsoft.NETCore.App", json.GetProperty("preferredRuntimePack").GetString());
+ Assert.IsNotNull(json.GetProperty("displayName").GetString());
+ Assert.IsFalse(json.GetProperty("isBundleBacked").GetBoolean());
+ Assert.IsTrue(json.GetProperty("canSaveInPlace").GetBoolean());
+ Assert.AreEqual("Microsoft.NETCore.App", json.GetProperty("preferredRuntimePack").GetString());
}
///
/// get_assembly_info reports a Native AOT executable's binary kind and
/// ReadyToRun header facts.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_NativeAot_ReportsBinaryKindAndRtr()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null,
+ TestSkip.When(Samples.NativeAotConsoleExe is null,
"NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("nativeAot", json.GetProperty("binaryKind").GetString());
+ Assert.AreEqual("nativeAot", json.GetProperty("binaryKind").GetString());
var aotInfo = json.GetProperty("nativeAotInfo");
- Assert.True(aotInfo.GetProperty("majorVersion").GetInt32() >= 1);
- Assert.True(aotInfo.GetProperty("sectionCount").GetInt32() >= 1);
- Assert.False(json.GetProperty("hasMetadata").GetBoolean());
+ Assert.IsGreaterThanOrEqualTo(1, aotInfo.GetProperty("majorVersion").GetInt32());
+ Assert.IsGreaterThanOrEqualTo(1, aotInfo.GetProperty("sectionCount").GetInt32());
+ Assert.IsFalse(json.GetProperty("hasMetadata").GetBoolean());
}
///
/// get_assembly_info reports a raw dotnet.native.wasm module as WebAssembly and includes
/// function, code, data, and symbol-map facts from the SDK-produced module.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_Wasm_ReportsModuleFacts()
{
var wasmPath = GetWasmNativePath();
@@ -350,155 +356,161 @@ public async Task GetAssemblyInfo_Wasm_ReportsModuleFacts()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("wasm", json.GetProperty("binaryKind").GetString());
- Assert.Equal("Wasm32", json.GetProperty("architecture").GetString());
- Assert.False(json.GetProperty("hasMetadata").GetBoolean());
+ Assert.AreEqual("wasm", json.GetProperty("binaryKind").GetString());
+ Assert.AreEqual("Wasm32", json.GetProperty("architecture").GetString());
+ Assert.IsFalse(json.GetProperty("hasMetadata").GetBoolean());
var wasm = json.GetProperty("wasm");
- Assert.True(wasm.GetProperty("definedFunctionCount").GetInt32() > 0);
- Assert.True(wasm.GetProperty("importedFunctionCount").GetInt32() > 0);
- Assert.True(wasm.GetProperty("codeSize").GetInt64() > 0);
- Assert.True(wasm.GetProperty("dataSize").GetInt64() > 0);
- Assert.Equal("Loaded", wasm.GetProperty("symbolMapStatus").GetString());
+ Assert.IsGreaterThan(0, wasm.GetProperty("definedFunctionCount").GetInt32());
+ Assert.IsGreaterThan(0, wasm.GetProperty("importedFunctionCount").GetInt32());
+ Assert.IsGreaterThan(0, wasm.GetProperty("codeSize").GetInt64());
+ Assert.IsGreaterThan(0, wasm.GetProperty("dataSize").GetInt64());
+ Assert.AreEqual("Loaded", wasm.GetProperty("symbolMapStatus").GetString());
}
///
/// get_assembly_info reports a Webcil-wrapped browser app assembly as normal managed metadata
/// with Webcil payload facts attached, not as the raw runtime WebAssembly module.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_WebcilWasm_ReportsManagedMetadata()
{
- Assert.SkipWhen(samples.WasmConsoleWebcilWasm is null,
+ TestSkip.When(Samples.WasmConsoleWebcilWasm is null,
"browser-wasm publish did not produce the Webcil app assembly on this leg.");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.WasmConsoleWebcilWasm },
+ new Dictionary { ["assemblyPath"] = Samples.WasmConsoleWebcilWasm },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("managed", json.GetProperty("binaryKind").GetString());
- Assert.Equal("Wasm32", json.GetProperty("architecture").GetString());
- Assert.True(json.GetProperty("hasMetadata").GetBoolean());
- Assert.Equal("WasmConsole", json.GetProperty("assemblyName").GetString());
- Assert.False(json.TryGetProperty("wasm", out _));
+ Assert.AreEqual("managed", json.GetProperty("binaryKind").GetString());
+ Assert.AreEqual("Wasm32", json.GetProperty("architecture").GetString());
+ Assert.IsTrue(json.GetProperty("hasMetadata").GetBoolean());
+ Assert.AreEqual("WasmConsole", json.GetProperty("assemblyName").GetString());
+ Assert.IsFalse(json.TryGetProperty("wasm", out _));
var webcil = json.GetProperty("webcil");
- Assert.True(webcil.GetProperty("isWasmWrapped").GetBoolean());
- Assert.True(webcil.GetProperty("sectionCount").GetInt32() > 0);
- Assert.True(webcil.GetProperty("metadataSize").GetInt32() > 0);
+ Assert.IsTrue(webcil.GetProperty("isWasmWrapped").GetBoolean());
+ Assert.IsGreaterThan(0, webcil.GetProperty("sectionCount").GetInt32());
+ Assert.IsGreaterThan(0, webcil.GetProperty("metadataSize").GetInt32());
}
///
/// get_assembly_info reports a managed assembly's binary kind as managed with
/// no Native AOT info attached.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_Managed_ReportsManagedKind()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("managed", json.GetProperty("binaryKind").GetString());
- Assert.False(json.TryGetProperty("nativeAotInfo", out _));
+ Assert.AreEqual("managed", json.GetProperty("binaryKind").GetString());
+ Assert.IsFalse(json.TryGetProperty("nativeAotInfo", out _));
}
///
/// A self-contained apphost is reported as bundle-backed with an in-bundle display name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_BundleBacked_ShowsBundleInfo()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.SelfContainedConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.SelfContainedConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("isBundleBacked").GetBoolean());
- Assert.Equal("SelfContainedConsole.dll", json.GetProperty("displayName").GetString());
- Assert.False(json.GetProperty("canSaveInPlace").GetBoolean());
+ Assert.IsTrue(json.GetProperty("isBundleBacked").GetBoolean());
+ Assert.AreEqual("SelfContainedConsole.dll", json.GetProperty("displayName").GetString());
+ Assert.IsFalse(json.GetProperty("canSaveInPlace").GetBoolean());
}
///
/// ASP.NET Core apps report Microsoft.AspNetCore.App as their preferred runtime pack.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_AspNetCore_PreferredPack()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.MinimalApiDll },
+ new Dictionary { ["assemblyPath"] = Samples.MinimalApiDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("Microsoft.AspNetCore.App", json.GetProperty("preferredRuntimePack").GetString());
+ Assert.AreEqual("Microsoft.AspNetCore.App", json.GetProperty("preferredRuntimePack").GetString());
}
///
/// get_assembly_info reports Native AOT section, recovered-type, and frozen-string counts.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_NativeAot_ReportsAotCounts()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("readyToRunSectionCount").GetInt32() > 0);
- Assert.True(json.GetProperty("recoveredTypeCount").GetInt32() > 0);
- Assert.True(json.TryGetProperty("frozenStringCount", out _));
+ Assert.IsGreaterThan(0, json.GetProperty("readyToRunSectionCount").GetInt32());
+ Assert.IsGreaterThan(0, json.GetProperty("recoveredTypeCount").GetInt32());
+ Assert.IsTrue(json.TryGetProperty("frozenStringCount", out _));
}
///
/// list_types falls back to the types recovered from a Native AOT binary's metadata.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListTypes_NativeAot_ReturnsRecoveredTypes()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("list_types",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, json.ValueKind);
- Assert.True(json.GetArrayLength() > 0);
+ Assert.AreEqual(JsonValueKind.Array, json.ValueKind);
+ Assert.IsGreaterThan(0, json.GetArrayLength());
var names = json.EnumerateArray().Select(e => e.GetProperty("fullName").GetString()).ToList();
Assert.Contains("Program", names);
}
@@ -506,10 +518,11 @@ public async Task ListTypes_NativeAot_ReturnsRecoveredTypes()
///
/// list_methods falls back to recovered Native AOT method names when ECMA metadata is absent.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListMethods_NativeAot_ReturnsRecoveredMethods()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -517,27 +530,28 @@ public async Task ListMethods_NativeAot_ReturnsRecoveredMethods()
var result = await client.CallToolAsync("list_methods",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["typeName"] = "Program"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, json.ValueKind);
- Assert.True(json.GetArrayLength() > 0);
- Assert.All(json.EnumerateArray(), e =>
- Assert.Equal("RecoveredNativeAot", e.GetProperty("source").GetString()));
+ Assert.AreEqual(JsonValueKind.Array, json.ValueKind);
+ Assert.IsGreaterThan(0, json.GetArrayLength());
+ TestAssert.All(json.EnumerateArray(), e =>
+ Assert.AreEqual("RecoveredNativeAot", e.GetProperty("source").GetString()));
}
///
/// find_members searches recovered Native AOT types and methods instead of metadata-only tables.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task FindMembers_NativeAot_SearchesRecoveredInventory()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -545,24 +559,24 @@ public async Task FindMembers_NativeAot_SearchesRecoveredInventory()
var result = await client.CallToolAsync("find_members",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["query"] = "Program"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("types").GetArrayLength() > 0
+ Assert.IsTrue(json.GetProperty("types").GetArrayLength() > 0
|| json.GetProperty("methods").GetArrayLength() > 0);
- Assert.Equal(0, json.GetProperty("memberRefs").GetArrayLength());
+ Assert.AreEqual(0, json.GetProperty("memberRefs").GetArrayLength());
}
- private string GetWasmNativePath()
+ private static string GetWasmNativePath()
{
- Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null,
+ TestSkip.When(Samples.WasmConsoleNativeWasm is null && Samples.ReadyToRunConsoleWasmNativeWasm is null,
"browser-wasm publish did not run on this leg.");
- return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!;
+ return Samples.WasmConsoleNativeWasm ?? Samples.ReadyToRunConsoleWasmNativeWasm!;
}
}
diff --git a/tests/Dotsider.Mcp.Tests/BundleToolsTests.cs b/tests/Dotsider.Mcp.Tests/BundleToolsTests.cs
index 76003cb6..91165cae 100644
--- a/tests/Dotsider.Mcp.Tests/BundleToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/BundleToolsTests.cs
@@ -5,72 +5,78 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests for single-file bundle MCP tools: get_bundle_info and list_bundle_entries.
///
-[Collection("SampleAssemblies")]
-public class BundleToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class BundleToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// get_bundle_info on a self-contained apphost surfaces bundle flag and file count.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetBundleInfo_ReturnsBundleMetadata()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_bundle_info",
- new Dictionary { ["assemblyPath"] = samples.SelfContainedConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.SelfContainedConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("isBundle").GetBoolean());
- Assert.True(json.GetProperty("fileCount").GetInt32() > 0);
+ Assert.IsTrue(json.GetProperty("isBundle").GetBoolean());
+ Assert.IsGreaterThan(0, json.GetProperty("fileCount").GetInt32());
}
///
/// Non-bundle input returns false without false-positive bundle detection.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetBundleInfo_NonBundle_ReturnsFalse()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("get_bundle_info",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.False(json.GetProperty("isBundle").GetBoolean());
+ Assert.IsFalse(json.GetProperty("isBundle").GetBoolean());
}
///
/// list_bundle_entries enumerates the files packed inside a single-file apphost.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListBundleEntries_ReturnsEntries()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("list_bundle_entries",
- new Dictionary { ["assemblyPath"] = samples.SelfContainedConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.SelfContainedConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var entries = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, entries.ValueKind);
- Assert.True(entries.GetArrayLength() > 0);
+ Assert.AreEqual(JsonValueKind.Array, entries.ValueKind);
+ Assert.IsGreaterThan(0, entries.GetArrayLength());
}
///
/// Tool registry exposes both bundle tools by their expected identifiers.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListTools_IncludesBundleTools()
{
await StartServerAsync();
diff --git a/tests/Dotsider.Mcp.Tests/CorrelationToolsTests.cs b/tests/Dotsider.Mcp.Tests/CorrelationToolsTests.cs
index 6d57386d..4660e1c5 100644
--- a/tests/Dotsider.Mcp.Tests/CorrelationToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/CorrelationToolsTests.cs
@@ -5,27 +5,30 @@ namespace Dotsider.Mcp.Tests;
/// AOT fixture — the counts summary on get_assembly_info, a unique method resolved by name
/// and by address, and the ambiguous-name error path.
///
-[Collection("SampleAssemblies")]
-public class CorrelationToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class CorrelationToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// get_assembly_info carries the cheap pre-ILC probe summary for a Native AOT binary.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_NativeAot_CarriesPreIlcSummary()
{
- Assert.SkipWhen(samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("preIlc", text);
Assert.Contains("hasAttachableCompanion", text);
}
@@ -33,10 +36,11 @@ public async Task GetAssemblyInfo_NativeAot_CarriesPreIlcSummary()
///
/// correlate_method resolves a unique method by name and returns its IL and status.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CorrelateMethod_ByName_ReturnsReport()
{
- Assert.SkipWhen(samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -46,13 +50,13 @@ public async Task CorrelateMethod_ByName_ReturnsReport()
new Dictionary
{
["methodOrAddress"] = "Greeter.Describe",
- ["assemblyPath"] = samples.NativeAotConsoleExe
+ ["assemblyPath"] = Samples.NativeAotConsoleExe
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
Assert.Contains("\"method\"", text);
Assert.Contains("Greeter::Describe", text);
Assert.Contains("\"il\"", text);
@@ -61,13 +65,14 @@ public async Task CorrelateMethod_ByName_ReturnsReport()
///
/// correlate_method resolves by native address and returns the correlation-aware disassembly.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CorrelateMethod_ByAddress_ReturnsNativeDisassembly()
{
- Assert.SkipWhen(samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
ulong? va = null;
- using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(samples.NativeAotConsoleExe!))
+ using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(Samples.NativeAotConsoleExe!))
{
analyzer.AttachPreIlcCompanions();
var correlation = analyzer.ManagedNativeIndex?.Methods.FirstOrDefault(m =>
@@ -77,7 +82,7 @@ public async Task CorrelateMethod_ByAddress_ReturnsNativeDisassembly()
va = correlation?.NativeSymbols[0].VirtualAddress;
}
- Assert.SkipWhen(va is null, "no exact correlation with a native symbol on this leg");
+ TestSkip.When(va is null, "no exact correlation with a native symbol on this leg");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -87,23 +92,24 @@ public async Task CorrelateMethod_ByAddress_ReturnsNativeDisassembly()
new Dictionary
{
["methodOrAddress"] = $"0x{va!.Value:x}",
- ["assemblyPath"] = samples.NativeAotConsoleExe
+ ["assemblyPath"] = Samples.NativeAotConsoleExe
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
Assert.Contains("\"nativeDisassembly\"", text);
}
///
/// correlate_method surfaces an ambiguous name as an error listing every candidate.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CorrelateMethod_AmbiguousName_ReturnsCandidateError()
{
- Assert.SkipWhen(samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -113,12 +119,12 @@ public async Task CorrelateMethod_AmbiguousName_ReturnsCandidateError()
new Dictionary
{
["methodOrAddress"] = "Greeter.Greet",
- ["assemblyPath"] = samples.NativeAotConsoleExe
+ ["assemblyPath"] = Samples.NativeAotConsoleExe
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("ambiguous", text);
Assert.Contains("Greeter::Greet", text);
}
@@ -126,7 +132,8 @@ public async Task CorrelateMethod_AmbiguousName_ReturnsCandidateError()
///
/// correlate_method reports the Native AOT requirement for a managed assembly.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CorrelateMethod_Managed_ReturnsError()
{
await StartServerAsync();
@@ -137,12 +144,12 @@ public async Task CorrelateMethod_Managed_ReturnsError()
new Dictionary
{
["methodOrAddress"] = "Foo",
- ["assemblyPath"] = samples.RichLibraryDll
+ ["assemblyPath"] = Samples.RichLibraryDll
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("Error", text);
Assert.Contains("Native AOT", text);
}
diff --git a/tests/Dotsider.Mcp.Tests/DependencyToolsTests.cs b/tests/Dotsider.Mcp.Tests/DependencyToolsTests.cs
index ec48a3c4..4d5ecdbe 100644
--- a/tests/Dotsider.Mcp.Tests/DependencyToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/DependencyToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class DependencyToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class DependencyToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// get_assembly_refs returns the AssemblyRef table entries for a real library.
///
- [Fact]
+ [TestMethod]
public async Task GetAssemblyRefs_RichLibrary_ReturnsDependencies()
{
await StartServerAsync();
@@ -22,13 +24,13 @@ public async Task GetAssemblyRefs_RichLibrary_ReturnsDependencies()
var result = await client.CallToolAsync(
"get_assembly_refs",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var refs = JsonSerializer.Deserialize(text);
- Assert.True(refs.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, refs.GetArrayLength());
}
///
@@ -37,7 +39,7 @@ public async Task GetAssemblyRefs_RichLibrary_ReturnsDependencies()
/// id that is not the root — proving the tool emits transitive child-to-child edges, not
/// only root-to-child edges. No internal navigation fields leak into the JSON payload.
///
- [Fact]
+ [TestMethod]
public async Task GetDependencyGraph_RichLibrary_ReturnsTransitiveGraphWithoutNavigationLeak()
{
await StartServerAsync();
@@ -45,41 +47,41 @@ public async Task GetDependencyGraph_RichLibrary_ReturnsTransitiveGraphWithoutNa
var result = await client.CallToolAsync(
"get_dependency_graph",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("nodes", out var nodes));
- Assert.True(nodes.GetArrayLength() > 0);
- Assert.True(json.TryGetProperty("edges", out var edges));
+ Assert.IsTrue(json.TryGetProperty("nodes", out var nodes));
+ Assert.IsGreaterThan(0, nodes.GetArrayLength());
+ Assert.IsTrue(json.TryGetProperty("edges", out var edges));
string? rootId = null;
var anyDepthOverZero = false;
foreach (var n in nodes.EnumerateArray())
{
- Assert.True(n.TryGetProperty("id", out var id));
- Assert.False(string.IsNullOrEmpty(id.GetString()));
+ Assert.IsTrue(n.TryGetProperty("id", out var id));
+ Assert.IsFalse(string.IsNullOrEmpty(id.GetString()));
foreach (var leak in NavigationFieldsThatMustNotLeak)
- Assert.False(n.TryGetProperty(leak, out _), $"node should not expose {leak}");
+ Assert.IsFalse(n.TryGetProperty(leak, out _), $"node should not expose {leak}");
if (n.TryGetProperty("isRoot", out var isRoot) && isRoot.GetBoolean())
rootId = id.GetString();
if (n.TryGetProperty("depth", out var depth) && depth.GetInt32() > 0)
anyDepthOverZero = true;
}
- Assert.True(anyDepthOverZero, "expected at least one node with depth > 0");
- Assert.NotNull(rootId);
+ Assert.IsTrue(anyDepthOverZero, "expected at least one node with depth > 0");
+ Assert.IsNotNull(rootId);
var anyNonRootSource = false;
foreach (var e in edges.EnumerateArray())
{
- Assert.True(e.TryGetProperty("sourceId", out var src));
- Assert.True(e.TryGetProperty("targetId", out _));
+ Assert.IsTrue(e.TryGetProperty("sourceId", out var src));
+ Assert.IsTrue(e.TryGetProperty("targetId", out _));
if (src.GetString() != rootId) anyNonRootSource = true;
}
- Assert.True(anyNonRootSource, "expected at least one edge whose source is not the root");
+ Assert.IsTrue(anyNonRootSource, "expected at least one edge whose source is not the root");
}
private static readonly string[] NavigationFieldsThatMustNotLeak =
@@ -92,7 +94,7 @@ public async Task GetDependencyGraph_RichLibrary_ReturnsTransitiveGraphWithoutNa
///
/// get_type_refs surfaces externally-referenced types imported by the assembly.
///
- [Fact]
+ [TestMethod]
public async Task GetTypeRefs_RichLibrary_ReturnsImportedTypes()
{
await StartServerAsync();
@@ -100,19 +102,19 @@ public async Task GetTypeRefs_RichLibrary_ReturnsImportedTypes()
var result = await client.CallToolAsync(
"get_type_refs",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var refs = JsonSerializer.Deserialize(text);
- Assert.True(refs.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, refs.GetArrayLength());
}
///
/// Even a nearly empty assembly still references System.Runtime at minimum.
///
- [Fact]
+ [TestMethod]
public async Task GetAssemblyRefs_EmptyLib_ReturnsAtLeastSystemRuntime()
{
await StartServerAsync();
@@ -120,12 +122,12 @@ public async Task GetAssemblyRefs_EmptyLib_ReturnsAtLeastSystemRuntime()
var result = await client.CallToolAsync(
"get_assembly_refs",
- new Dictionary { ["assemblyPath"] = samples.EmptyLibDll },
+ new Dictionary { ["assemblyPath"] = Samples.EmptyLibDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var refs = JsonSerializer.Deserialize(text);
- Assert.True(refs.GetArrayLength() >= 1);
+ Assert.IsGreaterThanOrEqualTo(1, refs.GetArrayLength());
}
}
diff --git a/tests/Dotsider.Mcp.Tests/DiffToolsTests.cs b/tests/Dotsider.Mcp.Tests/DiffToolsTests.cs
index eebf4702..a69e865f 100644
--- a/tests/Dotsider.Mcp.Tests/DiffToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/DiffToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class DiffToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class DiffToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Comparing v1 to v2 of the same library produces a non-error diff payload.
///
- [Fact]
+ [TestMethod]
public async Task DiffAssemblies_V1VsV2_ReturnsDifferences()
{
await StartServerAsync();
@@ -24,20 +26,20 @@ public async Task DiffAssemblies_V1VsV2_ReturnsDifferences()
"diff_assemblies",
new Dictionary
{
- ["leftPath"] = samples.RichLibraryDll,
- ["rightPath"] = samples.RichLibraryV2Dll
+ ["leftPath"] = Samples.RichLibraryDll,
+ ["rightPath"] = Samples.RichLibraryV2Dll
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.DoesNotContain("Error", text);
}
///
/// Diffing an assembly against itself produces an empty, error-free diff.
///
- [Fact]
+ [TestMethod]
public async Task DiffAssemblies_SameAssembly_ReturnsNoDifferences()
{
await StartServerAsync();
@@ -47,20 +49,20 @@ public async Task DiffAssemblies_SameAssembly_ReturnsNoDifferences()
"diff_assemblies",
new Dictionary
{
- ["leftPath"] = samples.HelloWorldDll,
- ["rightPath"] = samples.HelloWorldDll
+ ["leftPath"] = Samples.HelloWorldDll,
+ ["rightPath"] = Samples.HelloWorldDll
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.DoesNotContain("Error", text);
}
///
/// maxTypeDiffs caps the typeDiffs array while metadataSummary retains full counts.
///
- [Fact]
+ [TestMethod]
public async Task DiffAssemblies_MaxTypeDiffs_LimitsTypeOutput()
{
await StartServerAsync();
@@ -70,30 +72,30 @@ public async Task DiffAssemblies_MaxTypeDiffs_LimitsTypeOutput()
"diff_assemblies",
new Dictionary
{
- ["leftPath"] = samples.RichLibraryDll,
- ["rightPath"] = samples.RichLibraryV2Dll,
+ ["leftPath"] = Samples.RichLibraryDll,
+ ["rightPath"] = Samples.RichLibraryV2Dll,
["maxTypeDiffs"] = 2
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
var typeDiffs = json.GetProperty("typeDiffs");
- Assert.True(typeDiffs.GetArrayLength() <= 2);
+ Assert.IsLessThanOrEqualTo(2, typeDiffs.GetArrayLength());
// Summary should still reflect full counts
var summary = json.GetProperty("metadataSummary");
var totalTypes = summary.GetProperty("typesAdded").GetInt32()
+ summary.GetProperty("typesRemoved").GetInt32()
+ summary.GetProperty("typesChanged").GetInt32();
- Assert.True(totalTypes > 2, "Summary should reflect all diffs, not the limited output");
+ Assert.IsGreaterThan(2, totalTypes, "Summary should reflect all diffs, not the limited output");
}
///
/// maxMethodDiffs caps the methodDiffs array without altering the aggregate summary.
///
- [Fact]
+ [TestMethod]
public async Task DiffAssemblies_MaxMethodDiffs_LimitsMethodOutput()
{
await StartServerAsync();
@@ -103,30 +105,30 @@ public async Task DiffAssemblies_MaxMethodDiffs_LimitsMethodOutput()
"diff_assemblies",
new Dictionary
{
- ["leftPath"] = samples.RichLibraryDll,
- ["rightPath"] = samples.RichLibraryV2Dll,
+ ["leftPath"] = Samples.RichLibraryDll,
+ ["rightPath"] = Samples.RichLibraryV2Dll,
["maxMethodDiffs"] = 5
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
var methodDiffs = json.GetProperty("methodDiffs");
- Assert.True(methodDiffs.GetArrayLength() <= 5);
+ Assert.IsLessThanOrEqualTo(5, methodDiffs.GetArrayLength());
// Summary should still reflect full counts
var summary = json.GetProperty("metadataSummary");
var totalMethods = summary.GetProperty("methodsAdded").GetInt32()
+ summary.GetProperty("methodsRemoved").GetInt32()
+ summary.GetProperty("methodsChanged").GetInt32();
- Assert.True(totalMethods > 5, "Summary should reflect all diffs, not the limited output");
+ Assert.IsGreaterThan(5, totalMethods, "Summary should reflect all diffs, not the limited output");
}
///
/// Type and method limits compose independently on the same diff invocation.
///
- [Fact]
+ [TestMethod]
public async Task DiffAssemblies_BothLimits_LimitsBothOutputs()
{
await StartServerAsync();
@@ -136,18 +138,18 @@ public async Task DiffAssemblies_BothLimits_LimitsBothOutputs()
"diff_assemblies",
new Dictionary
{
- ["leftPath"] = samples.RichLibraryDll,
- ["rightPath"] = samples.RichLibraryV2Dll,
+ ["leftPath"] = Samples.RichLibraryDll,
+ ["rightPath"] = Samples.RichLibraryV2Dll,
["maxTypeDiffs"] = 3,
["maxMethodDiffs"] = 10
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("typeDiffs").GetArrayLength() <= 3);
- Assert.True(json.GetProperty("methodDiffs").GetArrayLength() <= 10);
+ Assert.IsLessThanOrEqualTo(3, json.GetProperty("typeDiffs").GetArrayLength());
+ Assert.IsLessThanOrEqualTo(10, json.GetProperty("methodDiffs").GetArrayLength());
}
// --- diff_size ---
@@ -156,18 +158,19 @@ public async Task DiffAssemblies_BothLimits_LimitsBothOutputs()
private static readonly string[] s_generousGrowthBudget = ["total:growth=1000%"];
private static readonly string[] s_bareGrowthBudget = ["growth=1%"];
- private (string V1, string V2) RequireMstats()
+ private static (string V1, string V2) RequireMstats()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "V1 mstat sidecar was not produced");
- Assert.SkipWhen(samples.NativeAotConsoleV2Mstat is null, "V2 mstat sidecar was not produced");
- return (samples.NativeAotConsoleMstat!, samples.NativeAotConsoleV2Mstat!);
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "V1 mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleV2Mstat is null, "V2 mstat sidecar was not produced");
+ return (Samples.NativeAotConsoleMstat!, Samples.NativeAotConsoleV2Mstat!);
}
///
/// diff_size over the real V1/V2 mstat pair returns the summary, aggregates, and top
/// contributors — and no tree unless asked.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffSize_V1V2Mstats_ReturnsSummaryAndContributors()
{
var (v1, v2) = RequireMstats();
@@ -185,17 +188,18 @@ public async Task DiffSize_V1V2Mstats_ReturnsSummaryAndContributors()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.NotEqual(0, json.GetProperty("summary").GetProperty("delta").GetInt64());
- Assert.True(json.GetProperty("assemblyDeltas").GetArrayLength() > 0);
- Assert.True(json.GetProperty("namespaceDeltas").GetArrayLength() > 0);
- Assert.True(json.GetProperty("contributors").GetArrayLength() <= 5);
- Assert.False(json.TryGetProperty("root", out var root) && root.ValueKind != JsonValueKind.Null);
+ Assert.AreNotEqual(0, json.GetProperty("summary").GetProperty("delta").GetInt64());
+ Assert.IsGreaterThan(0, json.GetProperty("assemblyDeltas").GetArrayLength());
+ Assert.IsGreaterThan(0, json.GetProperty("namespaceDeltas").GetArrayLength());
+ Assert.IsLessThanOrEqualTo(5, json.GetProperty("contributors").GetArrayLength());
+ Assert.IsFalse(json.TryGetProperty("root", out var root) && root.ValueKind != JsonValueKind.Null);
}
/// diff_size of a report against itself returns a zero delta and no contributors.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffSize_SelfDiff_ZeroDelta()
{
var (v1, _) = RequireMstats();
@@ -212,17 +216,18 @@ public async Task DiffSize_SelfDiff_ZeroDelta()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal(0, json.GetProperty("summary").GetProperty("delta").GetInt64());
- Assert.Equal(0, json.GetProperty("contributors").GetArrayLength());
+ Assert.AreEqual(0, json.GetProperty("summary").GetProperty("delta").GetInt64());
+ Assert.AreEqual(0, json.GetProperty("contributors").GetArrayLength());
}
///
/// includeTree with a tight node cap emits a pruned tree and says so through the
/// truncation metadata — deterministic, never a silent sample.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffSize_IncludeTreeWithCap_SetsTruncationMetadata()
{
var (v1, v2) = RequireMstats();
@@ -241,16 +246,17 @@ public async Task DiffSize_IncludeTreeWithCap_SetsTruncationMetadata()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.NotEqual(JsonValueKind.Null, json.GetProperty("root").ValueKind);
- Assert.True(json.GetProperty("treeTruncated").GetBoolean());
- Assert.True(json.GetProperty("treeTotalNodes").GetInt32() > 10);
- Assert.True(json.GetProperty("treeIncludedNodes").GetInt32() <= 10);
+ Assert.AreNotEqual(JsonValueKind.Null, json.GetProperty("root").ValueKind);
+ Assert.IsTrue(json.GetProperty("treeTruncated").GetBoolean());
+ Assert.IsGreaterThan(10, json.GetProperty("treeTotalNodes").GetInt32());
+ Assert.IsLessThanOrEqualTo(10, json.GetProperty("treeIncludedNodes").GetInt32());
}
/// diff_size rejects an input that is not mstat-backed with a message naming the fix.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffSize_NonMstatInput_ReturnsError()
{
var (v1, _) = RequireMstats();
@@ -262,12 +268,12 @@ public async Task DiffSize_NonMstatInput_ReturnsError()
new Dictionary
{
["leftPath"] = v1,
- ["rightPath"] = samples.RichLibraryDll
+ ["rightPath"] = Samples.RichLibraryDll
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("not mstat-backed", text);
}
@@ -277,7 +283,8 @@ public async Task DiffSize_NonMstatInput_ReturnsError()
/// A zero-growth budget on the namespace added in V2 breaches, and the report carries the
/// violation and its scoped contributors.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CheckSizeBudgets_Breach_ReportsFailed()
{
var (v1, v2) = RequireMstats();
@@ -295,16 +302,17 @@ public async Task CheckSizeBudgets_Breach_ReportsFailed()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.False(json.GetProperty("passed").GetBoolean());
+ Assert.IsFalse(json.GetProperty("passed").GetBoolean());
var evaluation = json.GetProperty("evaluations")[0];
- Assert.True(evaluation.GetProperty("violations").GetArrayLength() > 0);
- Assert.True(evaluation.GetProperty("topContributors").GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, evaluation.GetProperty("violations").GetArrayLength());
+ Assert.IsGreaterThan(0, evaluation.GetProperty("topContributors").GetArrayLength());
}
/// A generous budget passes.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CheckSizeBudgets_Pass_ReportsPassed()
{
var (v1, v2) = RequireMstats();
@@ -322,16 +330,17 @@ public async Task CheckSizeBudgets_Pass_ReportsPassed()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("passed").GetBoolean());
+ Assert.IsTrue(json.GetProperty("passed").GetBoolean());
}
///
/// budgetsJson carries the object form — named budgets with warning severity — at full
/// parity with the CLI's budget file.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CheckSizeBudgets_BudgetsJson_ObjectFormHonored()
{
var (v1, v2) = RequireMstats();
@@ -357,19 +366,20 @@ public async Task CheckSizeBudgets_BudgetsJson_ObjectFormHonored()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("passed").GetBoolean(), "warning severity must not fail the check");
- Assert.True(json.GetProperty("hasWarnings").GetBoolean());
+ Assert.IsTrue(json.GetProperty("passed").GetBoolean(), "warning severity must not fail the check");
+ Assert.IsTrue(json.GetProperty("hasWarnings").GetBoolean());
var evaluation = json.GetProperty("evaluations")[0];
- Assert.False(evaluation.GetProperty("passed").GetBoolean());
- Assert.Equal("telemetry-watch",
+ Assert.IsFalse(evaluation.GetProperty("passed").GetBoolean());
+ Assert.AreEqual("telemetry-watch",
evaluation.GetProperty("budget").GetProperty("name").GetString());
- Assert.True(evaluation.GetProperty("topContributors").GetArrayLength() <= 3);
+ Assert.IsLessThanOrEqualTo(3, evaluation.GetProperty("topContributors").GetArrayLength());
}
/// budgetFilePath loads the same document schema from disk.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CheckSizeBudgets_BudgetFilePath_Honored()
{
var (v1, v2) = RequireMstats();
@@ -392,9 +402,9 @@ await File.WriteAllTextAsync(
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("passed").GetBoolean());
+ Assert.IsTrue(json.GetProperty("passed").GetBoolean());
}
finally
{
@@ -403,7 +413,8 @@ await File.WriteAllTextAsync(
}
/// Every budget source absent is an error, not an empty pass.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CheckSizeBudgets_NoBudgetSource_ReturnsError()
{
var (v1, v2) = RequireMstats();
@@ -420,12 +431,13 @@ public async Task CheckSizeBudgets_NoBudgetSource_ReturnsError()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("budget source is required", text);
}
/// A growth budget without a baseline is an error naming the missing parameter.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CheckSizeBudgets_GrowthWithoutBaseline_ReturnsError()
{
var (_, v2) = RequireMstats();
@@ -442,7 +454,7 @@ public async Task CheckSizeBudgets_GrowthWithoutBaseline_ReturnsError()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("baselinePath", text);
}
}
diff --git a/tests/Dotsider.Mcp.Tests/Dotsider.Mcp.Tests.csproj b/tests/Dotsider.Mcp.Tests/Dotsider.Mcp.Tests.csproj
index 755737c6..bf1d840f 100644
--- a/tests/Dotsider.Mcp.Tests/Dotsider.Mcp.Tests.csproj
+++ b/tests/Dotsider.Mcp.Tests/Dotsider.Mcp.Tests.csproj
@@ -1,34 +1,27 @@
-
+
-
- net10.0
- enable
- enable
- false
- true
- true
- true
-
+
+ net10.0
+ enable
+ enable
+ false
+ true
+ true
+ true
+
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
diff --git a/tests/Dotsider.Mcp.Tests/FieldToolsTests.cs b/tests/Dotsider.Mcp.Tests/FieldToolsTests.cs
index 687fffcc..0af2b0bb 100644
--- a/tests/Dotsider.Mcp.Tests/FieldToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/FieldToolsTests.cs
@@ -5,33 +5,37 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests for field definition listing MCP tools.
///
-[Collection("SampleAssemblies")]
-public class FieldToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class FieldToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// list_fields returns a non-empty JSON array of field entries for a real library.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListFields_ReturnsFields()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync("list_fields",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var fields = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, fields.ValueKind);
- Assert.True(fields.GetArrayLength() > 0);
+ Assert.AreEqual(JsonValueKind.Array, fields.ValueKind);
+ Assert.IsGreaterThan(0, fields.GetArrayLength());
}
///
/// A query narrows list_fields to fields whose names match the substring.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListFields_WithQuery_Filters()
{
await StartServerAsync();
@@ -40,15 +44,15 @@ public async Task ListFields_WithQuery_Filters()
var result = await client.CallToolAsync("list_fields",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["query"] = "_counter"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var fields = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, fields.ValueKind);
+ Assert.AreEqual(JsonValueKind.Array, fields.ValueKind);
// All results should contain "_counter" in their name
foreach (var field in fields.EnumerateArray())
Assert.Contains("_counter", field.GetProperty("name").GetString()!, StringComparison.OrdinalIgnoreCase);
@@ -57,7 +61,8 @@ public async Task ListFields_WithQuery_Filters()
///
/// typeName restricts list_fields to members of the specified declaring type.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListFields_WithTypeName_Filters()
{
await StartServerAsync();
@@ -66,13 +71,13 @@ public async Task ListFields_WithTypeName_Filters()
var result = await client.CallToolAsync("list_fields",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["typeName"] = "IlNavigationFixture"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var fields = JsonSerializer.Deserialize(text);
foreach (var field in fields.EnumerateArray())
Assert.Contains("IlNavigationFixture", field.GetProperty("declaringType").GetString()!);
@@ -81,12 +86,13 @@ public async Task ListFields_WithTypeName_Filters()
///
/// Tool registry advertises list_fields alongside the other assembly tools.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListTools_IncludesFieldTools()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var tools = await client.ListToolsAsync(cancellationToken: TestCancellationToken);
- Assert.Contains(tools, t => t.Name == "list_fields");
+ Assert.Contains(t => t.Name == "list_fields", tools);
}
}
diff --git a/tests/Dotsider.Mcp.Tests/IlToolsTests.cs b/tests/Dotsider.Mcp.Tests/IlToolsTests.cs
index 1fe75102..82cdbaa8 100644
--- a/tests/Dotsider.Mcp.Tests/IlToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/IlToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class IlToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class IlToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// disassemble_method returns an instruction list for an existing method body.
///
- [Fact]
+ [TestMethod]
public async Task DisassembleMethod_ValidMethod_ReturnsIlInstructions()
{
await StartServerAsync();
@@ -24,23 +26,23 @@ public async Task DisassembleMethod_ValidMethod_ReturnsIlInstructions()
"disassemble_method",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["typeName"] = "UserService",
["methodName"] = "Add"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("instructions", out var instructions));
- Assert.True(instructions.GetArrayLength() > 0);
+ Assert.IsTrue(json.TryGetProperty("instructions", out var instructions));
+ Assert.IsGreaterThan(0, instructions.GetArrayLength());
}
///
/// disassemble_method can include portable PDB debug information when requested.
///
- [Fact]
+ [TestMethod]
public async Task DisassembleMethod_WithDebugInfo_ReturnsPortablePdbData()
{
await StartServerAsync();
@@ -50,7 +52,7 @@ public async Task DisassembleMethod_WithDebugInfo_ReturnsPortablePdbData()
"disassemble_method",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["typeName"] = "UserService",
["methodName"] = "Add",
["includeDebugInfo"] = true
@@ -58,21 +60,20 @@ public async Task DisassembleMethod_WithDebugInfo_ReturnsPortablePdbData()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("sidecar", json.GetProperty("pdb").GetProperty("kind").GetString());
- Assert.True(json.GetProperty("sourceLink").GetProperty("isPresent").GetBoolean());
- Assert.True(json.GetProperty("debugInfo").GetProperty("sequencePoints").GetArrayLength() > 0);
- Assert.Contains(json.GetProperty("instructions").EnumerateArray(),
- instruction => instruction.TryGetProperty("localName", out var localName)
- && localName.GetString() == "id");
+ Assert.AreEqual("sidecar", json.GetProperty("pdb").GetProperty("kind").GetString());
+ Assert.IsTrue(json.GetProperty("sourceLink").GetProperty("isPresent").GetBoolean());
+ Assert.IsGreaterThan(0, json.GetProperty("debugInfo").GetProperty("sequencePoints").GetArrayLength());
+ Assert.Contains(instruction => instruction.TryGetProperty("localName", out var localName)
+ && localName.GetString() == "id", json.GetProperty("instructions").EnumerateArray());
}
///
/// get_method_debug_info returns sequence points and local names for a method.
///
- [Fact]
+ [TestMethod]
public async Task GetMethodDebugInfo_ReturnsSequencePointsAndLocals()
{
await StartServerAsync();
@@ -82,28 +83,26 @@ public async Task GetMethodDebugInfo_ReturnsSequencePointsAndLocals()
"get_method_debug_info",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["typeName"] = "UserService",
["methodName"] = "Add"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("sidecar", json.GetProperty("pdb").GetProperty("kind").GetString());
- Assert.Contains(json.GetProperty("sequencePoints").EnumerateArray(),
- point => point.GetProperty("document").GetString()?.EndsWith("UserService.cs",
- StringComparison.OrdinalIgnoreCase) == true);
- Assert.Contains(json.GetProperty("locals").EnumerateArray(),
- local => local.GetProperty("name").GetString() == "id");
+ Assert.AreEqual("sidecar", json.GetProperty("pdb").GetProperty("kind").GetString());
+ Assert.Contains(point => point.GetProperty("document").GetString()?.EndsWith("UserService.cs",
+ StringComparison.OrdinalIgnoreCase) == true, json.GetProperty("sequencePoints").EnumerateArray());
+ Assert.Contains(local => local.GetProperty("name").GetString() == "id", json.GetProperty("locals").EnumerateArray());
}
///
/// get_source_link returns decoded Source Link mappings.
///
- [Fact]
+ [TestMethod]
public async Task GetSourceLink_ReturnsMappings()
{
await StartServerAsync();
@@ -113,24 +112,23 @@ public async Task GetSourceLink_ReturnsMappings()
"get_source_link",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll
+ ["assemblyPath"] = Samples.RichLibraryDll
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("isPresent").GetBoolean());
- Assert.Contains(json.GetProperty("mappings").EnumerateArray(),
- mapping => mapping.GetProperty("urlTemplate").GetString()?.Contains("raw.githubusercontent.com",
- StringComparison.OrdinalIgnoreCase) == true);
+ Assert.IsTrue(json.GetProperty("isPresent").GetBoolean());
+ Assert.Contains(mapping => mapping.GetProperty("urlTemplate").GetString()?.Contains("raw.githubusercontent.com",
+ StringComparison.OrdinalIgnoreCase) == true, json.GetProperty("mappings").EnumerateArray());
}
///
/// Requesting IL for a method that does not exist yields a descriptive error.
///
- [Fact]
+ [TestMethod]
public async Task DisassembleMethod_NonExistentMethod_ReturnsError()
{
await StartServerAsync();
@@ -140,21 +138,21 @@ public async Task DisassembleMethod_NonExistentMethod_ReturnsError()
"disassemble_method",
new Dictionary
{
- ["assemblyPath"] = samples.HelloWorldDll,
+ ["assemblyPath"] = Samples.HelloWorldDll,
["typeName"] = "Program",
["methodName"] = "NonExistentMethod"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("Error", text);
}
///
/// search_il_opcodes locates call-family instructions across the assembly's bodies.
///
- [Fact]
+ [TestMethod]
public async Task SearchIlOpcodes_CallInstruction_FindsMatches()
{
await StartServerAsync();
@@ -164,22 +162,22 @@ public async Task SearchIlOpcodes_CallInstruction_FindsMatches()
"search_il_opcodes",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["query"] = "call",
["maxResults"] = 5
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var results = JsonSerializer.Deserialize(text);
- Assert.True(results.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, results.GetArrayLength());
}
///
/// search_il_opcodes surfaces newobj sites for identifying object allocations.
///
- [Fact]
+ [TestMethod]
public async Task SearchIlOpcodes_NewobjInstruction_FindsObjectCreation()
{
await StartServerAsync();
@@ -189,15 +187,15 @@ public async Task SearchIlOpcodes_NewobjInstruction_FindsObjectCreation()
"search_il_opcodes",
new Dictionary
{
- ["assemblyPath"] = samples.ComplexAppDll,
+ ["assemblyPath"] = Samples.ComplexAppDll,
["query"] = "newobj",
["maxResults"] = 10
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var results = JsonSerializer.Deserialize(text);
- Assert.Equal(JsonValueKind.Array, results.ValueKind);
+ Assert.AreEqual(JsonValueKind.Array, results.ValueKind);
}
}
diff --git a/tests/Dotsider.Mcp.Tests/McpCliTests.cs b/tests/Dotsider.Mcp.Tests/McpCliTests.cs
index 9b6e4873..0d1c8b49 100644
--- a/tests/Dotsider.Mcp.Tests/McpCliTests.cs
+++ b/tests/Dotsider.Mcp.Tests/McpCliTests.cs
@@ -9,6 +9,7 @@ namespace Dotsider.Mcp.Tests;
///
/// Process-level tests for the dotsider-mcp CLI entry point.
///
+[TestClass]
public partial class McpCliTests
{
private static readonly string s_projectPath = Path.Combine(
@@ -19,12 +20,12 @@ public partial class McpCliTests
///
/// --help prints usage text identifying the binary and exits with success.
///
- [Fact]
+ [TestMethod]
public async Task Help_ShowsUsageAndReturnsZero()
{
var (exitCode, stdout, _) = await RunMcpAsync("--help");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("dotsider-mcp", stdout);
Assert.Contains("MCP", stdout);
}
@@ -32,19 +33,19 @@ public async Task Help_ShowsUsageAndReturnsZero()
///
/// --version emits a non-empty version string and a zero exit code.
///
- [Fact]
+ [TestMethod]
public async Task Version_ReturnsZero()
{
var (exitCode, stdout, _) = await RunMcpAsync("--version");
- Assert.Equal(0, exitCode);
- Assert.False(string.IsNullOrWhiteSpace(stdout));
+ Assert.AreEqual(0, exitCode);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(stdout));
}
///
/// Launched with no arguments, the CLI boots the MCP server and completes the client handshake.
///
- [Fact]
+ [TestMethod]
public async Task NoArgs_StartsServerAndCompletesHandshake()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
@@ -53,20 +54,21 @@ public async Task NoArgs_StartsServerAndCompletesHandshake()
// which has project resolution overhead that can exceed the timeout.
var dllPath = Path.Combine(
FindRepoRoot(), "src", "Dotsider.Mcp", "bin", s_buildConfig, "net10.0", "dotsider-mcp.dll");
- Assert.True(File.Exists(dllPath), $"dotsider-mcp.dll not found: {dllPath}");
+ Assert.IsTrue(File.Exists(dllPath), $"dotsider-mcp.dll not found: {dllPath}");
await using var client = await McpClient.CreateAsync(
new StdioClientTransport(new StdioClientTransportOptions
{
Command = "dotnet",
Arguments = [dllPath],
+ EnvironmentVariables = TestProcessEnvironment.WithoutCodeCoverage(),
}),
cancellationToken: cts.Token);
// If we get here, the MCP handshake completed successfully.
// Verify the server reports its tools.
var tools = await client.ListToolsAsync(cancellationToken: cts.Token);
- Assert.NotEmpty(tools);
+ Assert.IsNotEmpty(tools);
}
private static async Task<(int ExitCode, string Stdout, string Stderr)> RunMcpAsync(
@@ -80,6 +82,7 @@ public async Task NoArgs_StartsServerAndCompletesHandshake()
RedirectStandardOutput = true,
RedirectStandardError = true,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
var stdout = await process.StandardOutput.ReadToEndAsync();
@@ -96,35 +99,32 @@ public async Task NoArgs_StartsServerAndCompletesHandshake()
/// the orphaned process's read() on the terminal returns EIO → IOException.
/// Regression test for https://github.com/willibrandon/dotsider/issues/108.
///
- [Fact]
+ [TestMethod]
+ [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
public async Task CtrlC_InTerminal_ShutsDownWithoutTransportException()
{
- if (OperatingSystem.IsWindows())
- return; // Windows console handles Ctrl+C cleanly; this is a macOS/Linux issue
-
var repoRoot = FindRepoRoot();
var dotsiderExe = Path.Combine(
repoRoot, "src", "Dotsider", "bin", s_buildConfig, "net10.0", "dotsider");
var mcpDir = Path.Combine(
repoRoot, "src", "Dotsider.Mcp", "bin", s_buildConfig, "net10.0");
- Assert.True(File.Exists(dotsiderExe), $"dotsider not found: {dotsiderExe}");
- Assert.True(File.Exists(Path.Combine(mcpDir, "dotsider-mcp")),
+ Assert.IsTrue(File.Exists(dotsiderExe), $"dotsider not found: {dotsiderExe}");
+ Assert.IsTrue(File.Exists(Path.Combine(mcpDir, "dotsider-mcp")),
$"dotsider-mcp not found in: {mcpDir}");
// Start an interactive shell in the PTY (bash becomes session leader).
// This mirrors the real user experience: shell → dotsider → dotsider-mcp.
- var env = new Dictionary
- {
- ["PATH"] = $"{mcpDir}:{Environment.GetEnvironmentVariable("PATH")}"
- };
+ var env = TestProcessEnvironment.WithoutCodeCoverageStringValues();
+ env["PATH"] = $"{mcpDir}:{Environment.GetEnvironmentVariable("PATH")}";
await using var pty = new Hex1bTerminalChildProcess(
"/bin/bash", ["--norc", "--noprofile"],
environment: env,
+ inheritEnvironment: false,
initialWidth: 160, initialHeight: 24);
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
await pty.StartAsync(ct);
var output = new StringBuilder();
diff --git a/tests/Dotsider.Mcp.Tests/McpServerTestBase.cs b/tests/Dotsider.Mcp.Tests/McpServerTestBase.cs
index ae6ef635..1c2ee9bf 100644
--- a/tests/Dotsider.Mcp.Tests/McpServerTestBase.cs
+++ b/tests/Dotsider.Mcp.Tests/McpServerTestBase.cs
@@ -120,6 +120,7 @@ protected async Task CreateClientAsync(McpClientOptions? clientOption
///
public virtual async ValueTask DisposeAsync()
{
+ GC.SuppressFinalize(this);
await _cts.CancelAsync();
_clientToServerPipe.Writer.Complete();
@@ -143,6 +144,5 @@ public virtual async ValueTask DisposeAsync()
_cts.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Mcp.Tests/MetadataToolsTests.cs b/tests/Dotsider.Mcp.Tests/MetadataToolsTests.cs
index 2c9ae66b..f212d378 100644
--- a/tests/Dotsider.Mcp.Tests/MetadataToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/MetadataToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class MetadataToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class MetadataToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// get_pe_headers returns parsed PE header info without errors for a valid assembly.
///
- [Fact]
+ [TestMethod]
public async Task GetPeHeaders_ValidAssembly_ReturnsHeaders()
{
await StartServerAsync();
@@ -22,18 +24,18 @@ public async Task GetPeHeaders_ValidAssembly_ReturnsHeaders()
var result = await client.CallToolAsync(
"get_pe_headers",
- new Dictionary { ["assemblyPath"] = samples.HelloWorldDll },
+ new Dictionary { ["assemblyPath"] = Samples.HelloWorldDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.DoesNotContain("Error", text);
}
///
/// get_clr_header returns CLR directory info without errors for a managed assembly.
///
- [Fact]
+ [TestMethod]
public async Task GetClrHeader_ValidAssembly_ReturnsClrInfo()
{
await StartServerAsync();
@@ -41,18 +43,18 @@ public async Task GetClrHeader_ValidAssembly_ReturnsClrInfo()
var result = await client.CallToolAsync(
"get_clr_header",
- new Dictionary { ["assemblyPath"] = samples.HelloWorldDll },
+ new Dictionary { ["assemblyPath"] = Samples.HelloWorldDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.DoesNotContain("Error", text);
}
///
/// get_sections enumerates the PE section table as a non-empty JSON array.
///
- [Fact]
+ [TestMethod]
public async Task GetSections_ValidAssembly_ReturnsSections()
{
await StartServerAsync();
@@ -60,19 +62,19 @@ public async Task GetSections_ValidAssembly_ReturnsSections()
var result = await client.CallToolAsync(
"get_sections",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var sections = JsonSerializer.Deserialize(text);
- Assert.True(sections.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, sections.GetArrayLength());
}
///
/// get_custom_attributes returns at least one attribute for a real library.
///
- [Fact]
+ [TestMethod]
public async Task GetCustomAttributes_ValidAssembly_ReturnsAttributes()
{
await StartServerAsync();
@@ -80,19 +82,19 @@ public async Task GetCustomAttributes_ValidAssembly_ReturnsAttributes()
var result = await client.CallToolAsync(
"get_custom_attributes",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var attrs = JsonSerializer.Deserialize(text);
- Assert.True(attrs.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, attrs.GetArrayLength());
}
///
/// By default, compiler-generated attributes like Nullable/CompilerGenerated are filtered out.
///
- [Fact]
+ [TestMethod]
public async Task GetCustomAttributes_DefaultFiltering_ExcludesCompilerGenerated()
{
await StartServerAsync();
@@ -100,11 +102,11 @@ public async Task GetCustomAttributes_DefaultFiltering_ExcludesCompilerGenerated
var result = await client.CallToolAsync(
"get_custom_attributes",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.DoesNotContain("CompilerGeneratedAttribute", text);
Assert.DoesNotContain("NullableContextAttribute", text);
Assert.DoesNotContain("NullableAttribute", text);
@@ -114,7 +116,7 @@ public async Task GetCustomAttributes_DefaultFiltering_ExcludesCompilerGenerated
///
/// Opting in via includeCompilerGenerated re-exposes the noisy compiler attributes.
///
- [Fact]
+ [TestMethod]
public async Task GetCustomAttributes_IncludeCompilerGenerated_ReturnsAll()
{
await StartServerAsync();
@@ -124,13 +126,13 @@ public async Task GetCustomAttributes_IncludeCompilerGenerated_ReturnsAll()
"get_custom_attributes",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["includeCompilerGenerated"] = true
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
// With includeCompilerGenerated=true, these should be present
Assert.Contains("CompilerGeneratedAttribute", text);
}
@@ -138,7 +140,7 @@ public async Task GetCustomAttributes_IncludeCompilerGenerated_ReturnsAll()
///
/// The advertised tool schema surfaces the includeCompilerGenerated parameter to clients.
///
- [Fact]
+ [TestMethod]
public async Task GetCustomAttributes_ToolSchema_IncludesFilterParameter()
{
await StartServerAsync();
@@ -153,7 +155,7 @@ public async Task GetCustomAttributes_ToolSchema_IncludesFilterParameter()
///
/// get_resources always returns a JSON array, even for assemblies with no embedded resources.
///
- [Fact]
+ [TestMethod]
public async Task GetResources_ValidAssembly_ReturnsResourceList()
{
await StartServerAsync();
@@ -161,19 +163,19 @@ public async Task GetResources_ValidAssembly_ReturnsResourceList()
var result = await client.CallToolAsync(
"get_resources",
- new Dictionary { ["assemblyPath"] = samples.HelloWorldDll },
+ new Dictionary { ["assemblyPath"] = Samples.HelloWorldDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.Equal(JsonValueKind.Array,
+ Assert.IsNotNull(text);
+ Assert.AreEqual(JsonValueKind.Array,
JsonSerializer.Deserialize(text).ValueKind);
}
///
/// resolve_token turns a raw metadata token into a human-readable member name.
///
- [Fact]
+ [TestMethod]
public async Task ResolveToken_ValidToken_ReturnsResolvedName()
{
await StartServerAsync();
@@ -184,15 +186,15 @@ public async Task ResolveToken_ValidToken_ReturnsResolvedName()
"resolve_token",
new Dictionary
{
- ["assemblyPath"] = samples.HelloWorldDll,
+ ["assemblyPath"] = Samples.HelloWorldDll,
["token"] = 0x02000002
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("resolved", out var resolved));
- Assert.False(string.IsNullOrEmpty(resolved.GetString()));
+ Assert.IsTrue(json.TryGetProperty("resolved", out var resolved));
+ Assert.IsFalse(string.IsNullOrEmpty(resolved.GetString()));
}
}
diff --git a/tests/Dotsider.Mcp.Tests/NativeAotToolsTests.cs b/tests/Dotsider.Mcp.Tests/NativeAotToolsTests.cs
index 3a22abd5..6f1b9e31 100644
--- a/tests/Dotsider.Mcp.Tests/NativeAotToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/NativeAotToolsTests.cs
@@ -5,60 +5,65 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests for Native AOT-specific MCP tools.
///
-[Collection("SampleAssemblies")]
-public class NativeAotToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class NativeAotToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
/// get_native_aot_info reports Native AOT identity and sidecar availability.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeAotInfo_NativeAot_ReturnsSummary()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"get_native_aot_info",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("nativeAot", json.GetProperty("binaryKind").GetString());
- Assert.True(json.GetProperty("readyToRunSections").GetInt32() > 0);
- Assert.True(json.GetProperty("recoveredTypes").GetInt32() > 0);
- Assert.True(json.TryGetProperty("hasMstat", out _));
+ Assert.AreEqual("nativeAot", json.GetProperty("binaryKind").GetString());
+ Assert.IsGreaterThan(0, json.GetProperty("readyToRunSections").GetInt32());
+ Assert.IsGreaterThan(0, json.GetProperty("recoveredTypes").GetInt32());
+ Assert.IsTrue(json.TryGetProperty("hasMstat", out _));
}
/// list_native_aot_sections returns Native AOT RTR module sections.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListNativeAotSections_NativeAot_ReturnsRtrSections()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"list_native_aot_sections",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("sectionCount").GetInt32() > 0);
+ Assert.IsGreaterThan(0, json.GetProperty("sectionCount").GetInt32());
var first = json.GetProperty("sections").EnumerateArray().First();
- Assert.True(first.TryGetProperty("sectionId", out _));
- Assert.True(first.TryGetProperty("address", out _));
+ Assert.IsTrue(first.TryGetProperty("sectionId", out _));
+ Assert.IsTrue(first.TryGetProperty("address", out _));
}
/// get_native_aot_size_contributors returns normalized mstat contributors.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeAotSizeContributors_NativeAot_ReturnsTopMstatRows()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -67,30 +72,31 @@ public async Task GetNativeAotSizeContributors_NativeAot_ReturnsTopMstatRows()
"get_native_aot_size_contributors",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["section"] = "Method",
["topN"] = 5
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("totalMatches").GetInt32() > 0);
+ Assert.IsGreaterThan(0, json.GetProperty("totalMatches").GetInt32());
var rows = json.GetProperty("contributors");
- Assert.True(rows.GetArrayLength() > 0);
- Assert.All(rows.EnumerateArray(), row =>
+ Assert.IsGreaterThan(0, rows.GetArrayLength());
+ TestAssert.All(rows.EnumerateArray(), row =>
{
- Assert.Equal("method", row.GetProperty("section").GetString());
- Assert.True(row.GetProperty("size").GetInt64() > 0);
+ Assert.AreEqual("method", row.GetProperty("section").GetString());
+ Assert.IsGreaterThan(0, row.GetProperty("size").GetInt64());
});
}
/// explain_native_aot_size returns a DGML root chain for a real contributor.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ExplainNativeAotSize_NativeAot_ReturnsRootChain()
{
- Assert.SkipWhen(samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -99,36 +105,36 @@ public async Task ExplainNativeAotSize_NativeAot_ReturnsRootChain()
"get_native_aot_size_contributors",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["section"] = "Method",
["topN"] = 20
},
cancellationToken: TestCancellationToken);
var contributorText = GetTextContent(contributors);
- Assert.NotNull(contributorText);
+ Assert.IsNotNull(contributorText);
var contributorJson = JsonSerializer.Deserialize(contributorText);
var target = contributorJson.GetProperty("contributors")
.EnumerateArray()
.First(e => e.GetProperty("nodeNames").GetArrayLength() > 0)
.GetProperty("fullPath")
.GetString();
- Assert.NotNull(target);
+ Assert.IsNotNull(target);
var result = await client.CallToolAsync(
"explain_native_aot_size",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["target"] = target
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("resolved", json.GetProperty("outcome").GetString());
+ Assert.AreEqual("resolved", json.GetProperty("outcome").GetString());
var chains = json.GetProperty("contributor").GetProperty("whyChains");
- Assert.True(chains.GetArrayLength() > 0);
+ Assert.IsGreaterThan(0, chains.GetArrayLength());
}
}
diff --git a/tests/Dotsider.Mcp.Tests/NavigationToolsTests.cs b/tests/Dotsider.Mcp.Tests/NavigationToolsTests.cs
index 871432a2..85eddd4c 100644
--- a/tests/Dotsider.Mcp.Tests/NavigationToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/NavigationToolsTests.cs
@@ -11,9 +11,11 @@ namespace Dotsider.Mcp.Tests;
/// wired to real analyzers. These exercise the actual currentViewProvider and
/// getState paths that regressed in diff/nuget modes.
///
-[Collection("SampleAssemblies")]
-public class NavigationToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class NavigationToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private readonly List _disposables = [];
// --- Diff mode: real listener with real AssemblyAnalyzers ---
@@ -21,14 +23,14 @@ public class NavigationToolsTests(SampleAssemblyFixture samples) : McpServerTest
///
/// Diff-mode sessions expose both the active tab and the diff filter mode via get_current_view.
///
- [Fact]
+ [TestMethod]
public async Task GetCurrentView_DiffMode_ReturnsTabAndFilterMode()
{
var currentTab = 2;
var filterMode = DiffFilterMode.AddedOnly;
await using var listener = CreateDiffListener(
- samples.RichLibraryDll, samples.RichLibraryV2Dll,
+ Samples.RichLibraryDll, Samples.RichLibraryV2Dll,
() => currentTab, () => filterMode);
await StartServerAsync();
@@ -40,21 +42,21 @@ public async Task GetCurrentView_DiffMode_ReturnsTabAndFilterMode()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var doc = JsonDocument.Parse(text!);
- Assert.Equal("diff", doc.RootElement.GetProperty("mode").GetString());
- Assert.Equal(3, doc.RootElement.GetProperty("tab").GetInt32());
- Assert.Equal("addedOnly", doc.RootElement.GetProperty("filterMode").GetString());
+ Assert.AreEqual("diff", doc.RootElement.GetProperty("mode").GetString());
+ Assert.AreEqual(3, doc.RootElement.GetProperty("tab").GetInt32());
+ Assert.AreEqual("addedOnly", doc.RootElement.GetProperty("filterMode").GetString());
}
///
/// When browsing a nupkg without a selected DLL, isBrowsingPackage is reported as true.
///
- [Fact]
+ [TestMethod]
public async Task GetCurrentView_NugetMode_BrowsingPackage()
{
- await using var listener = CreateNugetListener(samples.RichLibraryNupkg);
+ await using var listener = CreateNugetListener(Samples.RichLibraryNupkg);
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -65,21 +67,21 @@ public async Task GetCurrentView_NugetMode_BrowsingPackage()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var doc = JsonDocument.Parse(text!);
- Assert.Equal("nuget", doc.RootElement.GetProperty("mode").GetString());
- Assert.True(doc.RootElement.GetProperty("isBrowsingPackage").GetBoolean());
+ Assert.AreEqual("nuget", doc.RootElement.GetProperty("mode").GetString());
+ Assert.IsTrue(doc.RootElement.GetProperty("isBrowsingPackage").GetBoolean());
}
///
/// Once a DLL is selected inside a nupkg, isBrowsingPackage flips false and the tab is reported.
///
- [Fact]
+ [TestMethod]
public async Task GetCurrentView_NugetMode_DllSelected()
{
await using var listener = CreateNugetListener(
- samples.RichLibraryNupkg,
+ Samples.RichLibraryNupkg,
selectDll: true, selectedDllTab: 3);
await StartServerAsync();
@@ -91,22 +93,22 @@ public async Task GetCurrentView_NugetMode_DllSelected()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var doc = JsonDocument.Parse(text!);
- Assert.Equal("nuget", doc.RootElement.GetProperty("mode").GetString());
- Assert.False(doc.RootElement.GetProperty("isBrowsingPackage").GetBoolean());
- Assert.Equal(4, doc.RootElement.GetProperty("tab").GetInt32());
+ Assert.AreEqual("nuget", doc.RootElement.GetProperty("mode").GetString());
+ Assert.IsFalse(doc.RootElement.GetProperty("isBrowsingPackage").GetBoolean());
+ Assert.AreEqual(4, doc.RootElement.GetProperty("tab").GetInt32());
}
///
/// Diff mode has no single DotsiderState, so navigate_to fails with a clear message.
///
- [Fact]
+ [TestMethod]
public async Task NavigateTo_DiffMode_FailsBecauseNoState()
{
await using var listener = CreateDiffListener(
- samples.RichLibraryDll, samples.RichLibraryV2Dll,
+ Samples.RichLibraryDll, Samples.RichLibraryV2Dll,
() => 0, () => DiffFilterMode.All);
await StartServerAsync();
@@ -119,17 +121,17 @@ public async Task NavigateTo_DiffMode_FailsBecauseNoState()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("No assembly is loaded", text);
}
///
/// Without a selected DLL, NuGet mode cannot satisfy navigate_to and returns an error.
///
- [Fact]
+ [TestMethod]
public async Task NavigateTo_NugetMode_NoDllSelected_Fails()
{
- await using var listener = CreateNugetListener(samples.RichLibraryNupkg);
+ await using var listener = CreateNugetListener(Samples.RichLibraryNupkg);
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -140,7 +142,7 @@ public async Task NavigateTo_NugetMode_NoDllSelected_Fails()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("No assembly is loaded", text);
}
@@ -149,14 +151,14 @@ public async Task NavigateTo_NugetMode_NoDllSelected_Fails()
///
/// Against a live headless NuGet TUI, navigate_to updates the current tab and get_current_view reflects it.
///
- [Fact]
+ [TestMethod]
public async Task NavigateTo_LiveNuget_OpenedDll_ChangesTabAndVerifiesView()
{
var ct = TestCancellationToken;
// Start a headless NuGet TUI with a real listener, exactly like Program.RunTui
var (app, nugetState, listener) = await StartLiveNugetTuiAsync(
- samples.RichLibraryNupkg, ct);
+ Samples.RichLibraryNupkg, ct);
_disposables.Add(listener);
// Open the first DLL in the package
@@ -167,7 +169,7 @@ public async Task NavigateTo_LiveNuget_OpenedDll_ChangesTabAndVerifiesView()
nugetState.IsBrowsingPackage = false;
app.Invalidate();
- Assert.Equal(TabId.General, nugetState.SelectedDllState.CurrentTab);
+ Assert.AreEqual(TabId.General, nugetState.SelectedDllState.CurrentTab);
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -182,7 +184,7 @@ public async Task NavigateTo_LiveNuget_OpenedDll_ChangesTabAndVerifiesView()
},
cancellationToken: ct);
var navText = GetTextContent(navResult);
- Assert.NotNull(navText);
+ Assert.IsNotNull(navText);
Assert.DoesNotContain("Error", navText);
// Wait for the render loop to drain the mutation queue
@@ -190,7 +192,7 @@ await WaitUntilAsync(
() => nugetState.SelectedDllState!.CurrentTab == TabId.Strings,
TimeSpan.FromSeconds(5));
- Assert.Equal(TabId.Strings, nugetState.SelectedDllState.CurrentTab);
+ Assert.AreEqual(TabId.Strings, nugetState.SelectedDllState.CurrentTab);
// Verify get_current_view reflects the navigated tab
var viewResult = await client.CallToolAsync(
@@ -198,12 +200,12 @@ await WaitUntilAsync(
new Dictionary { ["sessionId"] = listener.Pid },
cancellationToken: ct);
var viewText = GetTextContent(viewResult);
- Assert.NotNull(viewText);
+ Assert.IsNotNull(viewText);
var doc = JsonDocument.Parse(viewText!);
- Assert.Equal("nuget", doc.RootElement.GetProperty("mode").GetString());
- Assert.False(doc.RootElement.GetProperty("isBrowsingPackage").GetBoolean());
- Assert.Equal(TabId.Strings + 1, doc.RootElement.GetProperty("tab").GetInt32());
+ Assert.AreEqual("nuget", doc.RootElement.GetProperty("mode").GetString());
+ Assert.IsFalse(doc.RootElement.GetProperty("isBrowsingPackage").GetBoolean());
+ Assert.AreEqual(TabId.Strings + 1, doc.RootElement.GetProperty("tab").GetInt32());
}
// --- Disposal ---
@@ -214,7 +216,6 @@ await WaitUntilAsync(
public override async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
-
foreach (var d in _disposables)
await d.DisposeAsync();
_disposables.Clear();
@@ -409,6 +410,7 @@ private sealed class RealListenerHandle(
public async ValueTask DisposeAsync()
{
+ GC.SuppressFinalize(this);
await listener.DisposeAsync();
foreach (var d in owned)
d.Dispose();
diff --git a/tests/Dotsider.Mcp.Tests/NuGetToolsTests.cs b/tests/Dotsider.Mcp.Tests/NuGetToolsTests.cs
index 850bb9d8..adc6ff35 100644
--- a/tests/Dotsider.Mcp.Tests/NuGetToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/NuGetToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class NuGetToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class NuGetToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// analyze_nupkg surfaces packageId and packageVersion from the .nuspec manifest.
///
- [Fact]
+ [TestMethod]
public async Task AnalyzeNupkg_RichLibrary_ReturnsPackageMetadata()
{
await StartServerAsync();
@@ -22,20 +24,20 @@ public async Task AnalyzeNupkg_RichLibrary_ReturnsPackageMetadata()
var result = await client.CallToolAsync(
"analyze_nupkg",
- new Dictionary { ["nupkgPath"] = samples.RichLibraryNupkg },
+ new Dictionary { ["nupkgPath"] = Samples.RichLibraryNupkg },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("RichLibrary", json.GetProperty("packageId").GetString());
- Assert.Equal("2.5.1", json.GetProperty("packageVersion").GetString());
+ Assert.AreEqual("RichLibrary", json.GetProperty("packageId").GetString());
+ Assert.AreEqual("2.5.1", json.GetProperty("packageVersion").GetString());
}
///
/// analyze_nupkg enumerates the DLLs bundled in the lib/ folders of the package.
///
- [Fact]
+ [TestMethod]
public async Task AnalyzeNupkg_RichLibrary_ListsDlls()
{
await StartServerAsync();
@@ -43,13 +45,13 @@ public async Task AnalyzeNupkg_RichLibrary_ListsDlls()
var result = await client.CallToolAsync(
"analyze_nupkg",
- new Dictionary { ["nupkgPath"] = samples.RichLibraryNupkg },
+ new Dictionary { ["nupkgPath"] = Samples.RichLibraryNupkg },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("dllFiles", out var dlls));
- Assert.True(dlls.GetArrayLength() > 0);
+ Assert.IsTrue(json.TryGetProperty("dllFiles", out var dlls));
+ Assert.IsGreaterThan(0, dlls.GetArrayLength());
}
}
diff --git a/tests/Dotsider.Mcp.Tests/PromptTests.cs b/tests/Dotsider.Mcp.Tests/PromptTests.cs
index 1c112cff..42877913 100644
--- a/tests/Dotsider.Mcp.Tests/PromptTests.cs
+++ b/tests/Dotsider.Mcp.Tests/PromptTests.cs
@@ -3,12 +3,13 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests for the MCP prompts catalog and parameterized prompt retrieval.
///
+[TestClass]
public class PromptTests : McpServerTestBase
{
///
/// ListPrompts returns every prompt the server advertises by canonical name.
///
- [Fact]
+ [TestMethod]
public async Task ListPrompts_ReturnsAllRegisteredPrompts()
{
await StartServerAsync();
@@ -16,7 +17,7 @@ public async Task ListPrompts_ReturnsAllRegisteredPrompts()
var prompts = await client.ListPromptsAsync(cancellationToken: TestCancellationToken);
- Assert.True(prompts.Count >= 4);
+ Assert.IsGreaterThanOrEqualTo(4, prompts.Count);
var names = prompts.Select(p => p.Name).ToList();
Assert.Contains("security_audit", names);
Assert.Contains("api_review", names);
@@ -27,19 +28,20 @@ public async Task ListPrompts_ReturnsAllRegisteredPrompts()
///
/// The prompt catalog includes the bundle_analysis prompt registered alongside bundle tools.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListPrompts_IncludesBundleAnalysis()
{
await StartServerAsync();
await using var client = await CreateClientAsync();
var prompts = await client.ListPromptsAsync(cancellationToken: TestCancellationToken);
- Assert.Contains(prompts, p => p.Name == "bundle_analysis");
+ Assert.Contains(p => p.Name == "bundle_analysis", prompts);
}
///
/// security_audit materializes at least one message when given an assembly path.
///
- [Fact]
+ [TestMethod]
public async Task GetPrompt_SecurityAudit_ReturnsPromptContent()
{
await StartServerAsync();
@@ -50,14 +52,14 @@ public async Task GetPrompt_SecurityAudit_ReturnsPromptContent()
new Dictionary { ["assemblyPath"] = "/test/path.dll" },
cancellationToken: TestCancellationToken);
- Assert.NotNull(result);
- Assert.True(result.Messages.Count > 0);
+ Assert.IsNotNull(result);
+ Assert.IsGreaterThan(0, result.Messages.Count);
}
///
/// breaking_change_detection accepts both old and new assembly paths and produces messages.
///
- [Fact]
+ [TestMethod]
public async Task GetPrompt_BreakingChangeDetection_AcceptsTwoPaths()
{
await StartServerAsync();
@@ -72,8 +74,8 @@ public async Task GetPrompt_BreakingChangeDetection_AcceptsTwoPaths()
},
cancellationToken: TestCancellationToken);
- Assert.NotNull(result);
- Assert.True(result.Messages.Count > 0);
+ Assert.IsNotNull(result);
+ Assert.IsGreaterThan(0, result.Messages.Count);
}
///
@@ -83,7 +85,7 @@ public async Task GetPrompt_BreakingChangeDetection_AcceptsTwoPaths()
/// models don't expect the tool to filter them. Asserting content (not just registration)
/// catches future prompt/tool contract drift that was the original issue behind #149.
///
- [Fact]
+ [TestMethod]
public async Task GetPrompt_DependencyHealth_ContentMentionsTransitiveClosureAndFrameworkInclusion()
{
await StartServerAsync();
@@ -94,7 +96,7 @@ public async Task GetPrompt_DependencyHealth_ContentMentionsTransitiveClosureAnd
new Dictionary { ["assemblyPath"] = "/test/path.dll" },
cancellationToken: TestCancellationToken);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
var content = string.Join("\n", result.Messages.Select(m => m.Content.ToString()));
Assert.Contains("transitive", content, StringComparison.OrdinalIgnoreCase);
Assert.Contains("diamond", content, StringComparison.OrdinalIgnoreCase);
diff --git a/tests/Dotsider.Mcp.Tests/ReadyToRunToolsTests.cs b/tests/Dotsider.Mcp.Tests/ReadyToRunToolsTests.cs
index 2d434811..32b2f3c1 100644
--- a/tests/Dotsider.Mcp.Tests/ReadyToRunToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/ReadyToRunToolsTests.cs
@@ -6,16 +6,19 @@ namespace Dotsider.Mcp.Tests;
/// ambiguous-name error path) and get_native_disassembly made R2R-method-aware — a multi-range
/// method resolves to the method and renders all its ranges rather than a false per-range ambiguity.
///
-[Collection("SampleAssemblies")]
-public class ReadyToRunToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class ReadyToRunToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private const string SkipReason = "ReadyToRun crossgen2 publish did not run on this leg.";
/// correlate_r2r_method resolves a unique method to its report with IL and native code.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CorrelateR2rMethod_ByName_ReturnsReport()
{
- Assert.SkipWhen(samples.ReadyToRunConsoleDll is null, SkipReason);
+ TestSkip.When(Samples.ReadyToRunConsoleDll is null, SkipReason);
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -25,13 +28,13 @@ public async Task CorrelateR2rMethod_ByName_ReturnsReport()
new Dictionary
{
["methodOrAddress"] = "Greeter.get_Name",
- ["assemblyPath"] = samples.ReadyToRunConsoleDll,
+ ["assemblyPath"] = Samples.ReadyToRunConsoleDll,
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text!.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text!.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
Assert.Contains("get_Name", text);
// The JSON report carries the honest native-availability state (camelCase enum) and the IL.
Assert.Contains("precompiled", text, StringComparison.OrdinalIgnoreCase);
@@ -39,10 +42,11 @@ public async Task CorrelateR2rMethod_ByName_ReturnsReport()
}
/// correlate_r2r_method reports an overloaded name as an ambiguity, never first-match.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CorrelateR2rMethod_Overloaded_ReturnsAmbiguity()
{
- Assert.SkipWhen(samples.ReadyToRunConsoleDll is null, SkipReason);
+ TestSkip.When(Samples.ReadyToRunConsoleDll is null, SkipReason);
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -52,20 +56,21 @@ public async Task CorrelateR2rMethod_Overloaded_ReturnsAmbiguity()
new Dictionary
{
["methodOrAddress"] = "Greet",
- ["assemblyPath"] = samples.ReadyToRunConsoleDll,
+ ["assemblyPath"] = Samples.ReadyToRunConsoleDll,
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("ambiguous", text!.ToLowerInvariant());
}
/// get_native_disassembly renders every range of a multi-range R2R method (no false ambiguity).
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeDisassembly_ReadyToRun_MultiRange_RendersAllRanges()
{
- Assert.SkipWhen(samples.ReadyToRunConsoleDll is null, SkipReason);
+ TestSkip.When(Samples.ReadyToRunConsoleDll is null, SkipReason);
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -75,12 +80,12 @@ public async Task GetNativeDisassembly_ReadyToRun_MultiRange_RendersAllRanges()
new Dictionary
{
["symbolName"] = "MoveNext",
- ["assemblyPath"] = samples.ReadyToRunConsoleDll,
+ ["assemblyPath"] = Samples.ReadyToRunConsoleDll,
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
// Not an ambiguity error, and the import resolver named a cross-call target in the body.
Assert.DoesNotContain("ambiguous", text!.ToLowerInvariant());
Assert.Contains("WriteLine", text);
diff --git a/tests/Dotsider.Mcp.Tests/RemoteDotsiderTargetVersionTests.cs b/tests/Dotsider.Mcp.Tests/RemoteDotsiderTargetVersionTests.cs
index fc33f2c4..b17e7e49 100644
--- a/tests/Dotsider.Mcp.Tests/RemoteDotsiderTargetVersionTests.cs
+++ b/tests/Dotsider.Mcp.Tests/RemoteDotsiderTargetVersionTests.cs
@@ -6,12 +6,14 @@ namespace Dotsider.Mcp.Tests;
/// Tests that correctly rejects old or
/// mismatched server responses.
///
+[TestClass]
public class RemoteDotsiderTargetVersionTests
{
///
/// A pre-versioned server response is rejected because the protocol contract requires a 'v' field.
///
- [Fact(Timeout = 10_000)]
+ [TestMethod]
+ [Timeout(10_000, CooperativeCancellation = true)]
public async Task RejectsOldServerResponse()
{
var socketPath = GetUniqueSocketPath();
@@ -22,16 +24,17 @@ public async Task RejectsOldServerResponse()
var target = new RemoteDotsiderTarget(socketPath);
var response = await target.SendAsync(
new DotsiderRequest { Method = "assembly-info" },
- TestContext.Current.CancellationToken);
+ CancellationToken.None);
- Assert.False(response.Success);
+ Assert.IsFalse(response.Success);
Assert.Contains("server response", response.Error!, StringComparison.OrdinalIgnoreCase);
}
///
/// Rejects a remote server whose advertised version differs from the client target.
///
- [Fact(Timeout = 10_000)]
+ [TestMethod]
+ [Timeout(10_000, CooperativeCancellation = true)]
public async Task RejectsWrongServerVersion()
{
var socketPath = GetUniqueSocketPath();
@@ -42,9 +45,9 @@ public async Task RejectsWrongServerVersion()
var target = new RemoteDotsiderTarget(socketPath);
var response = await target.SendAsync(
new DotsiderRequest { Method = "assembly-info" },
- TestContext.Current.CancellationToken);
+ CancellationToken.None);
- Assert.False(response.Success);
+ Assert.IsFalse(response.Success);
Assert.Contains("version mismatch", response.Error!, StringComparison.OrdinalIgnoreCase);
}
@@ -54,6 +57,6 @@ private static string GetUniqueSocketPath()
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".dotsider", "sockets");
Directory.CreateDirectory(dir);
- return Path.Combine(dir, $"test-{Random.Shared.Next(100_000, 999_999)}.dotsider.socket");
+ return Path.Combine(dir, $"test-{TestSocketIds.NextPid()}.dotsider.socket");
}
}
diff --git a/tests/Dotsider.Mcp.Tests/RuntimeToolsTests.cs b/tests/Dotsider.Mcp.Tests/RuntimeToolsTests.cs
index fd30c8ff..3d549f48 100644
--- a/tests/Dotsider.Mcp.Tests/RuntimeToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/RuntimeToolsTests.cs
@@ -5,13 +5,16 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests for runtime discovery and assembly resolution MCP tools.
///
-[Collection("SampleAssemblies")]
-public class RuntimeToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class RuntimeToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// find_framework_assembly resolves System.Runtime to a path inside the shared framework pack.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task FindFrameworkAssembly_SystemRuntime_ReturnsPathAndPack()
{
await StartServerAsync();
@@ -22,16 +25,17 @@ public async Task FindFrameworkAssembly_SystemRuntime_ReturnsPathAndPack()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.NotEmpty(json.GetProperty("path").GetString()!);
- Assert.Equal("Microsoft.NETCore.App", json.GetProperty("runtimePack").GetString());
+ Assert.IsNotEmpty(json.GetProperty("path").GetString()!);
+ Assert.AreEqual("Microsoft.NETCore.App", json.GetProperty("runtimePack").GetString());
}
///
/// Unknown framework assembly names yield a serialized null rather than an error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task FindFrameworkAssembly_Nonexistent_ReturnsNull()
{
await StartServerAsync();
@@ -42,14 +46,15 @@ public async Task FindFrameworkAssembly_Nonexistent_ReturnsNull()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.Equal("null", text);
+ Assert.IsNotNull(text);
+ Assert.AreEqual("null", text);
}
///
/// resolve_assembly routes a shared framework dependency to a file-kind resolution.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ResolveAssembly_Direct_SharedFramework()
{
await StartServerAsync();
@@ -59,20 +64,21 @@ public async Task ResolveAssembly_Direct_SharedFramework()
new Dictionary
{
["assemblyName"] = "System.Runtime",
- ["assemblyPath"] = samples.RichLibraryDll
+ ["assemblyPath"] = Samples.RichLibraryDll
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("file", json.GetProperty("kind").GetString());
+ Assert.AreEqual("file", json.GetProperty("kind").GetString());
}
///
/// Tool registry advertises the two runtime discovery tools by canonical name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListTools_IncludesRuntimeTools()
{
await StartServerAsync();
diff --git a/tests/Dotsider.Mcp.Tests/SampleAssemblyCollection.cs b/tests/Dotsider.Mcp.Tests/SampleAssemblyCollection.cs
deleted file mode 100644
index 7f3e2301..00000000
--- a/tests/Dotsider.Mcp.Tests/SampleAssemblyCollection.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Dotsider.Mcp.Tests;
-
-///
-/// Collection definition that binds to tests opting into the shared fixture.
-///
-[CollectionDefinition("SampleAssemblies")]
-public class SampleAssemblyCollection : ICollectionFixture;
diff --git a/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs b/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs
index d7f05c95..601bdaa9 100644
--- a/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs
+++ b/tests/Dotsider.Mcp.Tests/SampleAssemblyFixture.cs
@@ -4,9 +4,9 @@
namespace Dotsider.Mcp.Tests;
///
-/// Shared xUnit fixture that builds and exposes sample assemblies used across MCP tool tests.
+/// Shared MSTest fixture that builds and exposes sample assemblies used across MCP tool tests.
///
-public class SampleAssemblyFixture : IAsyncLifetime
+internal class SampleAssemblyFixture : IAsyncDisposable
{
private string _repoRoot = null!;
@@ -177,12 +177,12 @@ public async ValueTask InitializeAsync()
"bin", "Release", tfm, "browser-wasm", "AppBundle", "_framework", "WasmConsole.wasm");
WasmConsoleWebcilWasm = File.Exists(wasmConsoleWebcil) ? wasmConsoleWebcil : null;
- Assert.True(File.Exists(HelloWorldDll), $"HelloWorld.dll not found at {HelloWorldDll}");
- Assert.True(File.Exists(HelloWorldExe), $"HelloWorld apphost not found at {HelloWorldExe}");
- Assert.True(File.Exists(RichLibraryDll), $"RichLibrary.dll not found at {RichLibraryDll}");
- Assert.True(File.Exists(RichLibraryNupkg), $"RichLibrary.nupkg not found at {RichLibraryNupkg}");
- Assert.True(File.Exists(MinimalApiDll), $"MinimalApi.dll not found at {MinimalApiDll}");
- Assert.True(File.Exists(SelfContainedConsoleExe), $"SelfContainedConsole not found at {SelfContainedConsoleExe}");
+ Assert.IsTrue(File.Exists(HelloWorldDll), $"HelloWorld.dll not found at {HelloWorldDll}");
+ Assert.IsTrue(File.Exists(HelloWorldExe), $"HelloWorld apphost not found at {HelloWorldExe}");
+ Assert.IsTrue(File.Exists(RichLibraryDll), $"RichLibrary.dll not found at {RichLibraryDll}");
+ Assert.IsTrue(File.Exists(RichLibraryNupkg), $"RichLibrary.nupkg not found at {RichLibraryNupkg}");
+ Assert.IsTrue(File.Exists(MinimalApiDll), $"MinimalApi.dll not found at {MinimalApiDll}");
+ Assert.IsTrue(File.Exists(SelfContainedConsoleExe), $"SelfContainedConsole not found at {SelfContainedConsoleExe}");
if (!File.Exists(NativeAotConsoleExe))
NativeAotConsoleExe = null;
@@ -276,6 +276,7 @@ private async Task BuildProject(string relativePath)
RedirectStandardOutput = true,
RedirectStandardError = true,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
var stdout = await process.StandardOutput.ReadToEndAsync();
@@ -338,6 +339,7 @@ private async Task PublishSelfContainedProject(string relativePath)
WorkingDirectory = projectDir,
UseShellExecute = false,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
await process.WaitForExitAsync();
@@ -390,6 +392,7 @@ private async Task PublishReadyToRunProject(string relativePath)
RedirectStandardOutput = true,
RedirectStandardError = true,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
_ = await process.StandardOutput.ReadToEndAsync();
_ = await process.StandardError.ReadToEndAsync();
@@ -445,6 +448,7 @@ private async Task PublishWasmProject(string relativePath)
RedirectStandardOutput = true,
RedirectStandardError = true,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
_ = await process.StandardOutput.ReadToEndAsync();
_ = await process.StandardError.ReadToEndAsync();
@@ -513,6 +517,7 @@ private async Task PublishNativeAotProject(string relativePath)
// is not resolved from the current directory inside FOR /F).
// Clear it for this process only.
psi.Environment.Remove("NoDefaultCurrentDirectoryInExePath");
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
await process.WaitForExitAsync();
diff --git a/tests/Dotsider.Mcp.Tests/SampleAssemblyHost.cs b/tests/Dotsider.Mcp.Tests/SampleAssemblyHost.cs
new file mode 100644
index 00000000..6ded0471
--- /dev/null
+++ b/tests/Dotsider.Mcp.Tests/SampleAssemblyHost.cs
@@ -0,0 +1,31 @@
+namespace Dotsider.Mcp.Tests;
+
+///
+/// MSTest assembly fixture that builds shared sample assemblies once for this test assembly.
+///
+[TestClass]
+public static class SampleAssemblyHost
+{
+ internal static SampleAssemblyFixture Instance { get; private set; } = null!;
+
+ ///
+ /// Initializes shared sample assemblies before the test assembly runs.
+ ///
+ /// The MSTest assembly initialization context.
+ [AssemblyInitialize]
+ public static async Task AssemblyInitialize(TestContext context)
+ {
+ Instance = new SampleAssemblyFixture();
+ await Instance.InitializeAsync();
+ }
+
+ ///
+ /// Cleans up shared sample assemblies after the test assembly completes.
+ ///
+ [AssemblyCleanup]
+ public static async Task AssemblyCleanup()
+ {
+ if (Instance is not null)
+ await Instance.DisposeAsync();
+ }
+}
diff --git a/tests/Dotsider.Mcp.Tests/SessionToolsTests.cs b/tests/Dotsider.Mcp.Tests/SessionToolsTests.cs
index 66069e2d..fb163bb4 100644
--- a/tests/Dotsider.Mcp.Tests/SessionToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/SessionToolsTests.cs
@@ -11,16 +11,18 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class SessionToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class SessionToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
// Use PIDs that won't collide with real processes or other test classes
private static int s_nextPid = 999_700;
///
/// discover_dotsider_sessions picks up a simulated listening instance by PID.
///
- [Fact]
+ [TestMethod]
public async Task DiscoverDotsiderSessions_FindsRunningInstance()
{
await using var socket = new TestDotsiderSocket(999_999, "/tmp/test/HelloWorld.dll");
@@ -33,22 +35,22 @@ public async Task DiscoverDotsiderSessions_FindsRunningInstance()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
// Should find our test instance in the JSON array
var sessions = JsonSerializer.Deserialize(text);
- Assert.NotNull(sessions);
+ Assert.IsNotNull(sessions);
var testSession = sessions!.FirstOrDefault(s =>
s.GetProperty("pid").GetInt32() == 999_999);
- Assert.NotEqual(default, testSession);
- Assert.Equal(999_999, testSession.GetProperty("pid").GetInt32());
+ Assert.AreNotEqual(default, testSession);
+ Assert.AreEqual(999_999, testSession.GetProperty("pid").GetInt32());
}
///
/// get_session_info combines the remote assembly-info and current-view payloads into one response.
///
- [Fact]
+ [TestMethod]
public async Task GetSessionInfo_ReturnsAssemblyAndViewData()
{
await using var socket = new TestDotsiderSocket(999_998, "/tmp/test/HelloWorld.dll");
@@ -69,11 +71,11 @@ public async Task GetSessionInfo_ReturnsAssemblyAndViewData()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var doc = JsonDocument.Parse(text!);
- Assert.True(doc.RootElement.TryGetProperty("assembly", out _));
- Assert.True(doc.RootElement.TryGetProperty("view", out _));
+ Assert.IsTrue(doc.RootElement.TryGetProperty("assembly", out _));
+ Assert.IsTrue(doc.RootElement.TryGetProperty("view", out _));
}
// --- Diff mode: real DotsiderDiagnosticsListener with real AssemblyAnalyzers ---
@@ -81,7 +83,7 @@ public async Task GetSessionInfo_ReturnsAssemblyAndViewData()
///
/// Discovery surfaces a real diff-mode listener and exposes both left/right assembly metadata.
///
- [Fact]
+ [TestMethod]
public async Task DiscoverDotsiderSessions_FindsDiffModeInstance()
{
var (pid, listener, analyzers) = CreateRealDiffListener();
@@ -97,23 +99,23 @@ public async Task DiscoverDotsiderSessions_FindsDiffModeInstance()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var sessions = JsonSerializer.Deserialize(text);
- Assert.NotNull(sessions);
+ Assert.IsNotNull(sessions);
var diffSession = sessions!.FirstOrDefault(s =>
s.GetProperty("pid").GetInt32() == pid);
- Assert.NotEqual(default, diffSession);
- Assert.Equal("diff", diffSession.GetProperty("info").GetProperty("mode").GetString());
- Assert.True(diffSession.GetProperty("info").TryGetProperty("left", out _));
- Assert.True(diffSession.GetProperty("info").TryGetProperty("right", out _));
+ Assert.AreNotEqual(default, diffSession);
+ Assert.AreEqual("diff", diffSession.GetProperty("info").GetProperty("mode").GetString());
+ Assert.IsTrue(diffSession.GetProperty("info").TryGetProperty("left", out _));
+ Assert.IsTrue(diffSession.GetProperty("info").TryGetProperty("right", out _));
}
///
/// Diff-mode session info carries left/right assembly names plus the current tab and filter mode.
///
- [Fact]
+ [TestMethod]
public async Task GetSessionInfo_DiffMode_ReturnsBothAssemblyAndView()
{
var (pid, listener, analyzers) = CreateRealDiffListener();
@@ -129,21 +131,21 @@ public async Task GetSessionInfo_DiffMode_ReturnsBothAssemblyAndView()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var doc = JsonDocument.Parse(text!);
// Assembly info from the real listener's assemblyInfoProvider
var assembly = doc.RootElement.GetProperty("assembly");
- Assert.Equal("diff", assembly.GetProperty("mode").GetString());
- Assert.Equal("RichLibrary", assembly.GetProperty("left").GetProperty("assemblyName").GetString());
- Assert.Equal("RichLibrary", assembly.GetProperty("right").GetProperty("assemblyName").GetString());
+ Assert.AreEqual("diff", assembly.GetProperty("mode").GetString());
+ Assert.AreEqual("RichLibrary", assembly.GetProperty("left").GetProperty("assemblyName").GetString());
+ Assert.AreEqual("RichLibrary", assembly.GetProperty("right").GetProperty("assemblyName").GetString());
// View from the real listener's currentViewProvider
var view = doc.RootElement.GetProperty("view");
- Assert.Equal("diff", view.GetProperty("mode").GetString());
- Assert.True(view.TryGetProperty("tab", out _));
- Assert.True(view.TryGetProperty("filterMode", out _));
+ Assert.AreEqual("diff", view.GetProperty("mode").GetString());
+ Assert.IsTrue(view.TryGetProperty("tab", out _));
+ Assert.IsTrue(view.TryGetProperty("filterMode", out _));
}
// --- NuGet mode: real DotsiderDiagnosticsListener with real NuGetPackageAnalyzer ---
@@ -151,7 +153,7 @@ public async Task GetSessionInfo_DiffMode_ReturnsBothAssemblyAndView()
///
/// Discovery surfaces a real NuGet-mode listener and includes its packageId in the info.
///
- [Fact]
+ [TestMethod]
public async Task DiscoverDotsiderSessions_FindsNugetModeInstance()
{
var (pid, listener, package) = CreateRealNugetListener();
@@ -167,23 +169,23 @@ public async Task DiscoverDotsiderSessions_FindsNugetModeInstance()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var sessions = JsonSerializer.Deserialize(text);
- Assert.NotNull(sessions);
+ Assert.IsNotNull(sessions);
var nugetSession = sessions!.FirstOrDefault(s =>
s.GetProperty("pid").GetInt32() == pid);
- Assert.NotEqual(default, nugetSession);
- Assert.Equal("nuget", nugetSession.GetProperty("info").GetProperty("mode").GetString());
- Assert.Equal("RichLibrary",
+ Assert.AreNotEqual(default, nugetSession);
+ Assert.AreEqual("nuget", nugetSession.GetProperty("info").GetProperty("mode").GetString());
+ Assert.AreEqual("RichLibrary",
nugetSession.GetProperty("info").GetProperty("packageId").GetString());
}
///
/// NuGet-mode session info reports package metadata alongside the browsing-package view state.
///
- [Fact]
+ [TestMethod]
public async Task GetSessionInfo_NugetMode_ReturnsBothAssemblyAndView()
{
var (pid, listener, package) = CreateRealNugetListener();
@@ -199,31 +201,31 @@ public async Task GetSessionInfo_NugetMode_ReturnsBothAssemblyAndView()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var doc = JsonDocument.Parse(text!);
// Assembly info from the real listener's assemblyInfoProvider
var assembly = doc.RootElement.GetProperty("assembly");
- Assert.Equal("nuget", assembly.GetProperty("mode").GetString());
- Assert.Equal("RichLibrary", assembly.GetProperty("packageId").GetString());
- Assert.Equal("2.5.1", assembly.GetProperty("packageVersion").GetString());
- Assert.True(assembly.GetProperty("dllCount").GetInt32() > 0);
+ Assert.AreEqual("nuget", assembly.GetProperty("mode").GetString());
+ Assert.AreEqual("RichLibrary", assembly.GetProperty("packageId").GetString());
+ Assert.AreEqual("2.5.1", assembly.GetProperty("packageVersion").GetString());
+ Assert.IsGreaterThan(0, assembly.GetProperty("dllCount").GetInt32());
// View from the real listener's currentViewProvider
var view = doc.RootElement.GetProperty("view");
- Assert.Equal("nuget", view.GetProperty("mode").GetString());
- Assert.True(view.GetProperty("isBrowsingPackage").GetBoolean());
+ Assert.AreEqual("nuget", view.GetProperty("mode").GetString());
+ Assert.IsTrue(view.GetProperty("isBrowsingPackage").GetBoolean());
}
// --- Helpers ---
- private (int pid, DotsiderDiagnosticsListener listener, AnalyzerPair analyzers)
+ private static (int pid, DotsiderDiagnosticsListener listener, AnalyzerPair analyzers)
CreateRealDiffListener()
{
var pid = Interlocked.Increment(ref s_nextPid);
- var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var listener = new DotsiderDiagnosticsListener(
() => null,
@@ -261,11 +263,11 @@ public async Task GetSessionInfo_NugetMode_ReturnsBothAssemblyAndView()
return (pid, listener, new AnalyzerPair(left, right));
}
- private (int pid, DotsiderDiagnosticsListener listener, NuGetPackageAnalyzer package)
+ private static (int pid, DotsiderDiagnosticsListener listener, NuGetPackageAnalyzer package)
CreateRealNugetListener()
{
var pid = Interlocked.Increment(ref s_nextPid);
- var package = new NuGetPackageAnalyzer(samples.RichLibraryNupkg);
+ var package = new NuGetPackageAnalyzer(Samples.RichLibraryNupkg);
var listener = new DotsiderDiagnosticsListener(
() => null,
diff --git a/tests/Dotsider.Mcp.Tests/SizeToolsTests.cs b/tests/Dotsider.Mcp.Tests/SizeToolsTests.cs
index 4d80aa58..a62b7630 100644
--- a/tests/Dotsider.Mcp.Tests/SizeToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/SizeToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class SizeToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class SizeToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// get_size_breakdown produces an error-free hierarchical size payload for a real library.
///
- [Fact]
+ [TestMethod]
public async Task GetSizeBreakdown_RichLibrary_ReturnsSizeTree()
{
await StartServerAsync();
@@ -22,18 +24,18 @@ public async Task GetSizeBreakdown_RichLibrary_ReturnsSizeTree()
var result = await client.CallToolAsync(
"get_size_breakdown",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.DoesNotContain("Error", text);
}
///
/// get_largest_methods honors maxResults and returns the top-N largest IL bodies.
///
- [Fact]
+ [TestMethod]
public async Task GetLargestMethods_RichLibrary_ReturnsSortedMethods()
{
await StartServerAsync();
@@ -43,22 +45,22 @@ public async Task GetLargestMethods_RichLibrary_ReturnsSortedMethods()
"get_largest_methods",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["maxResults"] = 5
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var methods = JsonSerializer.Deserialize(text);
- Assert.True(methods.GetArrayLength() > 0);
- Assert.True(methods.GetArrayLength() <= 5);
+ Assert.IsGreaterThan(0, methods.GetArrayLength());
+ Assert.IsLessThanOrEqualTo(5, methods.GetArrayLength());
}
///
/// Without an explicit limit, get_largest_methods caps output at the 20-method default.
///
- [Fact]
+ [TestMethod]
public async Task GetLargestMethods_DefaultCount_Returns20OrFewer()
{
await StartServerAsync();
@@ -66,36 +68,36 @@ public async Task GetLargestMethods_DefaultCount_Returns20OrFewer()
var result = await client.CallToolAsync(
"get_largest_methods",
- new Dictionary { ["assemblyPath"] = samples.ComplexAppDll },
+ new Dictionary { ["assemblyPath"] = Samples.ComplexAppDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var methods = JsonSerializer.Deserialize(text);
- Assert.True(methods.GetArrayLength() <= 20);
+ Assert.IsLessThanOrEqualTo(20, methods.GetArrayLength());
}
///
/// get_size_breakdown on a Native AOT binary with an mstat sidecar returns the AOT tree:
/// assembly subtrees plus category nodes, not an empty root.
///
- [Fact]
+ [TestMethod]
public async Task GetSizeBreakdown_NativeAot_ReturnsAotTree()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"get_size_breakdown",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var tree = JsonSerializer.Deserialize(text);
- Assert.True(tree.GetProperty("size").GetInt64() > 0);
+ Assert.IsGreaterThan(0, tree.GetProperty("size").GetInt64());
Assert.Contains("category", text);
Assert.Contains("System.Private.CoreLib", text);
}
@@ -103,10 +105,11 @@ public async Task GetSizeBreakdown_NativeAot_ReturnsAotTree()
///
/// get_largest_methods uses Native AOT mstat method sizes when IL bodies are absent.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetLargestMethods_NativeAot_ReturnsMstatMethods()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
await StartServerAsync();
await using var client = await CreateClientAsync();
@@ -115,19 +118,19 @@ public async Task GetLargestMethods_NativeAot_ReturnsMstatMethods()
"get_largest_methods",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["maxResults"] = 5
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var methods = JsonSerializer.Deserialize(text);
- Assert.True(methods.GetArrayLength() > 0);
- Assert.All(methods.EnumerateArray(), m =>
+ Assert.IsGreaterThan(0, methods.GetArrayLength());
+ TestAssert.All(methods.EnumerateArray(), m =>
{
- Assert.Equal("Mstat", m.GetProperty("source").GetString());
- Assert.True(m.GetProperty("size").GetInt64() > 0);
+ Assert.AreEqual("Mstat", m.GetProperty("source").GetString());
+ Assert.IsGreaterThan(0, m.GetProperty("size").GetInt64());
});
}
}
diff --git a/tests/Dotsider.Mcp.Tests/StringToolsTests.cs b/tests/Dotsider.Mcp.Tests/StringToolsTests.cs
index 47aa732d..7de8e266 100644
--- a/tests/Dotsider.Mcp.Tests/StringToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/StringToolsTests.cs
@@ -8,13 +8,15 @@ namespace Dotsider.Mcp.Tests;
///
/// Creates the tests using the shared sample assembly fixture.
///
-[Collection("SampleAssemblies")]
-public class StringToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class StringToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// extract_strings returns user, metadata, and raw string categories in a single payload.
///
- [Fact]
+ [TestMethod]
public async Task ExtractStrings_RichLibrary_ReturnsStringCategories()
{
await StartServerAsync();
@@ -22,21 +24,21 @@ public async Task ExtractStrings_RichLibrary_ReturnsStringCategories()
var result = await client.CallToolAsync(
"extract_strings",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("userStrings", out _));
- Assert.True(json.TryGetProperty("metadataStrings", out _));
- Assert.True(json.TryGetProperty("rawStrings", out _));
+ Assert.IsTrue(json.TryGetProperty("userStrings", out _));
+ Assert.IsTrue(json.TryGetProperty("metadataStrings", out _));
+ Assert.IsTrue(json.TryGetProperty("rawStrings", out _));
}
///
/// A query filter restricts extract_strings output to substring-matching entries.
///
- [Fact]
+ [TestMethod]
public async Task ExtractStrings_WithQuery_FiltersResults()
{
await StartServerAsync();
@@ -46,21 +48,21 @@ public async Task ExtractStrings_WithQuery_FiltersResults()
"extract_strings",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["query"] = "Hello"
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("userStrings", out _));
+ Assert.IsTrue(json.TryGetProperty("userStrings", out _));
}
///
/// maxResults caps each string category to avoid flooding the MCP client.
///
- [Fact]
+ [TestMethod]
public async Task ExtractStrings_WithMaxResults_LimitsOutput()
{
await StartServerAsync();
@@ -70,17 +72,17 @@ public async Task ExtractStrings_WithMaxResults_LimitsOutput()
"extract_strings",
new Dictionary
{
- ["assemblyPath"] = samples.RichLibraryDll,
+ ["assemblyPath"] = Samples.RichLibraryDll,
["maxResults"] = 2
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
if (json.TryGetProperty("userStrings", out var user))
{
- Assert.True(user.GetArrayLength() <= 2);
+ Assert.IsLessThanOrEqualTo(2, user.GetArrayLength());
}
}
@@ -88,10 +90,11 @@ public async Task ExtractStrings_WithMaxResults_LimitsOutput()
/// extract_strings on a Native AOT executable returns raw ASCII and raw UTF-16
/// strings even though the metadata heaps are absent.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ExtractStrings_NativeAot_ReturnsRawAndUtf16()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null,
+ TestSkip.When(Samples.NativeAotConsoleExe is null,
"NativeAOT sample was not built");
await StartServerAsync();
@@ -101,25 +104,26 @@ public async Task ExtractStrings_NativeAot_ReturnsRawAndUtf16()
"extract_strings",
new Dictionary
{
- ["assemblyPath"] = samples.NativeAotConsoleExe,
+ ["assemblyPath"] = Samples.NativeAotConsoleExe,
["minLength"] = 8,
["maxResults"] = 50
},
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Empty(json.GetProperty("userStrings").EnumerateArray());
- Assert.True(json.GetProperty("rawStrings").GetArrayLength() > 0);
- Assert.True(json.GetProperty("rawUtf16Strings").GetArrayLength() > 0);
+ Assert.IsEmpty(json.GetProperty("userStrings").EnumerateArray());
+ Assert.IsGreaterThan(0, json.GetProperty("rawStrings").GetArrayLength());
+ Assert.IsGreaterThan(0, json.GetProperty("rawUtf16Strings").GetArrayLength());
}
///
/// extract_strings always includes the rawUtf16Strings category for managed
/// assemblies too.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ExtractStrings_Managed_HasRawUtf16Field()
{
await StartServerAsync();
@@ -127,13 +131,13 @@ public async Task ExtractStrings_Managed_HasRawUtf16Field()
var result = await client.CallToolAsync(
"extract_strings",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.TryGetProperty("rawUtf16Strings", out _));
+ Assert.IsTrue(json.TryGetProperty("rawUtf16Strings", out _));
}
///
@@ -141,25 +145,25 @@ public async Task ExtractStrings_Managed_HasRawUtf16Field()
/// platform — from the file-backed region on Windows and macOS, and from the rehydrated
/// dehydrated data on Linux.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ExtractStrings_NativeAot_IncludesFrozenStrings()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"extract_strings",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
var frozen = json.GetProperty("frozenStrings");
- Assert.Equal(JsonValueKind.Array, frozen.ValueKind);
- Assert.Contains(frozen.EnumerateArray(),
- e => e.GetProperty("value").GetString()!.Contains("Hello from NativeAOT!"));
+ Assert.AreEqual(JsonValueKind.Array, frozen.ValueKind);
+ Assert.Contains(e => e.GetProperty("value").GetString()!.Contains("Hello from NativeAOT!"), frozen.EnumerateArray());
}
}
diff --git a/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs b/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs
index 6fed4ae2..eb4adf01 100644
--- a/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/SymbolToolsTests.cs
@@ -9,28 +9,31 @@ namespace Dotsider.Mcp.Tests;
/// Tests for the native-symbol MCP tool: get_native_symbols against the real Native AOT
/// fixture and the managed-assembly error path.
///
-[Collection("SampleAssemblies")]
-public class SymbolToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public class SymbolToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// get_native_symbols returns the symbol list with its provenance for a Native AOT binary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeSymbols_NativeAot_ReturnsSymbolsWithProvenance()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"get_native_symbols",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text.StartsWith("Error", StringComparison.Ordinal),
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text.StartsWith("Error", StringComparison.Ordinal),
"the tool reported an error instead of symbols");
Assert.Contains("\"source\"", text);
Assert.Contains("\"status\"", text);
@@ -40,7 +43,8 @@ public async Task GetNativeSymbols_NativeAot_ReturnsSymbolsWithProvenance()
///
/// get_native_symbols reports an error for a managed assembly instead of an empty list.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeSymbols_Managed_ReturnsError()
{
await StartServerAsync();
@@ -48,28 +52,29 @@ public async Task GetNativeSymbols_Managed_ReturnsError()
var result = await client.CallToolAsync(
"get_native_symbols",
- new Dictionary { ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("Error", text);
Assert.Contains("managed", text);
}
/// get_native_disassembly decodes a function by address into structured instructions.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeDisassembly_NativeAot_ByAddress_ReturnsInstructions()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
ulong va;
- using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(samples.NativeAotConsoleExe!))
+ using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(Samples.NativeAotConsoleExe!))
{
var fn = analyzer.NativeSymbols?.Symbols.FirstOrDefault(s =>
s.Kind == Dotsider.Core.Analysis.Models.NativeSymbolKind.Function
&& s.ManagedName is not null && s.FileOffset is not null && s.Size > 0);
- Assert.NotNull(fn);
+ Assert.IsNotNull(fn);
va = fn!.VirtualAddress;
}
@@ -78,19 +83,20 @@ public async Task GetNativeDisassembly_NativeAot_ByAddress_ReturnsInstructions()
var result = await client.CallToolAsync(
"get_native_disassembly",
- new Dictionary { ["address"] = $"0x{va:x}", ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["address"] = $"0x{va:x}", ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text.StartsWith("Error", StringComparison.Ordinal), "the tool reported an error");
Assert.Contains("\"mnemonic\"", text);
}
///
/// get_native_symbols returns WebAssembly function symbols for a raw SDK browser-wasm module.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeSymbols_Wasm_ReturnsWebAssemblySymbols()
{
var wasmPath = GetWasmNativePath();
@@ -104,18 +110,19 @@ public async Task GetNativeSymbols_Wasm_ReturnsWebAssemblySymbols()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("webAssembly", json.GetProperty("source").GetString());
- Assert.Equal("wasm32", json.GetProperty("architecture").GetString());
- Assert.True(json.GetProperty("symbols").GetArrayLength() > 0);
+ Assert.AreEqual("webAssembly", json.GetProperty("source").GetString());
+ Assert.AreEqual("wasm32", json.GetProperty("architecture").GetString());
+ Assert.IsGreaterThan(0, json.GetProperty("symbols").GetArrayLength());
}
///
/// get_native_disassembly decodes a WebAssembly function from a real dotnet.native.wasm
/// module through the same native-tool surface used for PE, ELF, Mach-O, and ReadyToRun code.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeDisassembly_Wasm_ByAddress_ReturnsInstructions()
{
var wasmPath = GetWasmNativePath();
@@ -140,20 +147,21 @@ public async Task GetNativeDisassembly_Wasm_ByAddress_ReturnsInstructions()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text.StartsWith("Error", StringComparison.Ordinal), text);
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text.StartsWith("Error", StringComparison.Ordinal), text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("Wasm32", json.GetProperty("architecture").GetString());
- Assert.Contains(json.GetProperty("instructions").EnumerateArray(), static instruction =>
+ Assert.AreEqual("Wasm32", json.GetProperty("architecture").GetString());
+ Assert.Contains(static instruction =>
instruction.TryGetProperty("targetName", out var targetName)
- && targetName.ValueKind == JsonValueKind.String);
+ && targetName.ValueKind == JsonValueKind.String, json.GetProperty("instructions").EnumerateArray());
}
///
/// get_native_disassembly accepts WebAssembly func:N identifiers through the same
/// symbolName parameter used for PE, ELF, Mach-O, ReadyToRun, and Native AOT symbols.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeDisassembly_Wasm_ByFunctionIndex_ReturnsInstructions()
{
var wasmPath = GetWasmNativePath();
@@ -179,15 +187,16 @@ public async Task GetNativeDisassembly_Wasm_ByFunctionIndex_ReturnsInstructions(
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
- Assert.False(text.StartsWith("Error", StringComparison.Ordinal), text);
+ Assert.IsNotNull(text);
+ Assert.IsFalse(text.StartsWith("Error", StringComparison.Ordinal), text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal("Wasm32", json.GetProperty("architecture").GetString());
- Assert.True(json.GetProperty("instructions").GetArrayLength() > 0);
+ Assert.AreEqual("Wasm32", json.GetProperty("architecture").GetString());
+ Assert.IsGreaterThan(0, json.GetProperty("instructions").GetArrayLength());
}
/// get_native_disassembly reports an error for a managed assembly.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetNativeDisassembly_Managed_ReturnsError()
{
await StartServerAsync();
@@ -195,32 +204,33 @@ public async Task GetNativeDisassembly_Managed_ReturnsError()
var result = await client.CallToolAsync(
"get_native_disassembly",
- new Dictionary { ["symbolName"] = "Foo", ["assemblyPath"] = samples.RichLibraryDll },
+ new Dictionary { ["symbolName"] = "Foo", ["assemblyPath"] = Samples.RichLibraryDll },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("Error", text);
}
///
/// get_assembly_info carries the native symbol provenance fields.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GetAssemblyInfo_NativeAot_CarriesSymbolProvenance()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
await StartServerAsync();
await using var client = await CreateClientAsync();
var result = await client.CallToolAsync(
"get_assembly_info",
- new Dictionary { ["assemblyPath"] = samples.NativeAotConsoleExe },
+ new Dictionary { ["assemblyPath"] = Samples.NativeAotConsoleExe },
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
Assert.Contains("nativeSymbolCount", text);
Assert.Contains("nativeSymbolSource", text);
Assert.Contains("nativeSymbolStatus", text);
@@ -229,7 +239,7 @@ public async Task GetAssemblyInfo_NativeAot_CarriesSymbolProvenance()
private static NativeSymbol FindWasmFunctionWithNamedCall(AssemblyAnalyzer analyzer)
{
var info = analyzer.NativeSymbols;
- Assert.NotNull(info);
+ Assert.IsNotNull(info);
foreach (var symbol in info.Symbols.Take(512))
{
var result = NativeDisassembler.DisassembleSymbol(analyzer, symbol);
@@ -243,11 +253,11 @@ private static NativeSymbol FindWasmFunctionWithNamedCall(AssemblyAnalyzer analy
throw new InvalidOperationException("No Wasm function with a named direct call was found.");
}
- private string GetWasmNativePath()
+ private static string GetWasmNativePath()
{
- Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null,
+ TestSkip.When(Samples.WasmConsoleNativeWasm is null && Samples.ReadyToRunConsoleWasmNativeWasm is null,
"browser-wasm publish did not run on this leg.");
- return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!;
+ return Samples.WasmConsoleNativeWasm ?? Samples.ReadyToRunConsoleWasmNativeWasm!;
}
}
diff --git a/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs b/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs
index 4d771087..c1d8eaba 100644
--- a/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs
+++ b/tests/Dotsider.Mcp.Tests/ToolRegistrationTests.cs
@@ -3,12 +3,13 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests that guard the advertised MCP tool surface and its metadata quality.
///
+[TestClass]
public class ToolRegistrationTests : McpServerTestBase
{
///
/// Every expected tool across the suite is present in ListTools output.
///
- [Fact]
+ [TestMethod]
public async Task ListTools_ReturnsAllRegisteredTools()
{
await StartServerAsync();
@@ -110,7 +111,7 @@ public async Task ListTools_ReturnsAllRegisteredTools()
///
/// No registered tool ships without a human-readable description, which MCP clients rely on.
///
- [Fact]
+ [TestMethod]
public async Task AllTools_HaveDescriptions()
{
await StartServerAsync();
@@ -120,7 +121,7 @@ public async Task AllTools_HaveDescriptions()
foreach (var tool in tools)
{
- Assert.False(string.IsNullOrWhiteSpace(tool.Description),
+ Assert.IsFalse(string.IsNullOrWhiteSpace(tool.Description),
$"Tool '{tool.Name}' should have a description");
}
}
diff --git a/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs b/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs
index 380924f2..84308a3d 100644
--- a/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs
+++ b/tests/Dotsider.Mcp.Tests/WasmToolsTests.cs
@@ -6,13 +6,16 @@ namespace Dotsider.Mcp.Tests;
///
/// Tests for the MCP WebAssembly tools over real SDK-produced browser-wasm output.
///
-[Collection("SampleAssemblies")]
-public sealed class WasmToolsTests(SampleAssemblyFixture samples) : McpServerTestBase
+[TestClass]
+public sealed class WasmToolsTests : McpServerTestBase
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// list_wasm_sections returns raw Wasm section payload offsets and sizes from dotnet.native.wasm.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListWasmSections_Direct_ReturnsSections()
{
var wasmPath = GetWasmNativePath();
@@ -26,17 +29,18 @@ public async Task ListWasmSections_Direct_ReturnsSections()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("sectionCount").GetInt32() > 0);
- Assert.Contains(json.GetProperty("sections").EnumerateArray(), static section =>
- section.GetProperty("name").GetString() == "code");
+ Assert.IsGreaterThan(0, json.GetProperty("sectionCount").GetInt32());
+ Assert.Contains(static section =>
+ section.GetProperty("name").GetString() == "code", json.GetProperty("sections").EnumerateArray());
}
///
/// list_wasm_functions returns imported and file-backed functions in Wasm function-index order.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListWasmFunctions_Direct_ReturnsFunctionInventory()
{
var wasmPath = GetWasmNativePath();
@@ -50,18 +54,19 @@ public async Task ListWasmFunctions_Direct_ReturnsFunctionInventory()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.True(json.GetProperty("functionCount").GetInt32() > 0);
+ Assert.IsGreaterThan(0, json.GetProperty("functionCount").GetInt32());
var functions = json.GetProperty("functions").EnumerateArray().ToArray();
- Assert.Contains(functions, static function => function.GetProperty("isImported").GetBoolean());
- Assert.Contains(functions, static function => !function.GetProperty("isImported").GetBoolean());
+ Assert.Contains(static function => function.GetProperty("isImported").GetBoolean(), functions);
+ Assert.Contains(static function => !function.GetProperty("isImported").GetBoolean(), functions);
}
///
/// list_wasm_functions forwards session requests to a running dotsider instance unchanged.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ListWasmFunctions_Session_ForwardsRequest()
{
await using var socket = new TestDotsiderSocket(999_997, "dotnet.native.wasm");
@@ -80,17 +85,17 @@ public async Task ListWasmFunctions_Session_ForwardsRequest()
cancellationToken: TestCancellationToken);
var text = GetTextContent(result);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
var json = JsonSerializer.Deserialize(text);
- Assert.Equal(1, json.GetProperty("functionCount").GetInt32());
- Assert.Equal("func_0", json.GetProperty("functions")[0].GetProperty("name").GetString());
+ Assert.AreEqual(1, json.GetProperty("functionCount").GetInt32());
+ Assert.AreEqual("func_0", json.GetProperty("functions")[0].GetProperty("name").GetString());
}
- private string GetWasmNativePath()
+ private static string GetWasmNativePath()
{
- Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null,
+ TestSkip.When(Samples.WasmConsoleNativeWasm is null && Samples.ReadyToRunConsoleWasmNativeWasm is null,
"browser-wasm publish did not run on this leg.");
- return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!;
+ return Samples.WasmConsoleNativeWasm ?? Samples.ReadyToRunConsoleWasmNativeWasm!;
}
}
diff --git a/tests/Dotsider.Tests/AgentCliTests.cs b/tests/Dotsider.Tests/AgentCliTests.cs
index 03f03def..e0c2bd0b 100644
--- a/tests/Dotsider.Tests/AgentCliTests.cs
+++ b/tests/Dotsider.Tests/AgentCliTests.cs
@@ -5,6 +5,7 @@ namespace Dotsider.Tests;
///
/// CLI integration tests for the agent command.
///
+[TestClass]
public class AgentCliTests
{
private static readonly string s_projectPath = Path.Combine(
@@ -27,13 +28,13 @@ private static string DetectBuildConfig()
///
/// Verifies agent init stdout writes skill content.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Init_Stdout_WritesSkillContent()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
"agent", "init", "--stdout");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("name: dotsider", stdout);
Assert.Contains("dotsider analyze", stdout);
Assert.Contains("dotsider sessions", stdout);
@@ -42,7 +43,7 @@ public async Task Agent_Init_Stdout_WritesSkillContent()
///
/// Verifies agent init with path creates file.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Init_WithPath_CreatesFile()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
@@ -53,8 +54,8 @@ public async Task Agent_Init_WithPath_CreatesFile()
var (exitCode, stdout, _) = await RunDotsiderAsync(
"agent", "init", "--path", outputPath);
- Assert.Equal(0, exitCode);
- Assert.True(File.Exists(outputPath));
+ Assert.AreEqual(0, exitCode);
+ Assert.IsTrue(File.Exists(outputPath));
var content = File.ReadAllText(outputPath);
Assert.Contains("name: dotsider", content);
}
@@ -68,7 +69,7 @@ public async Task Agent_Init_WithPath_CreatesFile()
///
/// Verifies agent init no force errors if exists.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Init_NoForce_ErrorsIfExists()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
@@ -82,9 +83,9 @@ public async Task Agent_Init_NoForce_ErrorsIfExists()
var (exitCode, _, stderr) = await RunDotsiderAsync(
"agent", "init", "--path", outputPath);
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("already exists", stderr);
- Assert.Equal("existing content", File.ReadAllText(outputPath));
+ Assert.AreEqual("existing content", File.ReadAllText(outputPath));
}
finally
{
@@ -96,7 +97,7 @@ public async Task Agent_Init_NoForce_ErrorsIfExists()
///
/// Verifies agent init force overwrites existing.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Init_Force_OverwritesExisting()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
@@ -110,7 +111,7 @@ public async Task Agent_Init_Force_OverwritesExisting()
var (exitCode, stdout, _) = await RunDotsiderAsync(
"agent", "init", "--path", outputPath, "--force");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var content = File.ReadAllText(outputPath);
Assert.Contains("name: dotsider", content);
Assert.DoesNotContain("existing content", content);
@@ -125,7 +126,7 @@ public async Task Agent_Init_Force_OverwritesExisting()
///
/// Verifies agent init with ai creates correct path.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Init_WithAi_CreatesCorrectPath()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
@@ -138,10 +139,10 @@ public async Task Agent_Init_WithAi_CreatesCorrectPath()
var (exitCode, stdout, _) = await RunDotsiderInDirAsync(
tempDir, "agent", "init", "--ai", "claude");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var expectedPath = Path.Combine(tempDir, ".claude", "skills", "dotsider", "SKILL.md");
- Assert.True(File.Exists(expectedPath), $"Expected file at {expectedPath}");
+ Assert.IsTrue(File.Exists(expectedPath), $"Expected file at {expectedPath}");
}
finally
{
@@ -153,13 +154,13 @@ public async Task Agent_Init_WithAi_CreatesCorrectPath()
///
/// Verifies agent init no args shows usage error.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Init_NoArgs_ShowsUsageError()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
"agent", "init");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("--ai", stderr);
Assert.Contains("--path", stderr);
Assert.Contains("--stdout", stderr);
@@ -168,13 +169,13 @@ public async Task Agent_Init_NoArgs_ShowsUsageError()
///
/// Verifies agent help shows subcommands.
///
- [Fact]
+ [TestMethod]
public async Task Agent_Help_ShowsSubcommands()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
"agent", "--help");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("mcp", stdout);
Assert.Contains("init", stdout);
}
@@ -192,6 +193,7 @@ public async Task Agent_Help_ShowsSubcommands()
RedirectStandardOutput = true,
RedirectStandardError = true,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
var stdout = await process.StandardOutput.ReadToEndAsync();
@@ -213,6 +215,7 @@ public async Task Agent_Help_ShowsSubcommands()
RedirectStandardError = true,
WorkingDirectory = workingDirectory,
};
+ TestProcessEnvironment.RemoveCodeCoverageVariables(psi);
var process = Process.Start(psi)!;
var stdout = await process.StandardOutput.ReadToEndAsync();
diff --git a/tests/Dotsider.Tests/ApphostDetectorTests.cs b/tests/Dotsider.Tests/ApphostDetectorTests.cs
index 7b990791..bfe41979 100644
--- a/tests/Dotsider.Tests/ApphostDetectorTests.cs
+++ b/tests/Dotsider.Tests/ApphostDetectorTests.cs
@@ -6,75 +6,83 @@ namespace Dotsider.Tests;
///
/// Tests for Apphost Detector.
///
-[Collection("SampleAssemblies")]
-public class ApphostDetectorTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class ApphostDetectorTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private readonly List _tempFiles = [];
///
/// Verifies find companion dll apphost returns companion dll path.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_Apphost_ReturnsCompanionDllPath()
{
- var result = ApphostDetector.FindCompanionDll(samples.HelloWorldExe);
+ var result = ApphostDetector.FindCompanionDll(Samples.HelloWorldExe);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
Assert.EndsWith(".dll", result!, StringComparison.OrdinalIgnoreCase);
- Assert.True(File.Exists(result));
+ Assert.IsTrue(File.Exists(result));
}
///
/// Verifies find companion dll managed dll returns null.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_ManagedDll_ReturnsNull()
{
- var result = ApphostDetector.FindCompanionDll(samples.HelloWorldDll);
+ var result = ApphostDetector.FindCompanionDll(Samples.HelloWorldDll);
- Assert.Null(result);
+ Assert.IsNull(result);
}
///
/// Verifies find companion dll native aot exe returns null.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_NativeAotExe_ReturnsNull()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null,
+ TestSkip.When(Samples.NativeAotConsoleExe is null,
"NativeAOT sample was not built");
- var result = ApphostDetector.FindCompanionDll(samples.NativeAotConsoleExe!);
+ var result = ApphostDetector.FindCompanionDll(Samples.NativeAotConsoleExe!);
- Assert.Null(result);
+ Assert.IsNull(result);
}
///
/// Verifies find companion dll non exe extension returns null.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_NonExeExtension_ReturnsNull()
{
- var result = ApphostDetector.FindCompanionDll(samples.RichLibraryDll);
+ var result = ApphostDetector.FindCompanionDll(Samples.RichLibraryDll);
- Assert.Null(result);
+ Assert.IsNull(result);
}
///
/// Verifies find companion dll non existent file returns null.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_NonExistentFile_ReturnsNull()
{
var result = ApphostDetector.FindCompanionDll(
Path.Combine(Path.GetTempPath(), "nonexistent-assembly.exe"));
- Assert.Null(result);
+ Assert.IsNull(result);
}
///
/// Verifies find companion dll native exe with dll name but no hostfxr returns null.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_NativeExeWithDllNameButNoHostfxr_ReturnsNull()
{
// Simulate a native launcher that embeds the DLL name for its own reasons
@@ -93,26 +101,27 @@ public void FindCompanionDll_NativeExeWithDllNameButNoHostfxr_ReturnsNull()
_tempFiles.Add(fakeExePath);
// Copy a real managed .dll so the companion check would pass
- File.Copy(samples.HelloWorldDll, fakeDllPath);
+ File.Copy(Samples.HelloWorldDll, fakeDllPath);
_tempFiles.Add(fakeDllPath);
_tempFiles.Add(dir);
var result = ApphostDetector.FindCompanionDll(fakeExePath);
- Assert.Null(result);
+ Assert.IsNull(result);
}
///
/// Verifies find companion dll dotted name apphost returns companion dll path.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindCompanionDll_DottedNameApphost_ReturnsCompanionDllPath()
{
- var result = ApphostDetector.FindCompanionDll(samples.DottedNameAppExe);
+ var result = ApphostDetector.FindCompanionDll(Samples.DottedNameAppExe);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
Assert.EndsWith("Dotted.Name.App.dll", result!, StringComparison.OrdinalIgnoreCase);
- Assert.True(File.Exists(result));
+ Assert.IsTrue(File.Exists(result));
}
///
@@ -120,6 +129,7 @@ public void FindCompanionDll_DottedNameApphost_ReturnsCompanionDllPath()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
foreach (var path in _tempFiles)
{
try
@@ -129,6 +139,5 @@ public void Dispose()
}
catch { /* best effort */ }
}
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/Arm64DecoderTests.cs b/tests/Dotsider.Tests/Arm64DecoderTests.cs
index 5573db8d..63b02cdb 100644
--- a/tests/Dotsider.Tests/Arm64DecoderTests.cs
+++ b/tests/Dotsider.Tests/Arm64DecoderTests.cs
@@ -9,6 +9,7 @@ namespace Dotsider.Tests;
/// (mov/cmp/cmn/tst/neg/mul/lsl/ubfx/cset) — cross-checked against Capstone. Every A64 instruction
/// is four bytes, so the fixed length is asserted throughout.
///
+[TestClass]
public class Arm64DecoderTests
{
private static NativeInstruction One(uint word)
@@ -24,81 +25,86 @@ private static NativeInstruction OneAt(uint word, ulong address)
}
/// Decodes representative A64 base instructions to their mnemonics and operands.
- [Theory(Timeout = 30_000)]
- [InlineData("ret", "", 0xD65F03C0u)]
- [InlineData("nop", "", 0xD503201Fu)]
- [InlineData("add", "x0, x1, x2", 0x8B020020u)]
- [InlineData("add", "x0, x1, #0x4", 0x91001020u)]
- [InlineData("mov", "x0, #0x1", 0xD2800020u)]
- [InlineData("mov", "x0, x1", 0xAA0103E0u)]
- [InlineData("cmp", "x0, #0x0", 0xF100001Fu)]
- [InlineData("cmp", "x0, x1", 0xEB01001Fu)]
- [InlineData("mul", "x0, x1, x2", 0x9B027C20u)]
- [InlineData("lsl", "x0, x1, #0x4", 0xD37CEC20u)]
- [InlineData("ubfx", "w0, w1, #0x1c, #0x1", 0x531C7020u)]
- [InlineData("udiv", "w0, w1, w2", 0x1AC20820u)]
- [InlineData("csel", "x0, x1, x2, eq", 0x9A820020u)]
- [InlineData("cset", "w0, eq", 0x1A9F17E0u)]
- [InlineData("tst", "x0, x1", 0xEA01001Fu)]
- [InlineData("neg", "x0, x1", 0xCB0103E0u)]
- [InlineData("and", "x0, x1, #0xff", 0x92401C20u)]
- [InlineData("udf", "#0x0", 0x00000000u)]
- [InlineData("udf", "#0x1", 0x00000001u)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow("ret", "", 0xD65F03C0u)]
+ [DataRow("nop", "", 0xD503201Fu)]
+ [DataRow("add", "x0, x1, x2", 0x8B020020u)]
+ [DataRow("add", "x0, x1, #0x4", 0x91001020u)]
+ [DataRow("mov", "x0, #0x1", 0xD2800020u)]
+ [DataRow("mov", "x0, x1", 0xAA0103E0u)]
+ [DataRow("cmp", "x0, #0x0", 0xF100001Fu)]
+ [DataRow("cmp", "x0, x1", 0xEB01001Fu)]
+ [DataRow("mul", "x0, x1, x2", 0x9B027C20u)]
+ [DataRow("lsl", "x0, x1, #0x4", 0xD37CEC20u)]
+ [DataRow("ubfx", "w0, w1, #0x1c, #0x1", 0x531C7020u)]
+ [DataRow("udiv", "w0, w1, w2", 0x1AC20820u)]
+ [DataRow("csel", "x0, x1, x2, eq", 0x9A820020u)]
+ [DataRow("cset", "w0, eq", 0x1A9F17E0u)]
+ [DataRow("tst", "x0, x1", 0xEA01001Fu)]
+ [DataRow("neg", "x0, x1", 0xCB0103E0u)]
+ [DataRow("and", "x0, x1, #0xff", 0x92401C20u)]
+ [DataRow("udf", "#0x0", 0x00000000u)]
+ [DataRow("udf", "#0x1", 0x00000001u)]
public void Decode_Base_MnemonicAndOperands(string mnemonic, string operands, uint word)
{
var insn = One(word);
- Assert.False(insn.IsFallback);
- Assert.Equal(mnemonic, insn.Mnemonic);
- Assert.Equal(operands, insn.OperandText);
- Assert.Equal(4, insn.Length);
+ Assert.IsFalse(insn.IsFallback);
+ Assert.AreEqual(mnemonic, insn.Mnemonic);
+ Assert.AreEqual(operands, insn.OperandText);
+ Assert.AreEqual(4, insn.Length);
}
/// Verifies direct branches compute the absolute target and the flow kind.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_Branches_ResolveTargetAndFlow()
{
var bl = One(0x94000010u); // bl +0x40
- Assert.Equal(0x1040UL, bl.TargetAddress);
- Assert.Equal(NativeFlowKind.Call, bl.Flow);
+ Assert.AreEqual(0x1040UL, bl.TargetAddress);
+ Assert.AreEqual(NativeFlowKind.Call, bl.Flow);
var beq = One(0x54000040u); // b.eq +8
- Assert.Equal("b.eq", beq.Mnemonic);
- Assert.Equal(0x1008UL, beq.TargetAddress);
- Assert.Equal(NativeFlowKind.ConditionalBranch, beq.Flow);
+ Assert.AreEqual("b.eq", beq.Mnemonic);
+ Assert.AreEqual(0x1008UL, beq.TargetAddress);
+ Assert.AreEqual(NativeFlowKind.ConditionalBranch, beq.Flow);
var cbz = One(0xB4000040u); // cbz x0, +8
- Assert.Equal(NativeFlowKind.ConditionalBranch, cbz.Flow);
+ Assert.AreEqual(NativeFlowKind.ConditionalBranch, cbz.Flow);
var ret = One(0xD65F03C0u);
- Assert.Equal(NativeFlowKind.Return, ret.Flow);
+ Assert.AreEqual(NativeFlowKind.Return, ret.Flow);
}
/// Verifies adrp computes the page-aligned target off the aligned instruction address.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_Adrp_ComputesPageTarget()
{
var insn = One(0x90000000u); // adrp x0, page(0)
- Assert.Equal("adrp", insn.Mnemonic);
- Assert.Equal(0x1000UL, insn.TargetAddress);
+ Assert.AreEqual("adrp", insn.Mnemonic);
+ Assert.AreEqual(0x1000UL, insn.TargetAddress);
}
/// Verifies ADR/ADRP concatenate immhi:immlo in architectural order.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_AdrAndAdrp_UsesImmhiImmloOrder()
{
var adr = One(0x10000440u); // adr x0, +0x88
- Assert.Equal("adr", adr.Mnemonic);
- Assert.Equal(0x1088UL, adr.TargetAddress);
+ Assert.AreEqual("adr", adr.Mnemonic);
+ Assert.AreEqual(0x1088UL, adr.TargetAddress);
// Real osx-arm64 R2R import-slot pattern: llvm-objdump decodes this as
// "adrp x11, 0x180032000" at 0x180010e28.
var adrp = OneAt(0xD000010Bu, 0x180010E28UL);
- Assert.Equal("adrp", adrp.Mnemonic);
- Assert.Equal(0x180032000UL, adrp.TargetAddress);
+ Assert.AreEqual("adrp", adrp.Mnemonic);
+ Assert.AreEqual(0x180032000UL, adrp.TargetAddress);
}
/// Verifies an arm64 import-slot call sequence is named structurally.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_Arm64ImportSlotCall_ResolvesTargetName()
{
byte[] code =
@@ -125,20 +131,21 @@ static bool Resolve(ulong va, out NativeSymbolRef symbol)
code, 0x180010E28UL, NativeArchitecture.Arm64, Resolve);
var branch = instructions[^1];
- Assert.Equal("blr", branch.Mnemonic);
- Assert.Equal(NativeTargetKind.Import, branch.TargetKind);
- Assert.Equal("Console.WriteLine", branch.TargetName);
+ Assert.AreEqual("blr", branch.Mnemonic);
+ Assert.AreEqual(NativeTargetKind.Import, branch.TargetKind);
+ Assert.AreEqual("Console.WriteLine", branch.TargetName);
}
/// Verifies an unallocated word decodes as a 4-byte .word that never desyncs.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_Unallocated_EmitsWord()
{
// bits[28:25] = 0b0001 is an unallocated top-level class (no decode group), so it desyncs
// into a .word. 0x00000000 is deliberately NOT used here — it is udf #0, a defined encoding.
var insn = One(0x02000000u);
- Assert.True(insn.IsFallback);
- Assert.Equal(".word", insn.Mnemonic);
- Assert.Equal(4, insn.Length);
+ Assert.IsTrue(insn.IsFallback);
+ Assert.AreEqual(".word", insn.Mnemonic);
+ Assert.AreEqual(4, insn.Length);
}
}
diff --git a/tests/Dotsider.Tests/Arm64LoadStoreTests.cs b/tests/Dotsider.Tests/Arm64LoadStoreTests.cs
index 439f58d6..d375e22f 100644
--- a/tests/Dotsider.Tests/Arm64LoadStoreTests.cs
+++ b/tests/Dotsider.Tests/Arm64LoadStoreTests.cs
@@ -9,6 +9,7 @@ namespace Dotsider.Tests;
/// PC-relative literals, exclusive and acquire/release accesses, and LSE atomics — cross-checked
/// against Capstone.
///
+[TestClass]
public class Arm64LoadStoreTests
{
private static NativeInstruction One(uint word)
@@ -18,39 +19,41 @@ private static NativeInstruction One(uint word)
}
/// Decodes representative A64 load/store forms to their mnemonics and addressing modes.
- [Theory(Timeout = 30_000)]
- [InlineData("ldr", "x0, [x1]", 0xF9400020u)]
- [InlineData("str", "x0, [x1]", 0xF9000020u)]
- [InlineData("ldrb", "w0, [x1]", 0x39400020u)]
- [InlineData("ldr", "w0, [x1]", 0xB9400020u)]
- [InlineData("ldr", "x0, [x30], #0x10", 0xF84107C0u)]
- [InlineData("str", "x0, [sp, #0x8]!", 0xF8008FE0u)]
- [InlineData("stp", "x29, x30, [sp, #-0x10]!", 0xA9BF7BFDu)]
- [InlineData("ldp", "x29, x30, [sp], #0x10", 0xA8C17BFDu)]
- [InlineData("ldr", "x0, [x1, x0]", 0xF8606820u)]
- [InlineData("ldr", "d0, [x1]", 0xFD400020u)]
- [InlineData("ldr", "q0, [x1]", 0x3DC00020u)]
- [InlineData("ldr", "s0, [x1]", 0xBD400020u)]
- [InlineData("ldxr", "x0, [x1]", 0xC85F7C20u)]
- [InlineData("stlr", "x0, [x1]", 0xC89FFC20u)]
- [InlineData("ldar", "x0, [x1]", 0xC8DFFC20u)]
- [InlineData("ldaxr", "w0, [x1]", 0x885FFC20u)]
- [InlineData("ldadd", "w0, w1, [x2]", 0xB8200041u)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow("ldr", "x0, [x1]", 0xF9400020u)]
+ [DataRow("str", "x0, [x1]", 0xF9000020u)]
+ [DataRow("ldrb", "w0, [x1]", 0x39400020u)]
+ [DataRow("ldr", "w0, [x1]", 0xB9400020u)]
+ [DataRow("ldr", "x0, [x30], #0x10", 0xF84107C0u)]
+ [DataRow("str", "x0, [sp, #0x8]!", 0xF8008FE0u)]
+ [DataRow("stp", "x29, x30, [sp, #-0x10]!", 0xA9BF7BFDu)]
+ [DataRow("ldp", "x29, x30, [sp], #0x10", 0xA8C17BFDu)]
+ [DataRow("ldr", "x0, [x1, x0]", 0xF8606820u)]
+ [DataRow("ldr", "d0, [x1]", 0xFD400020u)]
+ [DataRow("ldr", "q0, [x1]", 0x3DC00020u)]
+ [DataRow("ldr", "s0, [x1]", 0xBD400020u)]
+ [DataRow("ldxr", "x0, [x1]", 0xC85F7C20u)]
+ [DataRow("stlr", "x0, [x1]", 0xC89FFC20u)]
+ [DataRow("ldar", "x0, [x1]", 0xC8DFFC20u)]
+ [DataRow("ldaxr", "w0, [x1]", 0x885FFC20u)]
+ [DataRow("ldadd", "w0, w1, [x2]", 0xB8200041u)]
public void Decode_LoadStore_MnemonicAndOperands(string mnemonic, string operands, uint word)
{
var insn = One(word);
- Assert.False(insn.IsFallback);
- Assert.Equal(mnemonic, insn.Mnemonic);
- Assert.Equal(operands, insn.OperandText);
- Assert.Equal(4, insn.Length);
+ Assert.IsFalse(insn.IsFallback);
+ Assert.AreEqual(mnemonic, insn.Mnemonic);
+ Assert.AreEqual(operands, insn.OperandText);
+ Assert.AreEqual(4, insn.Length);
}
/// Verifies a PC-relative literal load resolves its absolute target.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_LiteralLoad_ResolvesTarget()
{
var insn = One(0x58000040u); // ldr x0, +8
- Assert.Equal("ldr", insn.Mnemonic);
- Assert.Equal(0x1008UL, insn.TargetAddress);
+ Assert.AreEqual("ldr", insn.Mnemonic);
+ Assert.AreEqual(0x1008UL, insn.TargetAddress);
}
}
diff --git a/tests/Dotsider.Tests/Arm64SimdTests.cs b/tests/Dotsider.Tests/Arm64SimdTests.cs
index 076c2b01..33cee36b 100644
--- a/tests/Dotsider.Tests/Arm64SimdTests.cs
+++ b/tests/Dotsider.Tests/Arm64SimdTests.cs
@@ -8,6 +8,7 @@ namespace Dotsider.Tests;
/// vector three-same and misc classes with their Vd.T arrangements, dup, dot-product, the AES
/// crypto rounds, and CRC32 — cross-checked against Capstone.
///
+[TestClass]
public class Arm64SimdTests
{
private static NativeInstruction One(uint word)
@@ -17,32 +18,33 @@ private static NativeInstruction One(uint word)
}
/// Decodes representative scalar-FP, SIMD, crypto, and CRC instructions.
- [Theory(Timeout = 30_000)]
- [InlineData("fadd", "s0, s1, s2", 0x1E222820u)]
- [InlineData("fmul", "d0, d1, d2", 0x1E620820u)]
- [InlineData("fcmp", "s0, s1", 0x1E212000u)]
- [InlineData("fmov", "s0, s1", 0x1E204020u)]
- [InlineData("scvtf", "s0, w0", 0x1E220000u)]
- [InlineData("fcvtzs", "w0, s0", 0x1E380000u)]
- [InlineData("add", "v0.4s, v1.4s, v2.4s", 0x4EA28420u)]
- [InlineData("fmul", "v0.4s, v1.4s, v2.4s", 0x6E22DC20u)]
- [InlineData("fadd", "v0.4s, v1.4s, v2.4s", 0x4E22D420u)]
- [InlineData("and", "v0.16b, v1.16b, v2.16b", 0x4E221C20u)]
- [InlineData("neg", "v0.4s, v1.4s", 0x6EA0B820u)]
- [InlineData("dup", "v0.4s, w1", 0x4E040C20u)]
- [InlineData("mov", "v16.s[0], w0", 0x4E041C10u)]
- [InlineData("smov", "x0, v16.s[0]", 0x4E042E00u)]
- [InlineData("umov", "w0, v16.b[0]", 0x0E013E00u)]
- [InlineData("aese", "v0.16b, v1.16b", 0x4E284820u)]
- [InlineData("crc32b", "w0, w1, w2", 0x1AC24020u)]
- [InlineData("crc32x", "w0, w1, x2", 0x9AC24C20u)]
- [InlineData("sdot", "v0.4s, v1.16b, v2.16b", 0x4E829420u)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow("fadd", "s0, s1, s2", 0x1E222820u)]
+ [DataRow("fmul", "d0, d1, d2", 0x1E620820u)]
+ [DataRow("fcmp", "s0, s1", 0x1E212000u)]
+ [DataRow("fmov", "s0, s1", 0x1E204020u)]
+ [DataRow("scvtf", "s0, w0", 0x1E220000u)]
+ [DataRow("fcvtzs", "w0, s0", 0x1E380000u)]
+ [DataRow("add", "v0.4s, v1.4s, v2.4s", 0x4EA28420u)]
+ [DataRow("fmul", "v0.4s, v1.4s, v2.4s", 0x6E22DC20u)]
+ [DataRow("fadd", "v0.4s, v1.4s, v2.4s", 0x4E22D420u)]
+ [DataRow("and", "v0.16b, v1.16b, v2.16b", 0x4E221C20u)]
+ [DataRow("neg", "v0.4s, v1.4s", 0x6EA0B820u)]
+ [DataRow("dup", "v0.4s, w1", 0x4E040C20u)]
+ [DataRow("mov", "v16.s[0], w0", 0x4E041C10u)]
+ [DataRow("smov", "x0, v16.s[0]", 0x4E042E00u)]
+ [DataRow("umov", "w0, v16.b[0]", 0x0E013E00u)]
+ [DataRow("aese", "v0.16b, v1.16b", 0x4E284820u)]
+ [DataRow("crc32b", "w0, w1, w2", 0x1AC24020u)]
+ [DataRow("crc32x", "w0, w1, x2", 0x9AC24C20u)]
+ [DataRow("sdot", "v0.4s, v1.16b, v2.16b", 0x4E829420u)]
public void Decode_SimdFp_MnemonicAndOperands(string mnemonic, string operands, uint word)
{
var insn = One(word);
- Assert.False(insn.IsFallback);
- Assert.Equal(mnemonic, insn.Mnemonic);
- Assert.Equal(operands, insn.OperandText);
- Assert.Equal(4, insn.Length);
+ Assert.IsFalse(insn.IsFallback);
+ Assert.AreEqual(mnemonic, insn.Mnemonic);
+ Assert.AreEqual(operands, insn.OperandText);
+ Assert.AreEqual(4, insn.Length);
}
}
diff --git a/tests/Dotsider.Tests/Arm64SveTests.cs b/tests/Dotsider.Tests/Arm64SveTests.cs
index e138ddf3..8366e749 100644
--- a/tests/Dotsider.Tests/Arm64SveTests.cs
+++ b/tests/Dotsider.Tests/Arm64SveTests.cs
@@ -8,6 +8,7 @@ namespace Dotsider.Tests;
/// registers, predicate generation (ptrue/whilelt), contiguous predicated loads/stores, the
/// predicate-writing compares, movprfx, and the mod-immediate move — cross-checked against Capstone.
///
+[TestClass]
public class Arm64SveTests
{
private static NativeInstruction One(uint word)
@@ -17,35 +18,37 @@ private static NativeInstruction One(uint word)
}
/// Decodes representative SVE instructions with Z/predicate registers and element suffixes.
- [Theory(Timeout = 30_000)]
- [InlineData("add", "z0.s, z0.s, z0.s", 0x04A00000u)]
- [InlineData("sub", "z0.s, z0.s, z0.s", 0x04A00400u)]
- [InlineData("add", "z0.b, p0/m, z0.b, z0.b", 0x04000000u)]
- [InlineData("mul", "z0.b, p0/m, z0.b, z0.b", 0x04100000u)]
- [InlineData("fadd", "z0.s, p0/m, z0.s, z0.s", 0x65808000u)]
- [InlineData("frintn", "z0.s, p0/m, z0.s", 0x6580A000u)]
- [InlineData("ptrue", "p0.s, pow2", 0x2598E000u)]
- [InlineData("whilelt", "p0.b, x0, x0", 0x25201400u)]
- [InlineData("ld1w", "{z0.s}, p0/z, [x0, x0, lsl #2]", 0xA5404000u)]
- [InlineData("st1w", "{z0.s}, p0, [x0, x0, lsl #2]", 0xE5404000u)]
- [InlineData("ld1d", "{z0.d}, p0/z, [x0, x0, lsl #3]", 0xA5E04000u)]
- [InlineData("cmphs", "p0.d, p0/z, z0.d, z0.d", 0x24C00000u)]
- [InlineData("cmpge", "p0.b, p4/z, z0.b, #1", 0x25011000u)]
- [InlineData("movprfx", "z0, z0", 0x0420BC00u)]
- [InlineData("mov", "z0.b, #1", 0x2538C020u)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow("add", "z0.s, z0.s, z0.s", 0x04A00000u)]
+ [DataRow("sub", "z0.s, z0.s, z0.s", 0x04A00400u)]
+ [DataRow("add", "z0.b, p0/m, z0.b, z0.b", 0x04000000u)]
+ [DataRow("mul", "z0.b, p0/m, z0.b, z0.b", 0x04100000u)]
+ [DataRow("fadd", "z0.s, p0/m, z0.s, z0.s", 0x65808000u)]
+ [DataRow("frintn", "z0.s, p0/m, z0.s", 0x6580A000u)]
+ [DataRow("ptrue", "p0.s, pow2", 0x2598E000u)]
+ [DataRow("whilelt", "p0.b, x0, x0", 0x25201400u)]
+ [DataRow("ld1w", "{z0.s}, p0/z, [x0, x0, lsl #2]", 0xA5404000u)]
+ [DataRow("st1w", "{z0.s}, p0, [x0, x0, lsl #2]", 0xE5404000u)]
+ [DataRow("ld1d", "{z0.d}, p0/z, [x0, x0, lsl #3]", 0xA5E04000u)]
+ [DataRow("cmphs", "p0.d, p0/z, z0.d, z0.d", 0x24C00000u)]
+ [DataRow("cmpge", "p0.b, p4/z, z0.b, #1", 0x25011000u)]
+ [DataRow("movprfx", "z0, z0", 0x0420BC00u)]
+ [DataRow("mov", "z0.b, #1", 0x2538C020u)]
public void Decode_Sve_MnemonicAndOperands(string mnemonic, string operands, uint word)
{
var insn = One(word);
- Assert.False(insn.IsFallback);
- Assert.Equal(mnemonic, insn.Mnemonic);
- Assert.Equal(operands, insn.OperandText);
- Assert.Equal(4, insn.Length);
+ Assert.IsFalse(insn.IsFallback);
+ Assert.AreEqual(mnemonic, insn.Mnemonic);
+ Assert.AreEqual(operands, insn.OperandText);
+ Assert.AreEqual(4, insn.Length);
}
/// Verifies SVE instructions classify as Vector.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Decode_Sve_ClassifiesVector()
{
- Assert.Equal(NativeInstructionCategory.Vector, One(0x04A00000u).Category);
+ Assert.AreEqual(NativeInstructionCategory.Vector, One(0x04A00000u).Category);
}
}
diff --git a/tests/Dotsider.Tests/AssemblyAnalyzerTests.cs b/tests/Dotsider.Tests/AssemblyAnalyzerTests.cs
index 3a11d09d..8aa7c725 100644
--- a/tests/Dotsider.Tests/AssemblyAnalyzerTests.cs
+++ b/tests/Dotsider.Tests/AssemblyAnalyzerTests.cs
@@ -7,103 +7,114 @@ namespace Dotsider.Tests;
///
/// Tests for Assembly Analyzer.
///
-[Collection("SampleAssemblies")]
-public class AssemblyAnalyzerTests(SampleAssemblyFixture samples)
+[TestClass]
+public class AssemblyAnalyzerTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
// --- HelloWorld (Exe, minimal) ---
///
/// Verifies hello world has correct name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasCorrectName()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.Equal("HelloWorld.dll", a.FileName);
- Assert.Equal("HelloWorld", a.AssemblyName);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.AreEqual("HelloWorld.dll", a.FileName);
+ Assert.AreEqual("HelloWorld", a.AssemblyName);
}
///
/// Verifies hello world has metadata.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasMetadata()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.True(a.HasMetadata);
- Assert.NotNull(a.GetMetadataReader());
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsTrue(a.HasMetadata);
+ Assert.IsNotNull(a.GetMetadataReader());
}
///
/// Verifies hello world has target framework.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasTargetFramework()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotNull(a.TargetFramework);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsNotNull(a.TargetFramework);
Assert.Contains("10.0", a.TargetFramework);
}
///
/// Verifies hello world has clr header with entry point.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasClrHeaderWithEntryPoint()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotNull(a.ClrHeader);
- Assert.True(a.ClrHeader!.EntryPointToken > 0);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsNotNull(a.ClrHeader);
+ Assert.IsGreaterThan(0, a.ClrHeader!.EntryPointToken);
}
///
/// Verifies hello world has pe headers.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasPeHeaders()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotNull(a.PeHeaders);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsNotNull(a.PeHeaders);
}
///
/// Verifies hello world has text section.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasTextSection()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.Contains(a.Sections, s => s.Name == ".text");
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.Contains(s => s.Name == ".text", a.Sections);
}
///
/// Verifies hello world raw bytes match file size.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_RawBytesMatchFileSize()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.Equal(a.FileSize, a.RawBytes.Length);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.AreEqual(a.FileSize, (long)a.RawBytes.Length);
}
///
/// Verifies hello world has type defs.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasTypeDefs()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotEmpty(a.TypeDefs);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsNotEmpty(a.TypeDefs);
}
///
/// Verifies hello world has method defs.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasMethodDefs()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotEmpty(a.MethodDefs);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsNotEmpty(a.MethodDefs);
}
// --- RichLibrary (Library, NuGet deps) ---
@@ -111,88 +122,96 @@ public void HelloWorld_HasMethodDefs()
///
/// Verifies rich library has correct version.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasCorrectVersion()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.NotNull(a.AssemblyVersion);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsNotNull(a.AssemblyVersion);
Assert.Contains("2.5.1", a.AssemblyVersion);
}
///
/// Verifies rich library has newton soft ref.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasNewtonSoftRef()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.Contains(a.AssemblyRefs, r => r.Name == "Newtonsoft.Json");
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.Contains(r => r.Name == "Newtonsoft.Json", a.AssemblyRefs);
}
///
/// Verifies rich library has system text json ref.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasSystemTextJsonRef()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.Contains(a.AssemblyRefs, r => r.Name == "System.Text.Json");
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.Contains(r => r.Name == "System.Text.Json", a.AssemblyRefs);
}
///
/// Verifies rich library has service types.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasServiceTypes()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.Contains(a.TypeDefs, t => t.FullName == "RichLibrary.Services.UserService");
- Assert.Contains(a.TypeDefs, t => t.FullName == "RichLibrary.Services.ProductCatalog");
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.Contains(t => t.FullName == "RichLibrary.Services.UserService", a.TypeDefs);
+ Assert.Contains(t => t.FullName == "RichLibrary.Services.ProductCatalog", a.TypeDefs);
}
///
/// Verifies rich library has no entry point.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasNoEntryPoint()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.NotNull(a.ClrHeader);
- Assert.Equal(0, a.ClrHeader!.EntryPointToken);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsNotNull(a.ClrHeader);
+ Assert.AreEqual(0, a.ClrHeader!.EntryPointToken);
}
///
/// Verifies rich library has many methods.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasManyMethods()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.True(a.MethodDefs.Count > 10);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsGreaterThan(10, a.MethodDefs.Count);
}
///
/// Verifies rich library opens its matching portable PDB sidecar.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_OpensPortablePdbSidecar()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
- Assert.True(a.HasPortablePdb);
- Assert.Equal(PdbProvenanceKind.Sidecar, a.PdbProvenance.Kind);
- Assert.NotNull(a.PdbProvenance.Path);
- Assert.NotNull(a.GetPdbReader());
- Assert.Contains(a.DebugDirectory, entry => entry.Type == DebugDirectoryEntryType.CodeView);
+ Assert.IsTrue(a.HasPortablePdb);
+ Assert.AreEqual(PdbProvenanceKind.Sidecar, a.PdbProvenance.Kind);
+ Assert.IsNotNull(a.PdbProvenance.Path);
+ Assert.IsNotNull(a.GetPdbReader());
+ Assert.Contains(entry => entry.Type == DebugDirectoryEntryType.CodeView, a.DebugDirectory);
}
///
/// Verifies rich library source link mappings resolve method documents.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_SourceLink_ResolvesMethodDocument()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var method = FindMethod(a, "RichLibrary.Services.UserService", "Add");
var debugInfo = a.GetMethodDebugInfo(method);
var document = debugInfo.SequencePoints
@@ -201,9 +220,9 @@ public void RichLibrary_SourceLink_ResolvesMethodDocument()
var url = a.ResolveSourceLinkUrl(document);
- Assert.True(a.SourceLink.IsPresent);
- Assert.NotEmpty(a.SourceLink.Mappings);
- Assert.NotNull(url);
+ Assert.IsTrue(a.SourceLink.IsPresent);
+ Assert.IsNotEmpty(a.SourceLink.Mappings);
+ Assert.IsNotNull(url);
Assert.Contains("raw.githubusercontent.com", url);
Assert.EndsWith("UserService.cs", url, StringComparison.OrdinalIgnoreCase);
}
@@ -211,20 +230,20 @@ public void RichLibrary_SourceLink_ResolvesMethodDocument()
///
/// Verifies method debug info exposes sequence points and PDB local names.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_MethodDebugInfo_IncludesSequencePointsAndLocals()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var method = FindMethod(a, "RichLibrary.Services.UserService", "Add");
var debugInfo = a.GetMethodDebugInfo(method);
- Assert.Equal(PdbProvenanceKind.Sidecar, debugInfo.Pdb.Kind);
- Assert.Contains(debugInfo.SequencePoints,
- point => point.Document?.EndsWith("UserService.cs", StringComparison.OrdinalIgnoreCase) == true
- && point.SourceLinkUrl is not null);
- Assert.Contains(debugInfo.Locals, local => local.Name == "id");
- Assert.Contains(debugInfo.Locals, local => local.Name == "user");
+ Assert.AreEqual(PdbProvenanceKind.Sidecar, debugInfo.Pdb.Kind);
+ Assert.Contains(point => point.Document?.EndsWith("UserService.cs", StringComparison.OrdinalIgnoreCase) == true
+ && point.SourceLinkUrl is not null, debugInfo.SequencePoints);
+ Assert.Contains(local => local.Name == "id", debugInfo.Locals);
+ Assert.Contains(local => local.Name == "user", debugInfo.Locals);
}
// --- ComplexApp (Exe, embedded resources) ---
@@ -232,22 +251,24 @@ public void RichLibrary_MethodDebugInfo_IncludesSequencePointsAndLocals()
///
/// Verifies complex app has embedded resources.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ComplexApp_HasEmbeddedResources()
{
- using var a = new AssemblyAnalyzer(samples.ComplexAppDll);
- Assert.Contains(a.Resources, r => r.Name.Contains("config.json"));
- Assert.Contains(a.Resources, r => r.Name.Contains("banner.txt"));
+ using var a = new AssemblyAnalyzer(Samples.ComplexAppDll);
+ Assert.Contains(r => r.Name.Contains("config.json"), a.Resources);
+ Assert.Contains(r => r.Name.Contains("banner.txt"), a.Resources);
}
///
/// Verifies complex app has version.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ComplexApp_HasVersion()
{
- using var a = new AssemblyAnalyzer(samples.ComplexAppDll);
- Assert.NotNull(a.AssemblyVersion);
+ using var a = new AssemblyAnalyzer(Samples.ComplexAppDll);
+ Assert.IsNotNull(a.AssemblyVersion);
Assert.Contains("1.0.0", a.AssemblyVersion);
}
@@ -256,23 +277,25 @@ public void ComplexApp_HasVersion()
///
/// Verifies minimal api has asp net refs.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void MinimalApi_HasAspNetRefs()
{
- using var a = new AssemblyAnalyzer(samples.MinimalApiDll);
+ using var a = new AssemblyAnalyzer(Samples.MinimalApiDll);
// Web SDK assemblies reference ASP.NET Core packages
- Assert.True(a.AssemblyRefs.Count > 0);
+ Assert.IsGreaterThan(0, a.AssemblyRefs.Count);
}
///
/// Verifies minimal api has record types.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void MinimalApi_HasRecordTypes()
{
- using var a = new AssemblyAnalyzer(samples.MinimalApiDll);
- Assert.Contains(a.TypeDefs, t => t.Name == "GreetingResponse");
- Assert.Contains(a.TypeDefs, t => t.Name == "EchoRequest");
+ using var a = new AssemblyAnalyzer(Samples.MinimalApiDll);
+ Assert.Contains(t => t.Name == "GreetingResponse", a.TypeDefs);
+ Assert.Contains(t => t.Name == "EchoRequest", a.TypeDefs);
}
// --- NativeLib (unsafe, P/Invoke) ---
@@ -280,22 +303,24 @@ public void MinimalApi_HasRecordTypes()
///
/// Verifies native lib has p invoke methods.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeLib_HasPInvokeMethods()
{
- using var a = new AssemblyAnalyzer(samples.NativeLibDll);
- Assert.Contains(a.TypeDefs, t => t.FullName == "NativeLib.NativeInterop");
- Assert.Contains(a.TypeDefs, t => t.FullName == "NativeLib.UnsafeOperations");
+ using var a = new AssemblyAnalyzer(Samples.NativeLibDll);
+ Assert.Contains(t => t.FullName == "NativeLib.NativeInterop", a.TypeDefs);
+ Assert.Contains(t => t.FullName == "NativeLib.UnsafeOperations", a.TypeDefs);
}
///
/// Verifies native lib has fixed buffer struct.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeLib_HasFixedBufferStruct()
{
- using var a = new AssemblyAnalyzer(samples.NativeLibDll);
- Assert.Contains(a.TypeDefs, t => t.Name == "FixedBuffer");
+ using var a = new AssemblyAnalyzer(Samples.NativeLibDll);
+ Assert.Contains(t => t.Name == "FixedBuffer", a.TypeDefs);
}
// --- EmptyLib (minimal) ---
@@ -303,45 +328,48 @@ public void NativeLib_HasFixedBufferStruct()
///
/// Verifies empty lib has minimal type defs.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EmptyLib_HasMinimalTypeDefs()
{
- using var a = new AssemblyAnalyzer(samples.EmptyLibDll);
+ using var a = new AssemblyAnalyzer(Samples.EmptyLibDll);
// + internal Module class
- Assert.True(a.TypeDefs.Count <= 3);
- Assert.True(a.TypeDefs.Count >= 1);
+ Assert.IsLessThanOrEqualTo(3, a.TypeDefs.Count);
+ Assert.IsGreaterThanOrEqualTo(1, a.TypeDefs.Count);
}
///
/// Verifies empty lib has metadata.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EmptyLib_HasMetadata()
{
- using var a = new AssemblyAnalyzer(samples.EmptyLibDll);
- Assert.True(a.HasMetadata);
- Assert.NotNull(a.ClrHeader);
+ using var a = new AssemblyAnalyzer(Samples.EmptyLibDll);
+ Assert.IsTrue(a.HasMetadata);
+ Assert.IsNotNull(a.ClrHeader);
}
///
/// Verifies embedded source can be decoded from an embedded portable PDB.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EmbeddedSourceLib_DecodesEmbeddedSource()
{
- using var a = new AssemblyAnalyzer(samples.EmbeddedSourceLibDll);
+ using var a = new AssemblyAnalyzer(Samples.EmbeddedSourceLibDll);
var method = FindMethod(a, "EmbeddedSourceLib.EmbeddedSourceFixture", "Compute");
var debugInfo = a.GetMethodDebugInfo(method);
var source = a.GetEmbeddedSource(method);
- Assert.True(a.HasPortablePdb);
- Assert.Equal(PdbProvenanceKind.Embedded, a.PdbProvenance.Kind);
- Assert.Contains(a.DebugDirectory, entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb);
- Assert.Contains(debugInfo.SequencePoints, point => point.HasEmbeddedSource);
- Assert.NotNull(source);
+ Assert.IsTrue(a.HasPortablePdb);
+ Assert.AreEqual(PdbProvenanceKind.Embedded, a.PdbProvenance.Kind);
+ Assert.Contains(entry => entry.Type == DebugDirectoryEntryType.EmbeddedPortablePdb, a.DebugDirectory);
+ Assert.Contains(point => point.HasEmbeddedSource, debugInfo.SequencePoints);
+ Assert.IsNotNull(source);
Assert.Contains("return doubled + 1;", source.Text);
- Assert.NotEmpty(source.Bytes);
+ Assert.IsNotEmpty(source.Bytes);
}
// --- RichLibraryV2 (same AssemblyName as V1) ---
@@ -349,33 +377,36 @@ public void EmbeddedSourceLib_DecodesEmbeddedSource()
///
/// Verifies rich library v2 has same assembly name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibraryV2_HasSameAssemblyName()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
- Assert.Equal("RichLibrary", a.AssemblyName);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
+ Assert.AreEqual("RichLibrary", a.AssemblyName);
}
///
/// Verifies rich library v2 has version3.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibraryV2_HasVersion3()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
- Assert.NotNull(a.AssemblyVersion);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
+ Assert.IsNotNull(a.AssemblyVersion);
Assert.Contains("3.0.0", a.AssemblyVersion);
}
///
/// Verifies rich library v2 has new types.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibraryV2_HasNewTypes()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
- Assert.Contains(a.TypeDefs, t => t.Name == "Order");
- Assert.Contains(a.TypeDefs, t => t.Name == "OrderService");
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
+ Assert.Contains(t => t.Name == "Order", a.TypeDefs);
+ Assert.Contains(t => t.Name == "OrderService", a.TypeDefs);
}
// --- Cross-assembly metadata checks ---
@@ -383,46 +414,49 @@ public void RichLibraryV2_HasNewTypes()
///
/// Verifies all samples have custom attributes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllSamples_HaveCustomAttributes()
{
- string[] paths = [samples.HelloWorldDll, samples.RichLibraryDll, samples.ComplexAppDll,
- samples.MinimalApiDll, samples.NativeLibDll];
+ string[] paths = [Samples.HelloWorldDll, Samples.RichLibraryDll, Samples.ComplexAppDll,
+ Samples.MinimalApiDll, Samples.NativeLibDll];
foreach (var path in paths)
{
using var a = new AssemblyAnalyzer(path);
- Assert.NotEmpty(a.CustomAttributes);
+ Assert.IsNotEmpty(a.CustomAttributes);
}
}
///
/// Verifies all samples have positive file size.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllSamples_HavePositiveFileSize()
{
- string[] paths = [samples.HelloWorldDll, samples.RichLibraryDll, samples.ComplexAppDll,
- samples.MinimalApiDll, samples.NativeLibDll, samples.EmptyLibDll];
+ string[] paths = [Samples.HelloWorldDll, Samples.RichLibraryDll, Samples.ComplexAppDll,
+ Samples.MinimalApiDll, Samples.NativeLibDll, Samples.EmptyLibDll];
foreach (var path in paths)
{
using var a = new AssemblyAnalyzer(path);
- Assert.True(a.FileSize > 0);
+ Assert.IsGreaterThan(0, a.FileSize);
}
}
///
/// Verifies all samples have clr header.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllSamples_HaveClrHeader()
{
- string[] paths = [samples.HelloWorldDll, samples.RichLibraryDll, samples.ComplexAppDll,
- samples.MinimalApiDll, samples.NativeLibDll, samples.EmptyLibDll];
+ string[] paths = [Samples.HelloWorldDll, Samples.RichLibraryDll, Samples.ComplexAppDll,
+ Samples.MinimalApiDll, Samples.NativeLibDll, Samples.EmptyLibDll];
foreach (var path in paths)
{
using var a = new AssemblyAnalyzer(path);
- Assert.NotNull(a.ClrHeader);
- Assert.True(a.ClrHeader!.MetadataSize > 0);
+ Assert.IsNotNull(a.ClrHeader);
+ Assert.IsGreaterThan(0, a.ClrHeader!.MetadataSize);
}
}
@@ -431,22 +465,24 @@ public void AllSamples_HaveClrHeader()
///
/// Verifies get method body returns non null for method with il.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetMethodBody_ReturnsNonNull_ForMethodWithIl()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var method = a.MethodDefs.First(m => m.Rva != 0);
var body = a.GetMethodBody(method);
- Assert.NotNull(body);
+ Assert.IsNotNull(body);
}
///
/// Verifies Dispose can be called multiple times without side effects.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Dispose_IsIdempotent()
{
- var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
a.Dispose();
a.Dispose(); // should not throw
}
@@ -454,97 +490,105 @@ public void Dispose_IsIdempotent()
///
/// Verifies invalid file path throws file not found.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void InvalidFilePath_ThrowsFileNotFound()
{
var path = Path.Combine(Path.GetTempPath(), "nonexistent-dotsider-test-" + Guid.NewGuid() + ".dll");
- Assert.Throws(() => new AssemblyAnalyzer(path));
+ Assert.ThrowsExactly(() => new AssemblyAnalyzer(path));
}
///
/// Verifies non dot net binary throws bad image format.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NonDotNetBinary_ThrowsBadImageFormat()
{
- Assert.ThrowsAny(() => new AssemblyAnalyzer(samples.NonDotNetBinaryPath));
+ Assert.Throws(() => new AssemblyAnalyzer(Samples.NonDotNetBinaryPath));
}
///
/// Verifies native apphost file constructor tolerates non pe binary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeApphost_FileConstructor_ToleratesNonPeBinary()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldExe);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldExe);
- Assert.False(a.HasMetadata);
- Assert.True(a.FileSize > 0);
- Assert.False(a.RawBytes.IsEmpty);
+ Assert.IsFalse(a.HasMetadata);
+ Assert.IsGreaterThan(0, a.FileSize);
+ Assert.IsFalse(a.RawBytes.IsEmpty);
}
///
/// Verifies native apphost byte array constructor tolerates non pe binary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeApphost_ByteArrayConstructor_ToleratesNonPeBinary()
{
- var bytes = File.ReadAllBytes(samples.HelloWorldExe);
- using var a = new AssemblyAnalyzer(bytes, samples.HelloWorldExe);
+ var bytes = File.ReadAllBytes(Samples.HelloWorldExe);
+ using var a = new AssemblyAnalyzer(bytes, Samples.HelloWorldExe);
- Assert.False(a.HasMetadata);
- Assert.Equal(bytes.Length, a.FileSize);
- Assert.False(a.RawBytes.IsEmpty);
+ Assert.IsFalse(a.HasMetadata);
+ Assert.AreEqual(bytes.Length, a.FileSize);
+ Assert.IsFalse(a.RawBytes.IsEmpty);
}
///
/// Verifies native aot byte array constructor tolerates non pe binary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeAot_ByteArrayConstructor_ToleratesNonPeBinary()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null,
+ TestSkip.When(Samples.NativeAotConsoleExe is null,
"NativeAOT sample was not built");
- var bytes = File.ReadAllBytes(samples.NativeAotConsoleExe!);
- using var a = new AssemblyAnalyzer(bytes, samples.NativeAotConsoleExe!);
+ var bytes = File.ReadAllBytes(Samples.NativeAotConsoleExe!);
+ using var a = new AssemblyAnalyzer(bytes, Samples.NativeAotConsoleExe!);
- Assert.False(a.HasMetadata);
- Assert.Equal(bytes.Length, a.FileSize);
+ Assert.IsFalse(a.HasMetadata);
+ Assert.AreEqual(bytes.Length, a.FileSize);
}
///
/// Verifies resolve token returns string.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveToken_ReturnsString()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var method = a.MethodDefs[0];
var result = a.ResolveToken(method.Token);
- Assert.NotNull(result);
- Assert.NotEmpty(result);
+ Assert.IsNotNull(result);
+ Assert.IsNotEmpty(result);
}
///
/// Verifies architecture is not unknown.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Architecture_IsNotUnknown()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotEqual("Unknown", a.Architecture);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.AreNotEqual("Unknown", a.Architecture);
}
///
/// Verifies file properties are populated.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FileProperties_ArePopulated()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.NotEqual(default, a.LastModified);
- Assert.NotEqual(default, a.CreatedTime);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.AreNotEqual(default, a.LastModified);
+ Assert.AreNotEqual(default, a.CreatedTime);
}
// --- Additional coverage tests ---
@@ -552,121 +596,131 @@ public void FileProperties_ArePopulated()
///
/// Verifies file path is absolute.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FilePath_IsAbsolute()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.Equal(samples.HelloWorldDll, a.FilePath);
- Assert.True(Path.IsPathRooted(a.FilePath));
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.AreEqual(Samples.HelloWorldDll, a.FilePath);
+ Assert.IsTrue(Path.IsPathRooted(a.FilePath));
}
///
/// Verifies rich library type refs non empty.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_TypeRefs_NonEmpty()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.NotEmpty(a.TypeRefs);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsNotEmpty(a.TypeRefs);
}
///
/// Verifies rich library member refs non empty.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_MemberRefs_NonEmpty()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.NotEmpty(a.MemberRefs);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsNotEmpty(a.MemberRefs);
var first = a.MemberRefs[0];
- Assert.NotNull(first.Name);
- Assert.NotNull(first.DeclaringType);
+ Assert.IsNotNull(first.Name);
+ Assert.IsNotNull(first.DeclaringType);
}
///
/// Verifies complex app resources have offsets.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ComplexApp_Resources_HaveOffsets()
{
- using var a = new AssemblyAnalyzer(samples.ComplexAppDll);
+ using var a = new AssemblyAnalyzer(Samples.ComplexAppDll);
foreach (var r in a.Resources)
{
- Assert.NotNull(r.Name);
- Assert.True(r.Size >= 0);
+ Assert.IsNotNull(r.Name);
+ Assert.IsGreaterThanOrEqualTo(0, r.Size);
}
}
///
/// Verifies rich library culture is neutral.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_Culture_IsNeutral()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.Equal("neutral", a.Culture);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.AreEqual("neutral", a.Culture);
}
///
/// Verifies hello world is read only is false.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_IsReadOnly_IsFalse()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
- Assert.False(a.IsReadOnly);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
+ Assert.IsFalse(a.IsReadOnly);
}
///
/// Verifies rich library type defs have properties.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_TypeDefs_HaveProperties()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var userService = a.TypeDefs.First(t => t.Name == "UserService");
- Assert.NotNull(userService.FullName);
- Assert.NotNull(userService.Namespace);
- Assert.True(userService.Token > 0);
+ Assert.IsNotNull(userService.FullName);
+ Assert.IsNotNull(userService.Namespace);
+ Assert.IsGreaterThan(0, userService.Token);
}
///
/// Verifies rich library method defs have signatures.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_MethodDefs_HaveSignatures()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var methods = a.MethodDefs.Where(m => m.Rva != 0).Take(5);
foreach (var m in methods)
{
- Assert.NotNull(m.Name);
- Assert.NotNull(m.Signature);
- Assert.True(m.Token > 0);
+ Assert.IsNotNull(m.Name);
+ Assert.IsNotNull(m.Signature);
+ Assert.IsGreaterThan(0, m.Token);
}
}
///
/// Verifies rich library type refs have properties.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_TypeRefs_HaveProperties()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
foreach (var tr in a.TypeRefs.Take(5))
{
- Assert.NotNull(tr.Name);
- Assert.NotNull(tr.Namespace);
+ Assert.IsNotNull(tr.Name);
+ Assert.IsNotNull(tr.Namespace);
}
}
///
/// Verifies resolve token type def.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveToken_TypeDef()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var typeDef = a.TypeDefs.First(t => t.Name != "");
var resolved = a.ResolveToken(typeDef.Token);
Assert.Contains(typeDef.Name, resolved);
@@ -675,34 +729,37 @@ public void ResolveToken_TypeDef()
///
/// Verifies resolve token type ref.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveToken_TypeRef()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var typeRef = a.TypeRefs[0];
var resolved = a.ResolveToken(typeRef.Token);
- Assert.NotEmpty(resolved);
+ Assert.IsNotEmpty(resolved);
}
///
/// Verifies resolve token member ref.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveToken_MemberRef()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var memberRef = a.MemberRefs[0];
var resolved = a.ResolveToken(memberRef.Token);
- Assert.NotEmpty(resolved);
+ Assert.IsNotEmpty(resolved);
}
///
/// Verifies resolve token invalid token returns hex string.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveToken_InvalidToken_ReturnsHexString()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var result = a.ResolveToken(0x7F000001);
Assert.Contains("0x", result);
}
@@ -710,182 +767,195 @@ public void ResolveToken_InvalidToken_ReturnsHexString()
///
/// Verifies rich library assembly refs have version and token.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_AssemblyRefs_HaveVersionAndToken()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var newtonsoftRef = a.AssemblyRefs.First(r => r.Name == "Newtonsoft.Json");
- Assert.NotNull(newtonsoftRef.Version);
- Assert.NotNull(newtonsoftRef.PublicKeyToken);
- Assert.NotNull(newtonsoftRef.Culture);
+ Assert.IsNotNull(newtonsoftRef.Version);
+ Assert.IsNotNull(newtonsoftRef.PublicKeyToken);
+ Assert.IsNotNull(newtonsoftRef.Culture);
}
///
/// Verifies rich library custom attributes have properties.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_CustomAttributes_HaveProperties()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
foreach (var attr in a.CustomAttributes.Take(3))
{
- Assert.NotNull(attr.Constructor);
- Assert.NotNull(attr.Parent);
+ Assert.IsNotNull(attr.Constructor);
+ Assert.IsNotNull(attr.Parent);
}
}
///
/// Verifies hello world get method body returns non null body.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_GetMethodBody_ReturnsNonNullBody()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var method = a.MethodDefs.First(m => m.Rva != 0);
var body = a.GetMethodBody(method);
- Assert.NotNull(body);
- Assert.True(body!.GetILBytes()!.Length > 0);
+ Assert.IsNotNull(body);
+ Assert.IsGreaterThan(0, body!.GetILBytes()!.Length);
}
///
/// Verifies rich library sections have properties.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_Sections_HaveProperties()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var textSection = a.Sections.First(s => s.Name == ".text");
- Assert.True(textSection.RawDataSize > 0);
- Assert.True(textSection.VirtualAddress > 0);
+ Assert.IsGreaterThan(0, textSection.RawDataSize);
+ Assert.IsGreaterThan(0, textSection.VirtualAddress);
}
///
/// Verifies rich library pe headers have valid fields.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_PeHeaders_HaveValidFields()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.NotNull(a.PeHeaders);
- Assert.NotEqual((Machine)0, a.PeHeaders!.Machine);
- Assert.NotEqual((Characteristics)0, a.PeHeaders.Characteristics);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsNotNull(a.PeHeaders);
+ Assert.AreNotEqual((Machine)0, a.PeHeaders!.Machine);
+ Assert.AreNotEqual((Characteristics)0, a.PeHeaders.Characteristics);
}
///
/// Verifies access after dispose assembly refs throws instead of crashing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AccessAfterDispose_AssemblyRefs_ThrowsInsteadOfCrashing()
{
- var a = new AssemblyAnalyzer(samples.RichLibraryDll);
- Assert.True(a.HasMetadata);
+ var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ Assert.IsTrue(a.HasMetadata);
a.Dispose();
// After Dispose, accessing AssemblyRefs must throw ObjectDisposedException
// rather than crashing the process with AccessViolationException from
// reading freed metadata memory.
- Assert.Throws(() => _ = a.AssemblyRefs);
+ Assert.ThrowsExactly(() => _ = a.AssemblyRefs);
}
///
/// Verifies access after dispose type defs throws instead of crashing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AccessAfterDispose_TypeDefs_ThrowsInsteadOfCrashing()
{
- var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
a.Dispose();
- Assert.Throws(() => _ = a.TypeDefs);
+ Assert.ThrowsExactly(() => _ = a.TypeDefs);
}
///
/// Verifies access after dispose method defs throws instead of crashing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AccessAfterDispose_MethodDefs_ThrowsInsteadOfCrashing()
{
- var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
a.Dispose();
- Assert.Throws(() => _ = a.MethodDefs);
+ Assert.ThrowsExactly(() => _ = a.MethodDefs);
}
///
/// Verifies access after dispose field defs throws instead of crashing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AccessAfterDispose_FieldDefs_ThrowsInsteadOfCrashing()
{
- var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
a.Dispose();
- Assert.Throws(() => _ = a.FieldDefs);
+ Assert.ThrowsExactly(() => _ = a.FieldDefs);
}
///
/// Verifies access after dispose get method body throws instead of crashing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AccessAfterDispose_GetMethodBody_ThrowsInsteadOfCrashing()
{
- var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var method = a.MethodDefs.First(m => m.Rva > 0);
a.Dispose();
- Assert.Throws(() => a.GetMethodBody(method));
+ Assert.ThrowsExactly(() => a.GetMethodBody(method));
}
///
/// Verifies access after dispose resolve token throws instead of crashing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AccessAfterDispose_ResolveToken_ThrowsInsteadOfCrashing()
{
- var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var token = a.MethodDefs.First(m => m.Rva > 0).Token;
a.Dispose();
- Assert.Throws(() => a.ResolveToken(token));
+ Assert.ThrowsExactly(() => a.ResolveToken(token));
}
///
/// Verifies the analyzer finds and parses the mstat and DGML sidecars published next to
/// the Native AOT sample, preferring the codegen graph.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Sidecars_NativeAotExe_ProbeAndParse()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- using var a = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ using var a = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
- Assert.Equal(samples.NativeAotConsoleMstat, a.MstatPath);
- Assert.NotNull(a.Mstat);
- Assert.NotEmpty(a.Mstat.Methods);
+ Assert.AreEqual(Samples.NativeAotConsoleMstat, a.MstatPath);
+ Assert.IsNotNull(a.Mstat);
+ Assert.IsNotEmpty(a.Mstat.Methods);
- if (samples.NativeAotConsoleDgml is not null)
+ if (Samples.NativeAotConsoleDgml is not null)
{
- Assert.Equal(samples.NativeAotConsoleDgml, a.DgmlPath);
- Assert.NotNull(a.Dgml);
- Assert.NotEmpty(a.Dgml.Nodes);
+ Assert.AreEqual(Samples.NativeAotConsoleDgml, a.DgmlPath);
+ Assert.IsNotNull(a.Dgml);
+ Assert.IsNotEmpty(a.Dgml.Nodes);
}
}
///
/// Verifies an AOT binary with no sidecars beside it probes to null without throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Sidecars_NativeAotExeWithoutSidecars_ReturnNull()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
var dir = Directory.CreateTempSubdirectory("dotsider-nosidecar-");
try
{
- var exeCopy = Path.Combine(dir.FullName, Path.GetFileName(samples.NativeAotConsoleExe!));
- File.Copy(samples.NativeAotConsoleExe!, exeCopy);
+ var exeCopy = Path.Combine(dir.FullName, Path.GetFileName(Samples.NativeAotConsoleExe!));
+ File.Copy(Samples.NativeAotConsoleExe!, exeCopy);
using var a = new AssemblyAnalyzer(exeCopy);
- Assert.Null(a.MstatPath);
- Assert.Null(a.Mstat);
- Assert.Null(a.DgmlPath);
- Assert.Null(a.Dgml);
+ Assert.IsNull(a.MstatPath);
+ Assert.IsNull(a.Mstat);
+ Assert.IsNull(a.DgmlPath);
+ Assert.IsNull(a.Dgml);
}
finally
{
@@ -897,21 +967,22 @@ public void Sidecars_NativeAotExeWithoutSidecars_ReturnNull()
/// Verifies a managed assembly never probes for sidecars, even when a stray mstat sits
/// next to it — the binary-kind gate short-circuits first.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Sidecars_ManagedAssemblyWithStrayMstat_ReturnsNull()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
var dir = Directory.CreateTempSubdirectory("dotsider-stray-");
try
{
var dllCopy = Path.Combine(dir.FullName, "RichLibrary.dll");
- File.Copy(samples.RichLibraryDll, dllCopy);
- File.Copy(samples.NativeAotConsoleMstat!, Path.Combine(dir.FullName, "RichLibrary.mstat"));
+ File.Copy(Samples.RichLibraryDll, dllCopy);
+ File.Copy(Samples.NativeAotConsoleMstat!, Path.Combine(dir.FullName, "RichLibrary.mstat"));
using var a = new AssemblyAnalyzer(dllCopy);
- Assert.Null(a.MstatPath);
- Assert.Null(a.Mstat);
+ Assert.IsNull(a.MstatPath);
+ Assert.IsNull(a.Mstat);
}
finally
{
@@ -922,23 +993,24 @@ public void Sidecars_ManagedAssemblyWithStrayMstat_ReturnsNull()
///
/// Verifies a corrupt sidecar reads as null rather than throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Sidecars_CorruptMstat_ReturnsNull()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
var dir = Directory.CreateTempSubdirectory("dotsider-corrupt-");
try
{
- var name = Path.GetFileName(samples.NativeAotConsoleExe!);
+ var name = Path.GetFileName(Samples.NativeAotConsoleExe!);
var exeCopy = Path.Combine(dir.FullName, name);
- File.Copy(samples.NativeAotConsoleExe!, exeCopy);
+ File.Copy(Samples.NativeAotConsoleExe!, exeCopy);
var stem = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name[..^4] : name;
File.WriteAllBytes(Path.Combine(dir.FullName, stem + ".mstat"), [0xDE, 0xAD]);
using var a = new AssemblyAnalyzer(exeCopy);
- Assert.NotNull(a.MstatPath);
- Assert.Null(a.Mstat);
+ Assert.IsNotNull(a.MstatPath);
+ Assert.IsNull(a.Mstat);
}
finally
{
@@ -950,39 +1022,41 @@ public void Sidecars_CorruptMstat_ReturnsNull()
/// Verifies a Native AOT binary with its matching PDB beside it is marked NativePdb, carrying
/// the PDB path, rather than the UnsupportedWindowsPdb it used to report.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Provenance_NativeAotExeWithMatchingPdb_IsNativePdb()
{
- Assert.SkipWhen(
- samples.NativeAotConsoleSymbols is null
- || !samples.NativeAotConsoleSymbols.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase),
+ TestSkip.When(
+ Samples.NativeAotConsoleSymbols is null
+ || !Samples.NativeAotConsoleSymbols.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase),
"native PDB not present on this platform");
- using var a = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ using var a = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
- Assert.Equal(PdbProvenanceKind.NativePdb, a.PdbProvenance.Kind);
- Assert.Equal(samples.NativeAotConsoleSymbols, a.PdbProvenance.Path);
+ Assert.AreEqual(PdbProvenanceKind.NativePdb, a.PdbProvenance.Kind);
+ Assert.AreEqual(Samples.NativeAotConsoleSymbols, a.PdbProvenance.Path);
}
///
/// Verifies a Native AOT binary copied away from its PDB falls back to UnsupportedWindowsPdb.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Provenance_NativeAotExeWithoutPdb_IsUnsupportedWindowsPdb()
{
- Assert.SkipWhen(
- samples.NativeAotConsoleSymbols is null
- || !samples.NativeAotConsoleSymbols.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase),
+ TestSkip.When(
+ Samples.NativeAotConsoleSymbols is null
+ || !Samples.NativeAotConsoleSymbols.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase),
"native PDB not present on this platform");
var dir = Directory.CreateTempSubdirectory("dotsider-nopdb-");
try
{
- var exeCopy = Path.Combine(dir.FullName, Path.GetFileName(samples.NativeAotConsoleExe!));
- File.Copy(samples.NativeAotConsoleExe!, exeCopy);
+ var exeCopy = Path.Combine(dir.FullName, Path.GetFileName(Samples.NativeAotConsoleExe!));
+ File.Copy(Samples.NativeAotConsoleExe!, exeCopy);
using var a = new AssemblyAnalyzer(exeCopy);
- Assert.Equal(PdbProvenanceKind.UnsupportedWindowsPdb, a.PdbProvenance.Kind);
+ Assert.AreEqual(PdbProvenanceKind.UnsupportedWindowsPdb, a.PdbProvenance.Kind);
}
finally
{
@@ -994,31 +1068,32 @@ samples.NativeAotConsoleSymbols is null
/// Verifies a PDB whose GUID does not match the binary is rejected as UnsupportedWindowsPdb,
/// not accepted as a native match.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Provenance_NativeAotExeWithWrongPdb_IsUnsupportedWindowsPdb()
{
- Assert.SkipWhen(
- samples.NativeAotConsoleSymbols is null
- || !samples.NativeAotConsoleSymbols.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase),
+ TestSkip.When(
+ Samples.NativeAotConsoleSymbols is null
+ || !Samples.NativeAotConsoleSymbols.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase),
"native PDB not present on this platform");
var dir = Directory.CreateTempSubdirectory("dotsider-wrongpdb-");
try
{
- var name = Path.GetFileName(samples.NativeAotConsoleExe!);
+ var name = Path.GetFileName(Samples.NativeAotConsoleExe!);
var exeCopy = Path.Combine(dir.FullName, name);
- File.Copy(samples.NativeAotConsoleExe!, exeCopy);
+ File.Copy(Samples.NativeAotConsoleExe!, exeCopy);
var stem = name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? name[..^4] : name;
// A valid PDB whose GUID no longer matches the binary: flip the info-stream GUID bytes
// in a copy of the real PDB, leaving the container otherwise intact.
- var pdb = File.ReadAllBytes(samples.NativeAotConsoleSymbols!);
+ var pdb = File.ReadAllBytes(Samples.NativeAotConsoleSymbols!);
MutatePdbGuid(pdb);
File.WriteAllBytes(Path.Combine(dir.FullName, stem + ".pdb"), pdb);
using var a = new AssemblyAnalyzer(exeCopy);
- Assert.Equal(PdbProvenanceKind.UnsupportedWindowsPdb, a.PdbProvenance.Kind);
+ Assert.AreEqual(PdbProvenanceKind.UnsupportedWindowsPdb, a.PdbProvenance.Kind);
}
finally
{
@@ -1060,7 +1135,7 @@ private static MethodDefInfo FindMethod(AssemblyAnalyzer analyzer, string typeNa
m.DeclaringType == typeName
&& m.Name == methodName);
- Assert.NotNull(method);
+ Assert.IsNotNull(method);
return method;
}
}
diff --git a/tests/Dotsider.Tests/AssemblyDifferTests.cs b/tests/Dotsider.Tests/AssemblyDifferTests.cs
index 1341b76e..c7e01ddc 100644
--- a/tests/Dotsider.Tests/AssemblyDifferTests.cs
+++ b/tests/Dotsider.Tests/AssemblyDifferTests.cs
@@ -6,138 +6,150 @@ namespace Dotsider.Tests;
///
/// Tests for Assembly Differ.
///
-[Collection("SampleAssemblies")]
-public class AssemblyDifferTests(SampleAssemblyFixture samples)
+[TestClass]
+public class AssemblyDifferTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Verifies rich library v1vs v2 has non empty diff.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibraryV1vsV2_HasNonEmptyDiff()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.NotEmpty(result.TypeDiffs);
- Assert.NotEmpty(result.MethodDiffs);
+ Assert.IsNotEmpty(result.TypeDiffs);
+ Assert.IsNotEmpty(result.MethodDiffs);
}
///
/// Verifies v1vs v2 type diffs i repository removed.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_TypeDiffs_IRepositoryRemoved()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.Contains(result.TypeDiffs, d =>
- d.Kind == DiffKind.Removed && d.Left!.Name.Contains("IRepository"));
+ Assert.Contains(d =>
+ d.Kind == DiffKind.Removed && d.Left!.Name.Contains("IRepository"), result.TypeDiffs);
}
///
/// Verifies v1vs v2 type diffs order added.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_TypeDiffs_OrderAdded()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.Contains(result.TypeDiffs, d =>
- d.Kind == DiffKind.Added && d.Right!.Name == "Order");
+ Assert.Contains(d =>
+ d.Kind == DiffKind.Added && d.Right!.Name == "Order", result.TypeDiffs);
}
///
/// Verifies v1vs v2 ref diffs system text json removed.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_RefDiffs_SystemTextJsonRemoved()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.Contains(result.AssemblyRefDiffs, d =>
- d.Kind == DiffKind.Removed && d.Left!.Name == "System.Text.Json");
+ Assert.Contains(d =>
+ d.Kind == DiffKind.Removed && d.Left!.Name == "System.Text.Json", result.AssemblyRefDiffs);
}
///
/// Verifies v1vs v2 summary has positive counts.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_Summary_HasPositiveCounts()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.True(result.MetadataSummary.TypesAdded > 0);
- Assert.True(result.MetadataSummary.TypesRemoved > 0);
- Assert.True(result.MetadataSummary.MethodsAdded > 0);
+ Assert.IsGreaterThan(0, result.MetadataSummary.TypesAdded);
+ Assert.IsGreaterThan(0, result.MetadataSummary.TypesRemoved);
+ Assert.IsGreaterThan(0, result.MetadataSummary.MethodsAdded);
}
///
/// Verifies same assembly all unchanged.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SameAssembly_AllUnchanged()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryDll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.DoesNotContain(result.TypeDiffs, d => d.Kind == DiffKind.Added);
- Assert.DoesNotContain(result.TypeDiffs, d => d.Kind == DiffKind.Removed);
- Assert.DoesNotContain(result.MethodDiffs, d => d.Kind == DiffKind.Added);
- Assert.DoesNotContain(result.MethodDiffs, d => d.Kind == DiffKind.Removed);
+ Assert.DoesNotContain(d => d.Kind == DiffKind.Added, result.TypeDiffs);
+ Assert.DoesNotContain(d => d.Kind == DiffKind.Removed, result.TypeDiffs);
+ Assert.DoesNotContain(d => d.Kind == DiffKind.Added, result.MethodDiffs);
+ Assert.DoesNotContain(d => d.Kind == DiffKind.Removed, result.MethodDiffs);
}
///
/// Verifies v1vs v2 method diffs signature changes detected.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_SignatureChangesDetected()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
// There should be methods that changed signature
- Assert.True(result.MetadataSummary.MethodsChanged > 0 ||
+ Assert.IsTrue(result.MetadataSummary.MethodsChanged > 0 ||
result.MetadataSummary.MethodsAdded > 0);
}
///
/// Verifies v1vs v2 ref diffs newtonsoft still present.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_RefDiffs_NewtonsoftStillPresent()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
// Newtonsoft.Json was dropped in V2 — should be Removed
- Assert.Contains(result.AssemblyRefDiffs, d =>
- d.Kind == DiffKind.Removed && d.Left?.Name == "Newtonsoft.Json");
+ Assert.Contains(d =>
+ d.Kind == DiffKind.Removed && d.Left?.Name == "Newtonsoft.Json", result.AssemblyRefDiffs);
}
///
/// Verifies v1vs v2 size delta is non zero.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_SizeDelta_IsNonZero()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.NotEqual(0, result.MetadataSummary.SizeDelta);
+ Assert.AreNotEqual(0, result.MetadataSummary.SizeDelta);
}
///
/// Verifies v1vs v2 diff entries have correct kinds.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_DiffEntries_HaveCorrectKinds()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var kinds = result.TypeDiffs.Select(d => d.Kind).Distinct().ToHashSet();
// Should have at least Added, Removed, and Unchanged
@@ -149,14 +161,15 @@ public void V1vsV2_DiffEntries_HaveCorrectKinds()
///
/// Verifies v1vs v2 type diffs audit log added.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_TypeDiffs_AuditLogAdded()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.Contains(result.TypeDiffs, d =>
- d.Kind == DiffKind.Added && d.Right!.Name == "AuditLog");
+ Assert.Contains(d =>
+ d.Kind == DiffKind.Added && d.Right!.Name == "AuditLog", result.TypeDiffs);
}
///
@@ -164,19 +177,20 @@ public void V1vsV2_TypeDiffs_AuditLogAdded()
/// Product.PrintMembers has same signature in v1/v2 but different IL because
/// the Product record shape changed (StockCount→Quantity, added Sku).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_BodyChangesDetected()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var printMembers = result.MethodDiffs.FirstOrDefault(d =>
(d.Left ?? d.Right)!.Name == "PrintMembers"
&& (d.Left ?? d.Right)!.DeclaringType.Contains("Product"));
- Assert.NotNull(printMembers);
- Assert.Equal(DiffKind.Changed, printMembers.Kind);
+ Assert.IsNotNull(printMembers);
+ Assert.AreEqual(DiffKind.Changed, printMembers.Kind);
Assert.Contains("body", printMembers.ChangeDescription!);
}
@@ -184,38 +198,40 @@ public void V1vsV2_MethodDiffs_BodyChangesDetected()
/// Verifies source-identical methods survive token renumbering without false positives.
/// CountActive is source-identical in v1/v2 but Product changed shape, causing token churn.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_SourceIdenticalMethodStaysUnchanged()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var countActive = result.MethodDiffs.FirstOrDefault(d =>
(d.Left ?? d.Right)!.Name == "CountActive"
&& (d.Left ?? d.Right)!.DeclaringType.Contains("ProductCatalog"));
- Assert.NotNull(countActive);
- Assert.Equal(DiffKind.Unchanged, countActive.Kind);
+ Assert.IsNotNull(countActive);
+ Assert.AreEqual(DiffKind.Unchanged, countActive.Kind);
}
///
/// Verifies exception region changes are detected.
/// TryFindById catches Exception (v1) vs InvalidOperationException (v2).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_ExceptionRegionChangeDetected()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var tryFindById = result.MethodDiffs.FirstOrDefault(d =>
(d.Left ?? d.Right)!.Name == "TryFindById"
&& (d.Left ?? d.Right)!.DeclaringType.Contains("UserService"));
- Assert.NotNull(tryFindById);
- Assert.Equal(DiffKind.Changed, tryFindById.Kind);
+ Assert.IsNotNull(tryFindById);
+ Assert.AreEqual(DiffKind.Changed, tryFindById.Kind);
Assert.Contains("body", tryFindById.ChangeDescription!);
}
@@ -223,19 +239,20 @@ public void V1vsV2_MethodDiffs_ExceptionRegionChangeDetected()
/// Verifies local signature changes are detected.
/// SummarizeUsers has 1 local (v1) vs 2+ locals (v2).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_LocalSignatureChangeDetected()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var summarize = result.MethodDiffs.FirstOrDefault(d =>
(d.Left ?? d.Right)!.Name == "SummarizeUsers"
&& (d.Left ?? d.Right)!.DeclaringType.Contains("UserService"));
- Assert.NotNull(summarize);
- Assert.Equal(DiffKind.Changed, summarize.Kind);
+ Assert.IsNotNull(summarize);
+ Assert.AreEqual(DiffKind.Changed, summarize.Kind);
Assert.Contains("body", summarize.ChangeDescription!);
}
@@ -243,10 +260,11 @@ public void V1vsV2_MethodDiffs_LocalSignatureChangeDetected()
/// Isolated test: LocalSignaturesDiffer returns true for two methods with
/// different non-empty local signatures, exercising the element-by-element path.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void LocalSignaturesDiffer_DifferentLocals_ReturnsTrue()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var reader = analyzer.GetMetadataReader()!;
// Add has locals (int id, User user); SummarizeUsers has local (int count)
@@ -258,89 +276,94 @@ public void LocalSignaturesDiffer_DifferentLocals_ReturnsTrue()
var addBody = analyzer.GetMethodBody(addMethod)!;
var summarizeBody = analyzer.GetMethodBody(summarizeMethod)!;
- Assert.True(AssemblyDiffer.LocalSignaturesDiffer(reader, addBody, reader, summarizeBody));
+ Assert.IsTrue(AssemblyDiffer.LocalSignaturesDiffer(reader, addBody, reader, summarizeBody));
}
///
/// Isolated test: LocalSignaturesDiffer returns false for the same method body.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void LocalSignaturesDiffer_SameMethod_ReturnsFalse()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var reader = analyzer.GetMetadataReader()!;
var addMethod = analyzer.MethodDefs.First(m =>
m.Name == "Add" && m.DeclaringType.Contains("UserService"));
var addBody = analyzer.GetMethodBody(addMethod)!;
- Assert.False(AssemblyDiffer.LocalSignaturesDiffer(reader, addBody, reader, addBody));
+ Assert.IsFalse(AssemblyDiffer.LocalSignaturesDiffer(reader, addBody, reader, addBody));
}
///
/// Verifies comparing an assembly against itself produces no body changes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SameAssembly_NoBodyChanges()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryDll);
var result = AssemblyDiffer.Compare(left, right);
- Assert.DoesNotContain(result.MethodDiffs, d =>
- d.ChangeDescription?.Contains("body") == true);
+ Assert.DoesNotContain(d =>
+ d.ChangeDescription?.Contains("body") == true, result.MethodDiffs);
}
///
/// Verifies abstract methods (Rva == 0) are not incorrectly flagged as body-changed.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SameAssembly_AbstractMethods_NoBodyChange()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryDll);
var result = AssemblyDiffer.Compare(left, right);
var abstractMethods = result.MethodDiffs.Where(d =>
(d.Left ?? d.Right)!.Rva == 0);
- Assert.All(abstractMethods, m => Assert.Equal(DiffKind.Unchanged, m.Kind));
+ TestAssert.All(abstractMethods, m => Assert.AreEqual(DiffKind.Unchanged, m.Kind));
}
///
/// Verifies that body differences increase the MethodsChanged count in the summary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_Summary_MethodsChangedIncludesBodyChanges()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var bodyChangedCount = result.MethodDiffs.Count(d =>
d.ChangeDescription?.Contains("body") == true);
- Assert.True(bodyChangedCount > 0, "Should detect at least one body change");
- Assert.True(result.MetadataSummary.MethodsChanged >= bodyChangedCount);
+ Assert.IsGreaterThan(0, bodyChangedCount, "Should detect at least one body change");
+ Assert.IsGreaterThanOrEqualTo(bodyChangedCount, result.MetadataSummary.MethodsChanged);
}
///
/// Verifies that calli instructions with different calling conventions are detected.
/// InvokeCallback uses managed (v1) vs unmanaged[Cdecl] (v2) — different StandaloneSig tokens.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_CalliCallingConventionChangeDetected()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var invokeCallback = result.MethodDiffs.FirstOrDefault(d =>
(d.Left ?? d.Right)!.Name == "InvokeCallback"
&& (d.Left ?? d.Right)!.DeclaringType.Contains("FunctionPointerHelpers"));
- Assert.NotNull(invokeCallback);
- Assert.Equal(DiffKind.Changed, invokeCallback.Kind);
+ Assert.IsNotNull(invokeCallback);
+ Assert.AreEqual(DiffKind.Changed, invokeCallback.Kind);
Assert.Contains("body", invokeCallback.ChangeDescription!);
}
@@ -348,19 +371,20 @@ public void V1vsV2_MethodDiffs_CalliCallingConventionChangeDetected()
/// Verifies that local variables with different function-pointer types are detected.
/// HasCallback has a managed function-pointer local (v1) vs unmanaged[Cdecl] (v2).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void V1vsV2_MethodDiffs_FunctionPointerLocalChangeDetected()
{
- using var left = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var right = new AssemblyAnalyzer(samples.RichLibraryV2Dll);
+ using var left = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var right = new AssemblyAnalyzer(Samples.RichLibraryV2Dll);
var result = AssemblyDiffer.Compare(left, right);
var hasCallback = result.MethodDiffs.FirstOrDefault(d =>
(d.Left ?? d.Right)!.Name == "HasCallback"
&& (d.Left ?? d.Right)!.DeclaringType.Contains("FunctionPointerHelpers"));
- Assert.NotNull(hasCallback);
- Assert.Equal(DiffKind.Changed, hasCallback.Kind);
+ Assert.IsNotNull(hasCallback);
+ Assert.AreEqual(DiffKind.Changed, hasCallback.Kind);
Assert.Contains("body", hasCallback.ChangeDescription!);
}
}
diff --git a/tests/Dotsider.Tests/AssemblyLoaderTests.cs b/tests/Dotsider.Tests/AssemblyLoaderTests.cs
index 9cdccb32..3d0193fd 100644
--- a/tests/Dotsider.Tests/AssemblyLoaderTests.cs
+++ b/tests/Dotsider.Tests/AssemblyLoaderTests.cs
@@ -7,88 +7,96 @@ namespace Dotsider.Tests;
/// Tests for covering managed DLLs, apphosts,
/// single-file bundles, and NativeAOT executables.
///
-[Collection("SampleAssemblies")]
-public sealed class AssemblyLoaderTests(SampleAssemblyFixture samples)
+[TestClass]
+public sealed class AssemblyLoaderTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
/// Verifies that a managed DLL returns a Direct result with metadata.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_ManagedDll_ReturnsDirect()
{
- var result = AssemblyLoader.Open(samples.RichLibraryDll);
- Assert.IsType(result);
+ var result = AssemblyLoader.Open(Samples.RichLibraryDll);
+ Assert.IsExactInstanceOfType(result);
var direct = (AssemblyOpenResult.Direct)result;
- Assert.True(direct.Analyzer.HasMetadata);
+ Assert.IsTrue(direct.Analyzer.HasMetadata);
direct.Analyzer.Dispose();
}
/// Verifies that an apphost exe returns ApphostWithCompanion with a valid companion path.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_ApphostExe_ReturnsApphostWithCompanion()
{
- var result = AssemblyLoader.Open(samples.HelloWorldExe);
- Assert.IsType(result);
+ var result = AssemblyLoader.Open(Samples.HelloWorldExe);
+ Assert.IsExactInstanceOfType(result);
var apphost = (AssemblyOpenResult.ApphostWithCompanion)result;
- Assert.False(apphost.HostAnalyzer.HasMetadata);
+ Assert.IsFalse(apphost.HostAnalyzer.HasMetadata);
Assert.EndsWith(".dll", apphost.CompanionDllPath, StringComparison.OrdinalIgnoreCase);
- Assert.True(File.Exists(apphost.CompanionDllPath));
+ Assert.IsTrue(File.Exists(apphost.CompanionDllPath));
apphost.HostAnalyzer.Dispose();
}
/// Verifies that a single-file bundle returns a BundleEntry with valid metadata.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_SingleFileBundle_ReturnsBundleEntry()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
- var result = AssemblyLoader.Open(samples.SelfContainedConsoleExe!);
- Assert.IsType(result);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
+ var result = AssemblyLoader.Open(Samples.SelfContainedConsoleExe!);
+ Assert.IsExactInstanceOfType(result);
var bundle = (AssemblyOpenResult.BundleEntry)result;
- Assert.True(bundle.EntryAnalyzer.HasMetadata);
- Assert.Equal("SelfContainedConsole", bundle.EntryAnalyzer.AssemblyName);
- Assert.Equal(samples.SelfContainedConsoleExe, bundle.BundlePath);
- Assert.Equal(samples.SelfContainedConsoleExe, bundle.EntryAnalyzer.SourceBundlePath);
+ Assert.IsTrue(bundle.EntryAnalyzer.HasMetadata);
+ Assert.AreEqual("SelfContainedConsole", bundle.EntryAnalyzer.AssemblyName);
+ Assert.AreEqual(Samples.SelfContainedConsoleExe, bundle.BundlePath);
+ Assert.AreEqual(Samples.SelfContainedConsoleExe, bundle.EntryAnalyzer.SourceBundlePath);
// FilePath is the on-disk bundle path; DisplayName is the entry assembly name
- Assert.Equal(samples.SelfContainedConsoleExe, bundle.EntryAnalyzer.FilePath);
- Assert.Equal("SelfContainedConsole.dll", bundle.EntryAnalyzer.DisplayName);
+ Assert.AreEqual(Samples.SelfContainedConsoleExe, bundle.EntryAnalyzer.FilePath);
+ Assert.AreEqual("SelfContainedConsole.dll", bundle.EntryAnalyzer.DisplayName);
bundle.EntryAnalyzer.Dispose();
}
/// Verifies that bundle-backed analyzers expose correct capabilities.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_SingleFileBundle_HasCorrectCapabilities()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
- var result = AssemblyLoader.Open(samples.SelfContainedConsoleExe!);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
+ var result = AssemblyLoader.Open(Samples.SelfContainedConsoleExe!);
var bundle = (AssemblyOpenResult.BundleEntry)result;
- Assert.True(bundle.EntryAnalyzer.IsBundleBacked);
- Assert.False(bundle.EntryAnalyzer.CanSaveInPlace);
- Assert.Equal(samples.SelfContainedConsoleExe, bundle.EntryAnalyzer.LaunchPath);
+ Assert.IsTrue(bundle.EntryAnalyzer.IsBundleBacked);
+ Assert.IsFalse(bundle.EntryAnalyzer.CanSaveInPlace);
+ Assert.AreEqual(Samples.SelfContainedConsoleExe, bundle.EntryAnalyzer.LaunchPath);
bundle.EntryAnalyzer.Dispose();
}
/// Verifies that file-backed analyzers expose correct capabilities.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_ManagedDll_HasCorrectCapabilities()
{
- var result = AssemblyLoader.Open(samples.RichLibraryDll);
+ var result = AssemblyLoader.Open(Samples.RichLibraryDll);
var direct = (AssemblyOpenResult.Direct)result;
- Assert.False(direct.Analyzer.IsBundleBacked);
- Assert.True(direct.Analyzer.CanSaveInPlace);
- Assert.Equal(samples.RichLibraryDll, direct.Analyzer.LaunchPath);
- Assert.Equal(direct.Analyzer.FileName, direct.Analyzer.DisplayName);
+ Assert.IsFalse(direct.Analyzer.IsBundleBacked);
+ Assert.IsTrue(direct.Analyzer.CanSaveInPlace);
+ Assert.AreEqual(Samples.RichLibraryDll, direct.Analyzer.LaunchPath);
+ Assert.AreEqual(direct.Analyzer.FileName, direct.Analyzer.DisplayName);
direct.Analyzer.Dispose();
}
/// Verifies that a NativeAOT exe returns a NativeAot result without metadata.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_NativeAotExe_ReturnsNativeAot()
{
- Assert.NotNull(samples.NativeAotConsoleExe);
- var result = AssemblyLoader.Open(samples.NativeAotConsoleExe!);
- Assert.IsType(result);
+ Assert.IsNotNull(Samples.NativeAotConsoleExe);
+ var result = AssemblyLoader.Open(Samples.NativeAotConsoleExe!);
+ Assert.IsExactInstanceOfType(result);
var aot = (AssemblyOpenResult.NativeAot)result;
- Assert.False(aot.Analyzer.HasMetadata);
- Assert.NotNull(aot.Analyzer.NativeAotInfo);
- Assert.Equal(BinaryKind.NativeAot, aot.Analyzer.BinaryKind);
+ Assert.IsFalse(aot.Analyzer.HasMetadata);
+ Assert.IsNotNull(aot.Analyzer.NativeAotInfo);
+ Assert.AreEqual(BinaryKind.NativeAot, aot.Analyzer.BinaryKind);
aot.Analyzer.Dispose();
}
@@ -97,23 +105,25 @@ public void Open_NativeAotExe_ReturnsNativeAot()
/// the ReadyToRun assemblies inside it contain RTR signatures — the bundle check
/// runs before the Native AOT probe.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_SingleFileBundle_NotClassifiedAsNativeAot()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
- var result = AssemblyLoader.Open(samples.SelfContainedConsoleExe!);
- Assert.IsType(result);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
+ var result = AssemblyLoader.Open(Samples.SelfContainedConsoleExe!);
+ Assert.IsExactInstanceOfType(result);
((AssemblyOpenResult.BundleEntry)result).EntryAnalyzer.Dispose();
}
/// Verifies that a managed DLL is classified as Managed.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Open_ManagedDll_HasManagedBinaryKind()
{
- var result = AssemblyLoader.Open(samples.RichLibraryDll);
+ var result = AssemblyLoader.Open(Samples.RichLibraryDll);
var direct = (AssemblyOpenResult.Direct)result;
- Assert.Equal(BinaryKind.Managed, direct.Analyzer.BinaryKind);
- Assert.Null(direct.Analyzer.NativeAotInfo);
+ Assert.AreEqual(BinaryKind.Managed, direct.Analyzer.BinaryKind);
+ Assert.IsNull(direct.Analyzer.NativeAotInfo);
direct.Analyzer.Dispose();
}
}
diff --git a/tests/Dotsider.Tests/AssemblyResolutionTests.cs b/tests/Dotsider.Tests/AssemblyResolutionTests.cs
index 96741bad..05997086 100644
--- a/tests/Dotsider.Tests/AssemblyResolutionTests.cs
+++ b/tests/Dotsider.Tests/AssemblyResolutionTests.cs
@@ -11,9 +11,11 @@ namespace Dotsider.Tests;
/// Tests for assembly resolution logic including app-local, shared framework,
/// bundle-backed, and type-forwarder resolution paths.
///
-[Collection("SampleAssemblies")]
-public sealed class AssemblyResolutionTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public sealed class AssemblyResolutionTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
/// Clears resolution caches after each test.
public void Dispose()
{
@@ -22,28 +24,30 @@ public void Dispose()
}
/// Verifies that an app-local assembly resolves as FromFile before other probes.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveAssembly_AppLocal_StillPreferred()
{
// HelloWorld.dll sits next to HelloWorld.exe — resolving "HelloWorld" from
// the exe's directory should find the .dll app-locally
var resolved = AssemblyAnalyzer.ResolveAssembly(
- samples.HelloWorldExe, "HelloWorld");
- Assert.NotNull(resolved);
- var fromFile = Assert.IsType(resolved);
+ Samples.HelloWorldExe, "HelloWorld");
+ Assert.IsNotNull(resolved);
+ var fromFile = Assert.IsExactInstanceOfType(resolved);
Assert.EndsWith("HelloWorld.dll", fromFile.Path, StringComparison.OrdinalIgnoreCase);
}
/// Verifies that System.Runtime resolves from the shared framework.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveAssembly_FromSharedFramework_ReturnsFromFile()
{
// System.Runtime should be found in the shared framework
var resolved = AssemblyAnalyzer.ResolveAssembly(
- samples.RichLibraryDll, "System.Runtime",
+ Samples.RichLibraryDll, "System.Runtime",
".NETCoreApp,Version=v10.0", "Microsoft.NETCore.App");
- Assert.NotNull(resolved);
- Assert.IsType(resolved);
+ Assert.IsNotNull(resolved);
+ Assert.IsExactInstanceOfType(resolved);
}
///
@@ -52,38 +56,41 @@ public void ResolveAssembly_FromSharedFramework_ReturnsFromFile()
/// succeed first; in a real single-file host the bundle probe (step 3) would win.
/// Either path is correct — the key invariant is that resolution succeeds.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveAssembly_WithBundleContext_FindsSystemRuntime()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
var resolved = AssemblyAnalyzer.ResolveAssembly(
"SelfContainedConsole.dll", "System.Runtime",
- sourceBundlePath: samples.SelfContainedConsoleExe);
- Assert.NotNull(resolved);
+ sourceBundlePath: Samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(resolved);
}
/// Verifies that mscorlib type forwarders resolve correctly through a bundle.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ImplementationAssemblyResolver_WithBundle_ResolvesTypeForwarders()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
// mscorlib type forwarders should work through bundle-backed resolution
var resolved = ImplementationAssemblyResolver.Resolve(
"SelfContainedConsole.dll", "mscorlib", "System.Console",
".NETCoreApp,Version=v10.0", "Microsoft.NETCore.App",
- samples.SelfContainedConsoleExe);
- Assert.NotNull(resolved);
+ Samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(resolved);
}
/// Verifies that target framework and preferred pack are threaded through resolution.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResolveAssembly_PreferredRuntimePack_ThreadedThrough()
{
// Verify that target framework and preferred pack reach the locator
var resolved = AssemblyAnalyzer.ResolveAssembly(
- samples.RichLibraryDll, "System.Runtime",
+ Samples.RichLibraryDll, "System.Runtime",
".NETCoreApp,Version=v10.0", "Microsoft.NETCore.App");
- Assert.NotNull(resolved);
+ Assert.IsNotNull(resolved);
}
///
@@ -92,30 +99,32 @@ public void ResolveAssembly_PreferredRuntimePack_ThreadedThrough()
/// Resolving a forwarded type must follow the ExportedType to the implementation,
/// not stop at the facade just because it happens to carry usable metadata.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ImplementationAssemblyResolver_PartialFacade_ForwardedType_LandsInImplementationAssembly()
{
var resolved = ImplementationAssemblyResolver.Resolve(
- samples.HelloWorldDll, "System.Collections",
+ Samples.HelloWorldDll, "System.Collections",
"System.Collections.Generic.List`1",
".NETCoreApp,Version=v10.0", "Microsoft.NETCore.App");
- var fromFile = Assert.IsType(resolved);
- Assert.Equal("System.Private.CoreLib.dll", Path.GetFileName(fromFile.Path));
+ var fromFile = Assert.IsExactInstanceOfType(resolved);
+ Assert.AreEqual("System.Private.CoreLib.dll", Path.GetFileName(fromFile.Path));
}
///
/// Guardrail for the same partial facade: a type the facade actually owns as a
/// TypeDef must stay in the facade, not be over-chased into CoreLib.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ImplementationAssemblyResolver_PartialFacade_LocallyOwnedType_StaysInFacade()
{
var resolved = ImplementationAssemblyResolver.Resolve(
- samples.HelloWorldDll, "System.Collections",
+ Samples.HelloWorldDll, "System.Collections",
"System.Collections.Generic.LinkedList`1",
".NETCoreApp,Version=v10.0", "Microsoft.NETCore.App");
- var fromFile = Assert.IsType(resolved);
- Assert.Equal("System.Collections.dll", Path.GetFileName(fromFile.Path));
+ var fromFile = Assert.IsExactInstanceOfType(resolved);
+ Assert.AreEqual("System.Collections.dll", Path.GetFileName(fromFile.Path));
}
///
@@ -125,7 +134,8 @@ public void ImplementationAssemblyResolver_PartialFacade_LocallyOwnedType_StaysI
/// Handing a callers a non-owning assembly recreates the "method not found"
/// failure downstream — the whole point of the type-aware probe is to avoid it.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ImplementationAssemblyResolver_ForwarderChaseBroken_ReturnsNullNotFacade()
{
// Build a synthetic facade that forwards "Sample.Forwarded" to an assembly
@@ -149,7 +159,7 @@ public void ImplementationAssemblyResolver_ForwarderChaseBroken_ReturnsNullNotFa
var result = ImplementationAssemblyResolver.Resolve(
referencingPath, "SyntheticFacade", "Sample.Forwarded");
- Assert.Null(result);
+ Assert.IsNull(result);
}
finally
{
@@ -212,23 +222,24 @@ private static byte[] BuildSyntheticFacade(
/// Partial-facade forwarder resolution must also work through the bundle context
/// path. Drives TryResolveFromBundle explicitly by passing sourceBundlePath.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ImplementationAssemblyResolver_PartialFacade_Bundle_ForwardedType_LandsInImplementationAssembly()
{
- Assert.NotNull(samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(Samples.SelfContainedConsoleExe);
var resolved = ImplementationAssemblyResolver.Resolve(
"SelfContainedConsole.dll",
"System.Collections",
"System.Collections.Generic.List`1",
".NETCoreApp,Version=v10.0", "Microsoft.NETCore.App",
- samples.SelfContainedConsoleExe);
- Assert.NotNull(resolved);
+ Samples.SelfContainedConsoleExe);
+ Assert.IsNotNull(resolved);
var name = resolved switch
{
ResolvedAssembly.FromFile f => Path.GetFileName(f.Path),
ResolvedAssembly.FromBundle b => b.Name,
_ => null
};
- Assert.Equal("System.Private.CoreLib.dll", name);
+ Assert.AreEqual("System.Private.CoreLib.dll", name);
}
}
diff --git a/tests/Dotsider.Tests/CliTests.cs b/tests/Dotsider.Tests/CliTests.cs
index 23f32d53..f616cc47 100644
--- a/tests/Dotsider.Tests/CliTests.cs
+++ b/tests/Dotsider.Tests/CliTests.cs
@@ -4,21 +4,23 @@ namespace Dotsider.Tests;
/// CLI integration tests that invoke the dotsider process to verify
/// argument parsing, output formatting, and error handling.
///
-[Collection("SampleAssemblies")]
-public class CliTests(SampleAssemblyFixture fixture)
+[TestClass]
+public class CliTests
{
+ private static SampleAssemblyFixture Fixture => SampleAssemblyHost.Instance;
+
// --- Default analyze output ---
///
/// Verifies analyze default lists types methods and references.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Default_ListsTypesMethodsAndReferences()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.HelloWorldDll);
+ "analyze", Fixture.HelloWorldDll);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Types (", stdout);
Assert.Contains("Methods (", stdout);
Assert.Contains("References (", stdout);
@@ -30,13 +32,13 @@ public async Task Analyze_Default_ListsTypesMethodsAndReferences()
///
/// Verifies analyze default output includes portable PDB summary lines.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Default_ShowsPortablePdbSummary()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll);
+ "analyze", Fixture.RichLibraryDll);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("PDB:", stdout);
Assert.Contains("Sidecar(", stdout);
Assert.Contains("SourceLink: present", stdout);
@@ -45,29 +47,29 @@ public async Task Analyze_Default_ShowsPortablePdbSummary()
///
/// Verifies analyze default JSON includes portable PDB metadata.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Default_Json_IncludesPortablePdbMetadata()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--json");
+ "analyze", Fixture.RichLibraryDll, "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(stdout);
- Assert.Equal("sidecar", json.GetProperty("pdbProvenance").GetProperty("kind").GetString());
- Assert.True(json.GetProperty("sourceLink").GetProperty("isPresent").GetBoolean());
- Assert.True(json.GetProperty("debugDirectory").GetArrayLength() > 0);
+ Assert.AreEqual("sidecar", json.GetProperty("pdbProvenance").GetProperty("kind").GetString());
+ Assert.IsTrue(json.GetProperty("sourceLink").GetProperty("isPresent").GetBoolean());
+ Assert.IsGreaterThan(0, json.GetProperty("debugDirectory").GetArrayLength());
}
///
/// Verifies analyze IL output includes portable PDB annotations.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Il_ShowsPortablePdbAnnotations()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--il", "RichLibrary.Services.UserService.Add");
+ "analyze", Fixture.RichLibraryDll, "--il", "RichLibrary.Services.UserService.Add");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("// PDB: Sidecar", stdout);
Assert.Contains("// Source Link: present", stdout);
Assert.Contains("UserService.cs", stdout);
@@ -79,14 +81,14 @@ public async Task Analyze_Il_ShowsPortablePdbAnnotations()
///
/// Verifies analyze embedded source prints source text from an embedded portable PDB.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_EmbeddedSource_PrintsEmbeddedSource()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.EmbeddedSourceLibDll, "--embedded-source",
+ "analyze", Fixture.EmbeddedSourceLibDll, "--embedded-source",
"EmbeddedSourceLib.EmbeddedSourceFixture.Compute");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("internal static class EmbeddedSourceFixture", stdout);
Assert.Contains("return doubled + 1;", stdout);
}
@@ -96,7 +98,7 @@ public async Task Analyze_EmbeddedSource_PrintsEmbeddedSource()
///
/// Verifies analyze missing input does not truncate output file.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_MissingInput_DoesNotTruncateOutputFile()
{
var outputFile = Path.GetTempFileName();
@@ -107,9 +109,9 @@ public async Task Analyze_MissingInput_DoesNotTruncateOutputFile()
var (exitCode, _, stderr) = await RunDotsiderAsync(
"analyze", "nonexistent-assembly.dll", "-o", outputFile);
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
- Assert.Equal("original content", File.ReadAllText(outputFile));
+ Assert.AreEqual("original content", File.ReadAllText(outputFile));
}
finally
{
@@ -120,44 +122,44 @@ public async Task Analyze_MissingInput_DoesNotTruncateOutputFile()
///
/// Verifies analyze same input and output rejects with error.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_SameInputAndOutput_RejectsWithError()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.HelloWorldDll, "-o", fixture.HelloWorldDll);
+ "analyze", Fixture.HelloWorldDll, "-o", Fixture.HelloWorldDll);
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("Output path cannot be the same as the input file", stderr);
}
///
/// Verifies analyze invalid output path produces controlled error.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_InvalidOutputPath_ProducesControlledError()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.HelloWorldDll, "-o", "/nonexistent/dir/report.txt");
+ "analyze", Fixture.HelloWorldDll, "-o", "/nonexistent/dir/report.txt");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("Error:", stderr);
}
///
/// Verifies analyze valid output writes to file.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_ValidOutput_WritesToFile()
{
var outputFile = Path.GetTempFileName();
try
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.HelloWorldDll, "--types", "-o", outputFile);
+ "analyze", Fixture.HelloWorldDll, "--types", "-o", outputFile);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
// stdout should be empty when writing to file
- Assert.Empty(stdout.Trim());
+ Assert.IsEmpty(stdout.Trim());
// File should have the table
var content = File.ReadAllText(outputFile);
Assert.Contains("Namespace", content);
@@ -174,7 +176,7 @@ public async Task Analyze_ValidOutput_WritesToFile()
///
/// Verifies tui mode options before file routes to tui mode.
///
- [Fact]
+ [TestMethod]
public async Task TuiMode_OptionsBeforeFile_RoutesToTuiMode()
{
// "--tab 2 " should enter TUI mode, not fall through to subcommand parser.
@@ -183,7 +185,7 @@ public async Task TuiMode_OptionsBeforeFile_RoutesToTuiMode()
var (exitCode, _, stderr) = await RunDotsiderAsync(
"--tab", "2", "nonexistent-assembly.dll");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
// Should NOT contain System.CommandLine error text
Assert.DoesNotContain("Required command was not provided", stderr);
@@ -192,13 +194,13 @@ public async Task TuiMode_OptionsBeforeFile_RoutesToTuiMode()
///
/// Verifies tui mode options after file still work.
///
- [Fact]
+ [TestMethod]
public async Task TuiMode_OptionsAfterFile_StillWork()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
"nonexistent-assembly.dll", "--tab", "2");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
Assert.DoesNotContain("Required command was not provided", stderr);
}
@@ -208,13 +210,13 @@ public async Task TuiMode_OptionsAfterFile_StillWork()
///
/// Verifies tui mode escape timeout option routes to tui mode.
///
- [Fact]
+ [TestMethod]
public async Task TuiMode_EscapeTimeoutOption_RoutesToTuiMode()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
"--escape-timeout", "200", "nonexistent-assembly.dll");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
Assert.DoesNotContain("Required command was not provided", stderr);
}
@@ -222,13 +224,13 @@ public async Task TuiMode_EscapeTimeoutOption_RoutesToTuiMode()
///
/// Verifies tui mode short escape timeout alias routes to tui mode.
///
- [Fact]
+ [TestMethod]
public async Task TuiMode_ShortEscapeTimeoutAlias_RoutesToTuiMode()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
"-e", "200", "nonexistent-assembly.dll");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
Assert.DoesNotContain("Required command was not provided", stderr);
}
@@ -236,13 +238,13 @@ public async Task TuiMode_ShortEscapeTimeoutAlias_RoutesToTuiMode()
///
/// Verifies diff mode escape timeout option accepted.
///
- [Fact]
+ [TestMethod]
public async Task DiffMode_EscapeTimeoutOption_Accepted()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
"diff", "--escape-timeout", "200", "nonexistent-left.dll", "nonexistent-right.dll");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
Assert.DoesNotContain("Unrecognized", stderr);
}
@@ -250,13 +252,13 @@ public async Task DiffMode_EscapeTimeoutOption_Accepted()
///
/// Verifies diff mode short escape timeout alias accepted.
///
- [Fact]
+ [TestMethod]
public async Task DiffMode_ShortEscapeTimeoutAlias_Accepted()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
"diff", "-e", "200", "nonexistent-left.dll", "nonexistent-right.dll");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("File not found", stderr);
Assert.DoesNotContain("Unrecognized", stderr);
}
@@ -266,12 +268,12 @@ public async Task DiffMode_ShortEscapeTimeoutAlias_Accepted()
///
/// Verifies no args shows help and returns zero.
///
- [Fact]
+ [TestMethod]
public async Task NoArgs_ShowsHelpAndReturnsZero()
{
var (exitCode, stdout, _) = await RunDotsiderAsync();
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("dotsider", stdout);
Assert.Contains("Commands:", stdout);
}
@@ -279,12 +281,12 @@ public async Task NoArgs_ShowsHelpAndReturnsZero()
///
/// Verifies json flag alone returns non zero.
///
- [Fact]
+ [TestMethod]
public async Task JsonFlagAlone_ReturnsNonZero()
{
var (exitCode, _, stderr) = await RunDotsiderAsync("--json");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("Required command was not provided", stderr);
}
@@ -293,13 +295,13 @@ public async Task JsonFlagAlone_ReturnsNonZero()
///
/// Verifies analyze apphost auto redirects to managed dll.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Apphost_AutoRedirectsToManagedDll()
{
var (exitCode, stdout, stderr) = await RunDotsiderAsync(
- "analyze", fixture.HelloWorldExe);
+ "analyze", Fixture.HelloWorldExe);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("apphost", stderr, StringComparison.OrdinalIgnoreCase);
Assert.Contains("HelloWorld", stdout);
}
@@ -307,107 +309,107 @@ public async Task Analyze_Apphost_AutoRedirectsToManagedDll()
// --- Fields ---
/// Verifies that --fields lists field definitions in text mode.
- [Fact]
+ [TestMethod]
public async Task Analyze_Fields_ListsFieldDefinitions()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--fields");
+ "analyze", Fixture.RichLibraryDll, "--fields");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Namespace", stdout);
Assert.Contains("Name", stdout);
Assert.Contains("Signature", stdout);
}
/// Verifies that --fields with --json outputs a JSON array.
- [Fact]
+ [TestMethod]
public async Task Analyze_Fields_Json_OutputsJson()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--fields", "--json");
+ "analyze", Fixture.RichLibraryDll, "--fields", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(stdout);
- Assert.Equal(System.Text.Json.JsonValueKind.Array, json.ValueKind);
- Assert.True(json.GetArrayLength() > 0);
+ Assert.AreEqual(System.Text.Json.JsonValueKind.Array, json.ValueKind);
+ Assert.IsGreaterThan(0, json.GetArrayLength());
}
// --- Bundle ---
/// Verifies that --bundle shows the manifest for a single-file bundle.
- [Fact]
+ [TestMethod]
public async Task Analyze_Bundle_ShowsManifest()
{
- Assert.NotNull(fixture.SelfContainedConsoleExe);
+ Assert.IsNotNull(Fixture.SelfContainedConsoleExe);
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.SelfContainedConsoleExe!, "--bundle");
+ "analyze", Fixture.SelfContainedConsoleExe!, "--bundle");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Bundle version:", stdout);
Assert.Contains("Entries:", stdout);
}
/// Verifies that --bundle with --json outputs structured manifest data.
- [Fact]
+ [TestMethod]
public async Task Analyze_Bundle_Json_OutputsJson()
{
- Assert.NotNull(fixture.SelfContainedConsoleExe);
+ Assert.IsNotNull(Fixture.SelfContainedConsoleExe);
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.SelfContainedConsoleExe!, "--bundle", "--json");
+ "analyze", Fixture.SelfContainedConsoleExe!, "--bundle", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(stdout);
- Assert.True(json.GetProperty("fileCount").GetInt32() > 0);
+ Assert.IsGreaterThan(0, json.GetProperty("fileCount").GetInt32());
}
/// Verifies that --bundle on a non-bundle file returns an error.
- [Fact]
+ [TestMethod]
public async Task Analyze_Bundle_NonBundle_ReturnsError()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--bundle");
+ "analyze", Fixture.RichLibraryDll, "--bundle");
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("not a single-file bundle", stderr);
}
/// Verifies that --bundle -o rejects writing to the same input file.
- [Fact]
+ [TestMethod]
public async Task Analyze_Bundle_SameInputAndOutput_RejectsWithError()
{
- Assert.NotNull(fixture.SelfContainedConsoleExe);
+ Assert.IsNotNull(Fixture.SelfContainedConsoleExe);
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.SelfContainedConsoleExe!, "--bundle", "-o", fixture.SelfContainedConsoleExe!);
+ "analyze", Fixture.SelfContainedConsoleExe!, "--bundle", "-o", Fixture.SelfContainedConsoleExe!);
- Assert.NotEqual(0, exitCode);
+ Assert.AreNotEqual(0, exitCode);
Assert.Contains("Output path cannot be the same as the input file", stderr);
}
/// Verifies that default output for a bundle-backed assembly shows DisplayName.
- [Fact]
+ [TestMethod]
public async Task Analyze_DefaultOutput_BundleBacked_ShowsDisplayName()
{
- Assert.NotNull(fixture.SelfContainedConsoleExe);
+ Assert.IsNotNull(Fixture.SelfContainedConsoleExe);
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.SelfContainedConsoleExe!);
+ "analyze", Fixture.SelfContainedConsoleExe!);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("from bundle", stdout);
}
/// Verifies that default JSON output for a bundle-backed assembly includes bundle properties.
- [Fact]
+ [TestMethod]
public async Task Analyze_DefaultOutput_BundleBacked_Json_IncludesProperties()
{
- Assert.NotNull(fixture.SelfContainedConsoleExe);
+ Assert.IsNotNull(Fixture.SelfContainedConsoleExe);
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.SelfContainedConsoleExe!, "--json");
+ "analyze", Fixture.SelfContainedConsoleExe!, "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(stdout);
- Assert.True(json.GetProperty("isBundleBacked").GetBoolean());
- Assert.Equal("SelfContainedConsole.dll", json.GetProperty("displayName").GetString());
- Assert.NotNull(json.GetProperty("preferredRuntimePack").GetString());
+ Assert.IsTrue(json.GetProperty("isBundleBacked").GetBoolean());
+ Assert.AreEqual("SelfContainedConsole.dll", json.GetProperty("displayName").GetString());
+ Assert.IsNotNull(json.GetProperty("preferredRuntimePack").GetString());
}
///
@@ -415,23 +417,23 @@ public async Task Analyze_DefaultOutput_BundleBacked_Json_IncludesProperties()
/// greater than zero and edges whose source is not always the root, and that no internal
/// navigation fields leak into the payload.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Deps_Json_EmitsTransitiveGraphWithoutNavigationLeak()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--deps", "--json");
+ "analyze", Fixture.RichLibraryDll, "--deps", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var root = System.Text.Json.JsonSerializer.Deserialize(stdout);
- Assert.True(root.TryGetProperty("graph", out var graph));
- Assert.True(graph.TryGetProperty("nodes", out var nodes));
- Assert.True(graph.TryGetProperty("edges", out var edges));
+ Assert.IsTrue(root.TryGetProperty("graph", out var graph));
+ Assert.IsTrue(graph.TryGetProperty("nodes", out var nodes));
+ Assert.IsTrue(graph.TryGetProperty("edges", out var edges));
string? rootId = null;
var anyDepthOverZero = false;
foreach (var n in nodes.EnumerateArray())
{
- Assert.True(n.TryGetProperty("id", out var id));
+ Assert.IsTrue(n.TryGetProperty("id", out var id));
foreach (var leak in new[]
{
"resolvedPath", "referencingFilePath", "referencingBundlePath",
@@ -439,7 +441,7 @@ public async Task Analyze_Deps_Json_EmitsTransitiveGraphWithoutNavigationLeak()
"candidateProbePath", "isFrameworkAssembly", "resolved",
})
{
- Assert.False(n.TryGetProperty(leak, out _), $"node must not expose {leak}");
+ Assert.IsFalse(n.TryGetProperty(leak, out _), $"node must not expose {leak}");
}
if (n.TryGetProperty("isRoot", out var isRoot) && isRoot.GetBoolean())
@@ -448,31 +450,31 @@ public async Task Analyze_Deps_Json_EmitsTransitiveGraphWithoutNavigationLeak()
anyDepthOverZero = true;
}
- Assert.True(anyDepthOverZero);
- Assert.NotNull(rootId);
+ Assert.IsTrue(anyDepthOverZero);
+ Assert.IsNotNull(rootId);
var anyNonRootSource = false;
foreach (var e in edges.EnumerateArray())
{
- Assert.True(e.TryGetProperty("sourceId", out var src));
+ Assert.IsTrue(e.TryGetProperty("sourceId", out var src));
if (src.GetString() != rootId) anyNonRootSource = true;
}
- Assert.True(anyNonRootSource);
+ Assert.IsTrue(anyNonRootSource);
}
///
/// Verifies analyze --deps --json on a Native AOT binary emits the compiled-in
/// assemblies and the native import modules, with only non-default node kinds serialized.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Deps_NativeAot_Json_EmitsAssembliesAndImports()
{
- Assert.SkipWhen(fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--deps", "--json");
+ "analyze", Fixture.NativeAotConsoleExe!, "--deps", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("System.Private.CoreLib", stdout);
Assert.Contains("\"nativeImport\"", stdout);
}
@@ -481,13 +483,13 @@ public async Task Analyze_Deps_NativeAot_Json_EmitsAssembliesAndImports()
/// Verifies managed analyze --deps --json output is byte-compatible with the
/// pre-AOT shape: the default assembly kind is never serialized.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Deps_Managed_Json_OmitsDefaultKind()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--deps", "--json");
+ "analyze", Fixture.RichLibraryDll, "--deps", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.DoesNotContain("\"kind\"", stdout);
}
@@ -495,15 +497,15 @@ public async Task Analyze_Deps_Managed_Json_OmitsDefaultKind()
/// Verifies analyze --size on a Native AOT binary with an mstat sidecar prints the
/// per-assembly breakdown and the data categories instead of an empty tree.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Size_NativeAot_PrintsAssemblyBreakdown()
{
- Assert.SkipWhen(fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--size");
+ "analyze", Fixture.NativeAotConsoleExe!, "--size");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("System.Private.CoreLib", stdout);
Assert.Contains("Blobs", stdout);
}
@@ -512,15 +514,15 @@ public async Task Analyze_Size_NativeAot_PrintsAssemblyBreakdown()
/// Verifies analyze --size --json on a Native AOT binary carries the new node
/// kinds and the dependency-graph node names that make the tree joinable.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Size_NativeAot_Json_HasAotKindsAndNodeNames()
{
- Assert.SkipWhen(fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--size", "--json");
+ "analyze", Fixture.NativeAotConsoleExe!, "--size", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("\"category\"", stdout);
Assert.Contains("aotNodeName", stdout);
}
@@ -529,13 +531,13 @@ public async Task Analyze_Size_NativeAot_Json_HasAotKindsAndNodeNames()
/// Verifies analyze --size --json on a managed assembly is unchanged by the AOT
/// additions: no aotNodeName property appears.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Size_Managed_Json_HasNoAotProperties()
{
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--size", "--json");
+ "analyze", Fixture.RichLibraryDll, "--size", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.DoesNotContain("aotNodeName", stdout);
}
@@ -543,15 +545,16 @@ public async Task Analyze_Size_Managed_Json_HasNoAotProperties()
/// Verifies analyze --symbols on a Native AOT binary prints the provenance header
/// and the symbol table.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Symbols_NativeAot_PrintsTable()
{
- Assert.SkipWhen(fixture.NativeAotConsoleSymbols is null, "native symbols were not produced");
+ TestSkip.When(Fixture.NativeAotConsoleSymbols is null, "native symbols were not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--symbols");
+ "analyze", Fixture.NativeAotConsoleExe!, "--symbols");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Source:", stdout);
Assert.Contains("Symbols (", stdout);
Assert.Contains("0x", stdout);
@@ -560,15 +563,16 @@ public async Task Analyze_Symbols_NativeAot_PrintsTable()
///
/// Verifies analyze --symbols --json carries the provenance and the symbol list.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Symbols_NativeAot_Json_CarriesProvenance()
{
- Assert.SkipWhen(fixture.NativeAotConsoleSymbols is null, "native symbols were not produced");
+ TestSkip.When(Fixture.NativeAotConsoleSymbols is null, "native symbols were not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--symbols", "--json");
+ "analyze", Fixture.NativeAotConsoleExe!, "--symbols", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("\"source\"", stdout);
Assert.Contains("\"status\"", stdout);
Assert.Contains("\"symbols\"", stdout);
@@ -579,20 +583,22 @@ public async Task Analyze_Symbols_NativeAot_Json_CarriesProvenance()
/// Verifies analyze --symbols on a managed assembly exits 1 — there are no native
/// symbols to read.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Symbols_Managed_ExitsOne()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--symbols");
+ "analyze", Fixture.RichLibraryDll, "--symbols");
- Assert.Equal(1, exitCode);
+ Assert.AreEqual(1, exitCode);
Assert.Contains("managed", stderr);
}
///
/// Verifies default and JSON analyze output report a raw SDK WebAssembly module as Wasm.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Wasm_PrintsModuleSummary()
{
var wasmPath = GetWasmNativePath();
@@ -600,7 +606,7 @@ public async Task Analyze_Wasm_PrintsModuleSummary()
var (exitCode, stdout, _) = await RunDotsiderAsync(
"analyze", wasmPath);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Kind: WebAssembly (.NET)", stdout);
Assert.Contains("Functions:", stdout);
Assert.Contains("Symbols:", stdout);
@@ -608,17 +614,18 @@ public async Task Analyze_Wasm_PrintsModuleSummary()
var (jsonExitCode, jsonStdout, _) = await RunDotsiderAsync(
"analyze", wasmPath, "--json");
- Assert.Equal(0, jsonExitCode);
+ Assert.AreEqual(0, jsonExitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(jsonStdout);
- Assert.Equal("wasm", json.GetProperty("binaryKind").GetString());
- Assert.Equal("Wasm32", json.GetProperty("architecture").GetString());
- Assert.True(json.GetProperty("wasm").GetProperty("definedFunctionCount").GetInt32() > 0);
+ Assert.AreEqual("wasm", json.GetProperty("binaryKind").GetString());
+ Assert.AreEqual("Wasm32", json.GetProperty("architecture").GetString());
+ Assert.IsGreaterThan(0, json.GetProperty("wasm").GetProperty("definedFunctionCount").GetInt32());
}
///
/// Verifies analyze --symbols on a raw Wasm module prints WebAssembly provenance.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Symbols_Wasm_PrintsTable()
{
var wasmPath = GetWasmNativePath();
@@ -626,7 +633,7 @@ public async Task Analyze_Symbols_Wasm_PrintsTable()
var (exitCode, stdout, _) = await RunDotsiderAsync(
"analyze", wasmPath, "--symbols");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("WebAssembly", stdout);
Assert.Contains("Symbols (", stdout);
Assert.Contains("0x", stdout);
@@ -635,7 +642,8 @@ public async Task Analyze_Symbols_Wasm_PrintsTable()
///
/// Verifies analyze --size on a raw Wasm module reports the Wasm function tree.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Size_Wasm_PrintsFunctionBreakdown()
{
var wasmPath = GetWasmNativePath();
@@ -643,39 +651,41 @@ public async Task Analyze_Size_Wasm_PrintsFunctionBreakdown()
var (exitCode, stdout, _) = await RunDotsiderAsync(
"analyze", wasmPath, "--size");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("(Wasm)", stdout);
Assert.Contains("Functions", stdout);
}
/// Verifies analyze --disasm 0xVA on a Native AOT binary prints a named listing.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Disasm_NativeAot_ByAddress_PrintsListing()
{
- Assert.SkipWhen(fixture.NativeAotConsoleExe is null || !File.Exists(fixture.NativeAotConsoleExe),
+ TestSkip.When(Fixture.NativeAotConsoleExe is null || !File.Exists(Fixture.NativeAotConsoleExe),
"NativeAOT publish did not run on this leg.");
ulong va;
- using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(fixture.NativeAotConsoleExe!))
+ using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(Fixture.NativeAotConsoleExe!))
{
var fn = analyzer.NativeSymbols?.Symbols.FirstOrDefault(s =>
s.Kind == Dotsider.Core.Analysis.Models.NativeSymbolKind.Function
&& s.ManagedName is not null && s.FileOffset is not null && s.Size > 0);
- Assert.NotNull(fn);
+ Assert.IsNotNull(fn);
va = fn!.VirtualAddress;
}
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--disasm", $"0x{va:x}");
+ "analyze", Fixture.NativeAotConsoleExe!, "--disasm", $"0x{va:x}");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains($"0x{va:x}:", stdout);
}
///
/// Verifies analyze --disasm on a raw Wasm module decodes a real function body.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Disasm_Wasm_ByAddress_PrintsListing()
{
var wasmPath = GetWasmNativePath();
@@ -690,7 +700,7 @@ public async Task Analyze_Disasm_Wasm_ByAddress_PrintsListing()
var (exitCode, stdout, _) = await RunDotsiderAsync(
"analyze", wasmPath, "--disasm", $"0x{va:x}");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains($"0x{va:x}:", stdout);
Assert.Contains("call", stdout);
}
@@ -699,7 +709,8 @@ public async Task Analyze_Disasm_Wasm_ByAddress_PrintsListing()
/// Verifies analyze --disasm accepts WebAssembly function identifiers in the forms users
/// see in wasm tooling: func:N and a bare decimal function index.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Disasm_Wasm_ByFunctionIndex_PrintsListing()
{
var wasmPath = GetWasmNativePath();
@@ -719,8 +730,8 @@ public async Task Analyze_Disasm_Wasm_ByFunctionIndex_PrintsListing()
var (indexExitCode, indexStdout, _) = await RunDotsiderAsync(
"analyze", wasmPath, "--disasm", funcIndex);
- Assert.Equal(0, aliasExitCode);
- Assert.Equal(0, indexExitCode);
+ Assert.AreEqual(0, aliasExitCode);
+ Assert.AreEqual(0, indexExitCode);
Assert.Contains("func[", aliasStdout);
Assert.Contains("func[", indexStdout);
}
@@ -728,29 +739,31 @@ public async Task Analyze_Disasm_Wasm_ByFunctionIndex_PrintsListing()
///
/// Verifies Webcil-wrapped .wasm app assemblies use managed metadata CLI behavior.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_WebcilWasm_PrintsManagedMetadata()
{
- Assert.SkipWhen(fixture.WasmConsoleWebcilWasm is null,
+ TestSkip.When(Fixture.WasmConsoleWebcilWasm is null,
"browser-wasm publish did not produce the Webcil app assembly on this leg.");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.WasmConsoleWebcilWasm!);
+ "analyze", Fixture.WasmConsoleWebcilWasm!);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Kind: Managed", stdout);
Assert.Contains("Webcil:", stdout);
Assert.DoesNotContain("Kind: WebAssembly (.NET)", stdout);
}
/// Verifies analyze --disasm on a managed assembly exits 1 with a native-symbols error.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Disasm_Managed_ExitsOne()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--disasm", "Foo");
+ "analyze", Fixture.RichLibraryDll, "--disasm", "Foo");
- Assert.Equal(1, exitCode);
+ Assert.AreEqual(1, exitCode);
Assert.Contains("native symbols", stderr);
}
@@ -758,15 +771,16 @@ public async Task Analyze_Disasm_Managed_ExitsOne()
/// Verifies the default analyze --json info carries the native symbol provenance
/// fields for a Native AOT binary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Info_NativeAot_Json_CarriesSymbolProvenance()
{
- Assert.SkipWhen(fixture.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Fixture.NativeAotConsoleExe is null, "NativeAOT sample was not built");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--json");
+ "analyze", Fixture.NativeAotConsoleExe!, "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("nativeSymbolCount", stdout);
Assert.Contains("nativeSymbolSource", stdout);
Assert.Contains("nativeSymbolStatus", stdout);
@@ -777,16 +791,16 @@ public async Task Analyze_Info_NativeAot_Json_CarriesSymbolProvenance()
/// Verifies analyze --why prints the root-first dependency chain for a compiled
/// method when both sidecars are present.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Why_KnownType_PrintsChain()
{
- Assert.SkipWhen(fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- Assert.SkipWhen(fixture.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--why", "Program");
+ "analyze", Fixture.NativeAotConsoleExe!, "--why", "Program");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("in the binary? (root first)", stdout);
Assert.Contains("1.", stdout);
}
@@ -794,47 +808,47 @@ public async Task Analyze_Why_KnownType_PrintsChain()
///
/// Verifies analyze --why --json emits the chain as structured steps.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Why_Json_EmitsChainSteps()
{
- Assert.SkipWhen(fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- Assert.SkipWhen(fixture.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--why", "Program", "--json");
+ "analyze", Fixture.NativeAotConsoleExe!, "--why", "Program", "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(stdout);
- Assert.True(json.GetProperty("chain").GetArrayLength() > 0);
- Assert.False(string.IsNullOrEmpty(json.GetProperty("target").GetString()));
+ Assert.IsGreaterThan(0, json.GetProperty("chain").GetArrayLength());
+ Assert.IsFalse(string.IsNullOrEmpty(json.GetProperty("target").GetString()));
}
///
/// Verifies analyze --why with an unknown name errors with a clear message.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Why_UnknownName_Errors()
{
- Assert.SkipWhen(fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- Assert.SkipWhen(fixture.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--why", "NoSuchThingAnywhere12345");
+ "analyze", Fixture.NativeAotConsoleExe!, "--why", "NoSuchThingAnywhere12345");
- Assert.Equal(1, exitCode);
+ Assert.AreEqual(1, exitCode);
Assert.Contains("no compiled type or method matches", stderr);
}
///
/// Verifies analyze --why on a managed assembly explains the sidecar requirement.
///
- [Fact]
+ [TestMethod]
public async Task Analyze_Why_ManagedAssembly_Errors()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--why", "Program");
+ "analyze", Fixture.RichLibraryDll, "--why", "Program");
- Assert.Equal(1, exitCode);
+ Assert.AreEqual(1, exitCode);
Assert.Contains("requires a Native AOT binary", stderr);
}
@@ -843,15 +857,16 @@ public async Task Analyze_Why_ManagedAssembly_Errors()
///
/// Verifies plain analyze prints the pre-ILC probe summary without attaching.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Default_PreIlc_PrintsProbeSummaryWithoutAttaching()
{
- Assert.SkipWhen(fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!);
+ "analyze", Fixture.NativeAotConsoleExe!);
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Pre-ILC:", stdout);
Assert.Contains("Origin:", stdout);
// The counts summary attaches; a plain info dump must not print it.
@@ -861,33 +876,35 @@ public async Task Analyze_Default_PreIlc_PrintsProbeSummaryWithoutAttaching()
///
/// Verifies the default analyze --json carries the preIlc probe object.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Default_PreIlc_Json_CarriesProbeObject()
{
- Assert.SkipWhen(fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--json");
+ "analyze", Fixture.NativeAotConsoleExe!, "--json");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
var json = System.Text.Json.JsonSerializer.Deserialize(stdout);
var preIlc = json.GetProperty("preIlc");
- Assert.True(preIlc.GetProperty("hasAttachableCompanion").GetBoolean());
- Assert.False(string.IsNullOrEmpty(preIlc.GetProperty("managedAssemblyPath").GetString()));
+ Assert.IsTrue(preIlc.GetProperty("hasAttachableCompanion").GetBoolean());
+ Assert.IsFalse(string.IsNullOrEmpty(preIlc.GetProperty("managedAssemblyPath").GetString()));
}
///
/// Verifies bare --correlate attaches and prints the correlation counts.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Correlate_Bare_PrintsCounts()
{
- Assert.SkipWhen(fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--correlate");
+ "analyze", Fixture.NativeAotConsoleExe!, "--correlate");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Correlation:", stdout);
Assert.Contains("methods", stdout);
}
@@ -895,15 +912,16 @@ public async Task Analyze_Correlate_Bare_PrintsCounts()
///
/// Verifies --correlate Type.Method resolves a unique method and prints its IL.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Correlate_ByName_PrintsIl()
{
- Assert.SkipWhen(fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--correlate", "Greeter.Describe");
+ "analyze", Fixture.NativeAotConsoleExe!, "--correlate", "Greeter.Describe");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("Greeter::Describe", stdout);
Assert.Contains("--- IL (pre-ILC) ---", stdout);
}
@@ -911,43 +929,45 @@ public async Task Analyze_Correlate_ByName_PrintsIl()
///
/// Verifies --correlate 0xVA resolves by address and prints the native listing.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Correlate_ByAddress_PrintsNative()
{
- Assert.SkipWhen(fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
- Assert.SkipWhen(fixture.NativeAotConsoleSymbols is null, "native symbols were not produced");
+ TestSkip.When(Fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleSymbols is null, "native symbols were not produced");
ulong va;
- using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(fixture.NativeAotConsoleExe!))
+ using (var analyzer = new Dotsider.Core.Analysis.AssemblyAnalyzer(Fixture.NativeAotConsoleExe!))
{
analyzer.AttachPreIlcCompanions();
var correlation = analyzer.ManagedNativeIndex?.Methods.FirstOrDefault(m =>
m.Status == Dotsider.Core.Analysis.Models.MethodCorrelationStatus.CorrelatedExact
&& m.NativeSymbols.Count > 0
&& m.NativeSymbols[0].FileOffset is not null);
- Assert.NotNull(correlation);
+ Assert.IsNotNull(correlation);
va = correlation!.NativeSymbols[0].VirtualAddress;
}
var (exitCode, stdout, _) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--correlate", $"0x{va:x}");
+ "analyze", Fixture.NativeAotConsoleExe!, "--correlate", $"0x{va:x}");
- Assert.Equal(0, exitCode);
+ Assert.AreEqual(0, exitCode);
Assert.Contains("--- Native ---", stdout);
}
///
/// Verifies an ambiguous name (overloads) lists every candidate and exits non-zero.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Correlate_AmbiguousName_ListsCandidatesAndExitsNonZero()
{
- Assert.SkipWhen(fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Fixture.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.NativeAotConsoleExe!, "--correlate", "Greeter.Greet");
+ "analyze", Fixture.NativeAotConsoleExe!, "--correlate", "Greeter.Greet");
- Assert.Equal(2, exitCode);
+ Assert.AreEqual(2, exitCode);
Assert.Contains("ambiguous", stderr);
Assert.Contains("Greeter::Greet", stderr);
}
@@ -955,13 +975,14 @@ public async Task Analyze_Correlate_AmbiguousName_ListsCandidatesAndExitsNonZero
///
/// Verifies --correlate on a managed assembly explains the Native AOT requirement.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Analyze_Correlate_ManagedAssembly_ExitsOne()
{
var (exitCode, _, stderr) = await RunDotsiderAsync(
- "analyze", fixture.RichLibraryDll, "--correlate", "Foo");
+ "analyze", Fixture.RichLibraryDll, "--correlate", "Foo");
- Assert.Equal(1, exitCode);
+ Assert.AreEqual(1, exitCode);
Assert.Contains("requires a Native AOT binary", stderr);
}
@@ -974,7 +995,7 @@ private static Dotsider.Core.Analysis.Models.NativeSymbol FindWasmFunctionWithNa
Dotsider.Core.Analysis.AssemblyAnalyzer analyzer)
{
var info = analyzer.NativeSymbols;
- Assert.NotNull(info);
+ Assert.IsNotNull(info);
foreach (var symbol in info.Symbols.Take(512))
{
var result = Dotsider.Core.Analysis.Disasm.NativeDisassembler.DisassembleSymbol(analyzer, symbol);
@@ -988,11 +1009,11 @@ private static Dotsider.Core.Analysis.Models.NativeSymbol FindWasmFunctionWithNa
throw new InvalidOperationException("No Wasm function with a named direct call was found.");
}
- private string GetWasmNativePath()
+ private static string GetWasmNativePath()
{
- Assert.SkipWhen(fixture.WasmConsoleNativeWasm is null && fixture.ReadyToRunConsoleWasmNativeWasm is null,
+ TestSkip.When(Fixture.WasmConsoleNativeWasm is null && Fixture.ReadyToRunConsoleWasmNativeWasm is null,
"browser-wasm publish did not run on this leg.");
- return fixture.WasmConsoleNativeWasm ?? fixture.ReadyToRunConsoleWasmNativeWasm!;
+ return Fixture.WasmConsoleNativeWasm ?? Fixture.ReadyToRunConsoleWasmNativeWasm!;
}
}
diff --git a/tests/Dotsider.Tests/ConnectionLimitTests.cs b/tests/Dotsider.Tests/ConnectionLimitTests.cs
index 85db7c72..336c51d7 100644
--- a/tests/Dotsider.Tests/ConnectionLimitTests.cs
+++ b/tests/Dotsider.Tests/ConnectionLimitTests.cs
@@ -12,14 +12,18 @@ namespace Dotsider.Tests;
/// Tests that the connection limit (4 concurrent) works correctly.
/// Uses the full headless TUI stack with real assemblies.
///
-[Collection("SampleAssemblies")]
-public class ConnectionLimitTests(SampleAssemblyFixture samples) : IAsyncDisposable
+[TestClass]
+public class ConnectionLimitTests : IAsyncDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _app;
private DotsiderState? _state;
private DotsiderDiagnosticsListener? _listener;
+ private CancellationTokenSource? _appCts;
+ private Task? _appTask;
private async Task StartTuiWithDiagnosticsAsync(CancellationToken ct)
{
@@ -35,7 +39,7 @@ private async Task StartTuiWithDiagnosticsAsync(CancellationToken ct)
_app = new Hex1bApp(
ctx =>
{
- _state ??= new DotsiderState(_app!, samples.HelloWorldDll, pendingMutations);
+ _state ??= new DotsiderState(_app!, Samples.HelloWorldDll, pendingMutations);
var dotsiderApp = new DotsiderApp(_state);
return Task.FromResult(dotsiderApp.Build(ctx));
},
@@ -43,12 +47,13 @@ private async Task StartTuiWithDiagnosticsAsync(CancellationToken ct)
{
WorkloadAdapter = _workload,
EnableInputCoalescing = false
- });
+ });
_listener = new DotsiderDiagnosticsListener(() => _state);
- _listener.StartListening(overridePid: Random.Shared.Next(100_000, 999_999));
+ _listener.StartListening(overridePid: TestSocketIds.NextPid());
- _ = _app.RunAsync(ct);
+ _appCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ _appTask = _app.RunAsync(_appCts.Token);
await Task.Delay(100, ct);
await TestHelpers.WaitUntilAsync(
@@ -64,24 +69,32 @@ await TestHelpers.WaitUntilAsync(
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
+ _appCts?.Cancel();
if (_listener is not null)
{
_listener.TestDelayHook = null;
await _listener.DisposeAsync();
}
+ if (_appTask is not null)
+ {
+ try { await _appTask; }
+ catch (OperationCanceledException) { }
+ }
_state?.Dispose();
_app?.Dispose();
if (_terminal is not null) await _terminal.DisposeAsync();
+ _appCts?.Dispose();
}
///
/// Verifies four connections all succeed.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task FourConnections_AllSucceed()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var socketPath = await StartTuiWithDiagnosticsAsync(ct);
// Fire 4 concurrent requests — all should succeed
@@ -91,16 +104,17 @@ public async Task FourConnections_AllSucceed()
.ToList();
var responses = await Task.WhenAll(tasks);
- Assert.All(responses, r => Assert.True(r.Success));
+ TestAssert.All(responses, r => Assert.IsTrue(r.Success));
}
///
/// Verifies fifth connection is rejected.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task FifthConnection_IsRejected()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var socketPath = await StartTuiWithDiagnosticsAsync(ct);
// Use the test delay hook to hold 4 slots open
@@ -128,7 +142,7 @@ public async Task FifthConnection_IsRejected()
var response = await DotsiderClient.SendAsync(socketPath,
new DotsiderRequest { Method = "assembly-info" }, ct);
- Assert.False(response.Success);
+ Assert.IsFalse(response.Success);
Assert.Contains("too many", response.Error!, StringComparison.OrdinalIgnoreCase);
// Release the held connections
@@ -143,10 +157,11 @@ public async Task FifthConnection_IsRejected()
///
/// Verifies slot freed allows new connection.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task SlotFreed_AllowsNewConnection()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var socketPath = await StartTuiWithDiagnosticsAsync(ct);
// Hold 4 slots with a controllable hook
@@ -186,16 +201,17 @@ public async Task SlotFreed_AllowsNewConnection()
var response = await DotsiderClient.SendAsync(socketPath,
new DotsiderRequest { Method = "assembly-info" }, ct);
- Assert.True(response.Success);
+ Assert.IsTrue(response.Success);
}
///
/// Verifies stalled client times out.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task StalledClient_TimesOut()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var socketPath = await StartTuiWithDiagnosticsAsync(ct);
// Connect but never send a newline — the read timeout (5s) should free the slot
@@ -209,16 +225,17 @@ public async Task StalledClient_TimesOut()
var response = await DotsiderClient.SendAsync(socketPath,
new DotsiderRequest { Method = "assembly-info" }, ct);
- Assert.True(response.Success);
+ Assert.IsTrue(response.Success);
}
///
/// Verifies shutdown with active connections completes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ShutdownWithActiveConnections_Completes()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var socketPath = await StartTuiWithDiagnosticsAsync(ct);
// Hold a connection slot open
@@ -242,7 +259,7 @@ public async Task ShutdownWithActiveConnections_Completes()
var disposeTask = _listener.DisposeAsync().AsTask();
var completed = await Task.WhenAny(disposeTask, Task.Delay(10_000, ct));
- Assert.Same(disposeTask, completed);
+ Assert.AreSame(disposeTask, completed);
// Prevent double-dispose in DisposeAsync
_listener = null;
diff --git a/tests/Dotsider.Tests/DataInterpretationPanelTests.cs b/tests/Dotsider.Tests/DataInterpretationPanelTests.cs
index 0b6ac2cd..5ea0a482 100644
--- a/tests/Dotsider.Tests/DataInterpretationPanelTests.cs
+++ b/tests/Dotsider.Tests/DataInterpretationPanelTests.cs
@@ -7,9 +7,11 @@ namespace Dotsider.Tests;
///
/// Tests for Data Interpretation Panel.
///
-[Collection("SampleAssemblies")]
-public class DataInterpretationPanelTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DataInterpretationPanelTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -45,11 +47,12 @@ public class DataInterpretationPanelTests(SampleAssemblyFixture samples) : IDisp
///
/// Verifies hex dump tab shows interpretation labels.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task HexDumpTab_ShowsInterpretationLabels()
{
- var (terminal, app) = CreateDotsiderApp(samples.HelloWorldDll);
- var ct = TestContext.Current.CancellationToken;
+ var (terminal, app) = CreateDotsiderApp(Samples.HelloWorldDll);
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -69,11 +72,12 @@ public async Task HexDumpTab_ShowsInterpretationLabels()
///
/// Verifies hex dump tab shows endian label.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task HexDumpTab_ShowsEndianLabel()
{
- var (terminal, app) = CreateDotsiderApp(samples.HelloWorldDll);
- var ct = TestContext.Current.CancellationToken;
+ var (terminal, app) = CreateDotsiderApp(Samples.HelloWorldDll);
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -91,11 +95,12 @@ public async Task HexDumpTab_ShowsEndianLabel()
///
/// Verifies hex dump tab shows length matching file size.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task HexDumpTab_ShowsLengthMatchingFileSize()
{
- var (terminal, app) = CreateDotsiderApp(samples.HelloWorldDll);
- var ct = TestContext.Current.CancellationToken;
+ var (terminal, app) = CreateDotsiderApp(Samples.HelloWorldDll);
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -107,8 +112,8 @@ public async Task HexDumpTab_ShowsLengthMatchingFileSize()
.ApplyAsync(terminal, ct);
// Verify the length in the panel matches the actual file size
- Assert.NotNull(_state);
- Assert.Equal(_state.Analyzer.RawBytes.Length, _state.HexEditorState.Document.ByteCount);
+ Assert.IsNotNull(_state);
+ Assert.AreEqual(_state.Analyzer.RawBytes.Length, _state.HexEditorState.Document.ByteCount);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -116,11 +121,12 @@ public async Task HexDumpTab_ShowsLengthMatchingFileSize()
///
/// Verifies hex dump tab shows hex addresses.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task HexDumpTab_ShowsHexAddresses()
{
- var (terminal, app) = CreateDotsiderApp(samples.HelloWorldDll);
- var ct = TestContext.Current.CancellationToken;
+ var (terminal, app) = CreateDotsiderApp(Samples.HelloWorldDll);
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -138,11 +144,12 @@ public async Task HexDumpTab_ShowsHexAddresses()
///
/// Verifies hex dump tab endian toggle updates values.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task HexDumpTab_EndianToggleUpdatesValues()
{
- var (terminal, app) = CreateDotsiderApp(samples.HelloWorldDll);
- var ct = TestContext.Current.CancellationToken;
+ var (terminal, app) = CreateDotsiderApp(Samples.HelloWorldDll);
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -161,7 +168,7 @@ public async Task HexDumpTab_EndianToggleUpdatesValues()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(HexEndianness.Big, _state!.HexEndianness);
+ Assert.AreEqual(HexEndianness.Big, _state!.HexEndianness);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -169,11 +176,12 @@ public async Task HexDumpTab_EndianToggleUpdatesValues()
///
/// Verifies hex dump tab cursor move updates interpretation.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task HexDumpTab_CursorMoveUpdatesInterpretation()
{
- var (terminal, app) = CreateDotsiderApp(samples.HelloWorldDll);
- var ct = TestContext.Current.CancellationToken;
+ var (terminal, app) = CreateDotsiderApp(Samples.HelloWorldDll);
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -184,7 +192,7 @@ public async Task HexDumpTab_CursorMoveUpdatesInterpretation()
.ApplyAsync(terminal, ct);
var textBefore = _state!.DataInterpEditorText;
- Assert.NotNull(textBefore);
+ Assert.IsNotNull(textBefore);
// Move cursor right — byte value changes
await new Hex1bTerminalInputSequenceBuilder()
@@ -194,7 +202,7 @@ public async Task HexDumpTab_CursorMoveUpdatesInterpretation()
.ApplyAsync(terminal, ct);
var textAfter = _state!.DataInterpEditorText;
- Assert.NotEqual(textBefore, textAfter);
+ Assert.AreNotEqual(textBefore, textAfter);
await new Hex1bTerminalInputSequenceBuilder()
.Ctrl().Key(Hex1bKey.C)
@@ -209,10 +217,10 @@ public async Task HexDumpTab_CursorMoveUpdatesInterpretation()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/DependencyGraphBuilderTests.cs b/tests/Dotsider.Tests/DependencyGraphBuilderTests.cs
index 9b002d47..af3209ce 100644
--- a/tests/Dotsider.Tests/DependencyGraphBuilderTests.cs
+++ b/tests/Dotsider.Tests/DependencyGraphBuilderTests.cs
@@ -9,67 +9,75 @@ namespace Dotsider.Tests;
/// TypeRef counts by full identity, determinism, and behavior across bundle, apphost, and
/// native deployment shapes.
///
-[Collection("SampleAssemblies")]
-public class DependencyGraphBuilderTests(SampleAssemblyFixture samples)
+[TestClass]
+public class DependencyGraphBuilderTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
/// HelloWorld produces a root node marked IsRoot.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_HasRootNode()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.NotEmpty(graph.Nodes);
- Assert.Single(graph.Nodes, n => n.IsRoot);
+ Assert.IsNotEmpty(graph.Nodes);
+ Assert.ContainsSingle(n => n.IsRoot, graph.Nodes);
}
/// The root node's name matches the analyzed assembly name.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_RootNodeNameMatchesAssembly()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Equal("HelloWorld", graph.Nodes.First(n => n.IsRoot).Name);
+ Assert.AreEqual("HelloWorld", graph.Nodes.First(n => n.IsRoot).Name);
}
/// RichLibrary still sees its direct third-party references as nodes.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasDirectReferenceNode_NewtonSoft()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Contains(graph.Nodes, n => n.Name == "Newtonsoft.Json");
+ Assert.Contains(n => n.Name == "Newtonsoft.Json", graph.Nodes);
}
///
/// RichLibrary's graph has nodes at depth greater than one, proving the traversal walks
/// past the root's direct references into transitive references.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasNodesAtDepthGreaterThanOne()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Contains(graph.Nodes, n => n.Depth > 1);
+ Assert.Contains(n => n.Depth > 1, graph.Nodes);
}
///
/// RichLibrary's graph contains edges whose source is not the root — direct evidence that
/// the builder is emitting transitive child-to-child edges, not only root-to-child.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_HasEdgesWhereSourceIsNotRoot()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var rootId = graph.Nodes.First(n => n.IsRoot).Id;
- Assert.Contains(graph.Edges, e => e.SourceId != rootId);
+ Assert.Contains(e => e.SourceId != rootId, graph.Edges);
}
/// Every edge references a node id that exists in the node set.
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllEdgesReferenceExistingNodesById()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var ids = graph.Nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
foreach (var e in graph.Edges)
@@ -80,62 +88,67 @@ public void AllEdgesReferenceExistingNodesById()
}
/// Node ids are unique across the graph.
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NoDuplicateNodeIds()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var ids = graph.Nodes.Select(n => n.Id).ToList();
- Assert.Equal(ids.Count, ids.Distinct(StringComparer.Ordinal).Count());
+ Assert.HasCount(ids.Count, ids.Distinct(StringComparer.Ordinal));
}
/// Exactly one root node.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void OnlyOneRootNode()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Single(graph.Nodes, n => n.IsRoot);
+ Assert.ContainsSingle(n => n.IsRoot, graph.Nodes);
}
/// At least one transitive edge has a positive TypeRef count.
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TransitiveEdgesCarryTypeRefCounts()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Contains(graph.Edges, e => e.TypeRefCount > 0);
+ Assert.Contains(e => e.TypeRefCount > 0, graph.Edges);
}
///
/// Every non-root node carries navigation context keyed by its id, and the root entry
/// has .
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigationById_CoversEveryNode_AndRootIsRoot()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var graph = DependencyGraphBuilder.Build(a);
foreach (var n in graph.Nodes)
- Assert.True(graph.NavigationById.ContainsKey(n.Id), $"missing nav for {n.Id}");
+ Assert.IsTrue(graph.NavigationById.ContainsKey(n.Id), $"missing nav for {n.Id}");
var root = graph.Nodes.First(n => n.IsRoot);
- Assert.Equal(AssemblyProvenance.Root, graph.NavigationById[root.Id].Provenance);
+ Assert.AreEqual(AssemblyProvenance.Root, graph.NavigationById[root.Id].Provenance);
}
/// Building the same graph twice produces identical node id order and edge lists.
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Determinism_NodesAndEdgesOrderStableAcrossRuns()
{
- using var a1 = new AssemblyAnalyzer(samples.RichLibraryDll);
- using var a2 = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a1 = new AssemblyAnalyzer(Samples.RichLibraryDll);
+ using var a2 = new AssemblyAnalyzer(Samples.RichLibraryDll);
var g1 = DependencyGraphBuilder.Build(a1);
var g2 = DependencyGraphBuilder.Build(a2);
- Assert.Equal>(
+ Assert.AreSequenceEqual(
[.. g1.Nodes.Select(n => n.Id)],
[.. g2.Nodes.Select(n => n.Id)]);
- Assert.Equal>(
+ Assert.AreSequenceEqual(
[.. g1.Edges.Select(e => (e.SourceId, e.TargetId))],
[.. g2.Edges.Select(e => (e.SourceId, e.TargetId))]);
}
@@ -144,7 +157,8 @@ [.. g1.Edges.Select(e => (e.SourceId, e.TargetId))],
/// A reference to an assembly that cannot be located anywhere produces an unresolved leaf
/// with set and provenance .
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void UnresolvedRef_AddedAsLeafWithUnresolvedFlag()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -154,9 +168,9 @@ public void UnresolvedRef_AddedAsLeafWithUnresolvedFlag()
var graph = DependencyGraphBuilder.Build(a);
var leaf = graph.Nodes.FirstOrDefault(n => n.Name == "NonExistentTarget_ZZZ");
- Assert.NotNull(leaf);
- Assert.True(leaf!.Unresolved);
- Assert.Equal(AssemblyProvenance.Unresolved,
+ Assert.IsNotNull(leaf);
+ Assert.IsTrue(leaf!.Unresolved);
+ Assert.AreEqual(AssemblyProvenance.Unresolved,
graph.NavigationById[leaf.Id].Provenance);
}
@@ -166,7 +180,8 @@ public void UnresolvedRef_AddedAsLeafWithUnresolvedFlag()
/// and must not expand from the mismatched
/// file. The candidate probe path is recorded on the navigation context for diagnostics.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IdentityMismatch_DoesNotGraftSubtree()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -185,18 +200,19 @@ public void IdentityMismatch_DoesNotGraftSubtree()
var graph = DependencyGraphBuilder.Build(a);
var mis = graph.Nodes.FirstOrDefault(n => n.Name == "MisIdent");
- Assert.NotNull(mis);
- Assert.True(mis!.Unresolved);
- Assert.Equal(AssemblyProvenance.IdentityMismatch,
+ Assert.IsNotNull(mis);
+ Assert.IsTrue(mis!.Unresolved);
+ Assert.AreEqual(AssemblyProvenance.IdentityMismatch,
graph.NavigationById[mis.Id].Provenance);
- Assert.DoesNotContain(graph.Nodes, n => n.Name == "ChildOfMis");
- Assert.NotNull(graph.NavigationById[mis.Id].CandidateProbePath);
+ Assert.DoesNotContain(n => n.Name == "ChildOfMis", graph.Nodes);
+ Assert.IsNotNull(graph.NavigationById[mis.Id].CandidateProbePath);
}
///
/// Two references with the same simple name but different versions produce two distinct nodes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void DuplicateSimpleName_DifferentVersion_ProducesTwoNodes()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -212,14 +228,15 @@ public void DuplicateSimpleName_DifferentVersion_ProducesTwoNodes()
var graph = DependencyGraphBuilder.Build(a);
var targets = graph.Nodes.Where(n => n.Name == "TargetLib").ToList();
- Assert.Equal(2, targets.Count);
- Assert.NotEqual(targets[0].Id, targets[1].Id);
+ Assert.HasCount(2, targets);
+ Assert.AreNotEqual(targets[0].Id, targets[1].Id);
}
///
/// Two references with the same simple name but different public key tokens produce two distinct nodes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void DuplicateSimpleName_DifferentPublicKeyToken_ProducesTwoNodes()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -235,8 +252,8 @@ public void DuplicateSimpleName_DifferentPublicKeyToken_ProducesTwoNodes()
var graph = DependencyGraphBuilder.Build(a);
var targets = graph.Nodes.Where(n => n.Name == "TargetPktLib").ToList();
- Assert.Equal(2, targets.Count);
- Assert.NotEqual(targets[0].Id, targets[1].Id);
+ Assert.HasCount(2, targets);
+ Assert.AreNotEqual(targets[0].Id, targets[1].Id);
}
///
@@ -244,7 +261,8 @@ public void DuplicateSimpleName_DifferentPublicKeyToken_ProducesTwoNodes()
/// the full identity of that AssemblyRef, so an edge's TypeRefCount is meaningful even when
/// two refs share a simple name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TypeRefCount_GroupsByFullIdentity_NotSimpleName()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -272,8 +290,8 @@ public void TypeRefCount_GroupsByFullIdentity_NotSimpleName()
var edgeToV1 = graph.Edges.Single(e => e.SourceId == rootId && e.TargetId == v1Id);
var edgeToV2 = graph.Edges.Single(e => e.SourceId == rootId && e.TargetId == v2Id);
- Assert.Equal(2, edgeToV1.TypeRefCount);
- Assert.Equal(1, edgeToV2.TypeRefCount);
+ Assert.AreEqual(2, edgeToV1.TypeRefCount);
+ Assert.AreEqual(1, edgeToV2.TypeRefCount);
}
///
@@ -285,37 +303,39 @@ public void TypeRefCount_GroupsByFullIdentity_NotSimpleName()
/// AssemblyRef points at Microsoft.Diagnostics.NETCore.Client v0.2.10.10501 while NuGet
/// restored v0.2.13.11903 next to it.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AppLocalRollForward_FrameworkPkt_HigherVersion_ResolvesAndRecordsRequestedIdentity()
{
- using var a = new AssemblyAnalyzer(samples.AppLocalRollForwardDll);
+ using var a = new AssemblyAnalyzer(Samples.AppLocalRollForwardDll);
var graph = DependencyGraphBuilder.Build(a);
var nodes = graph.Nodes
.Where(n => n.Name == "Microsoft.Diagnostics.NETCore.Client")
.ToList();
- var node = Assert.Single(nodes);
- Assert.False(node.Unresolved);
+ var node = Assert.ContainsSingle(nodes);
+ Assert.IsFalse(node.Unresolved);
var nav = graph.NavigationById[node.Id];
- Assert.Equal(AssemblyProvenance.AppLocal, nav.Provenance);
- Assert.NotNull(nav.LoadedIdentity);
- Assert.Equal(node.Version, nav.LoadedIdentity!.Version);
+ Assert.AreEqual(AssemblyProvenance.AppLocal, nav.Provenance);
+ Assert.IsNotNull(nav.LoadedIdentity);
+ Assert.AreEqual(node.Version, nav.LoadedIdentity!.Version);
var traceEventNode = graph.Nodes.Single(
n => n.Name == "Microsoft.Diagnostics.Tracing.TraceEvent");
var edge = graph.Edges.Single(
e => e.SourceId == traceEventNode.Id && e.TargetId == node.Id);
- Assert.NotNull(edge.RequestedIdentity);
- Assert.NotEqual(node.Version, edge.RequestedIdentity!.Version);
- Assert.Equal("31bf3856ad364e35", edge.RequestedIdentity.PublicKeyToken);
+ Assert.IsNotNull(edge.RequestedIdentity);
+ Assert.AreNotEqual(node.Version, edge.RequestedIdentity!.Version);
+ Assert.AreEqual("31bf3856ad364e35", edge.RequestedIdentity.PublicKeyToken);
}
///
/// When a reference forms a cycle back to an ancestor, the cycle-closing edge is emitted but
/// the ancestor is not re-expanded.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Cycle_EdgeEmittedButNotRecursed()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -329,17 +349,18 @@ public void Cycle_EdgeEmittedButNotRecursed()
var cycA = graph.Nodes.Single(n => n.Name == "CycA");
var cycB = graph.Nodes.Single(n => n.Name == "CycB");
- Assert.Contains(graph.Edges, e => e.SourceId == cycA.Id && e.TargetId == cycB.Id);
- Assert.Contains(graph.Edges, e => e.SourceId == cycB.Id && e.TargetId == cycA.Id);
- Assert.Equal(1, graph.Nodes.Count(n => n.Name == "CycA"));
- Assert.Equal(1, graph.Nodes.Count(n => n.Name == "CycB"));
+ Assert.Contains(e => e.SourceId == cycA.Id && e.TargetId == cycB.Id, graph.Edges);
+ Assert.Contains(e => e.SourceId == cycB.Id && e.TargetId == cycA.Id, graph.Edges);
+ Assert.ContainsSingle(n => n.Name == "CycA", graph.Nodes);
+ Assert.ContainsSingle(n => n.Name == "CycB", graph.Nodes);
}
///
/// When two different parents reference the same child, the child appears once (merged on
/// identity) and both parent-to-child edges are emitted.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Diamond_MergedOnIdentity()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -356,9 +377,9 @@ public void Diamond_MergedOnIdentity()
using var a = new AssemblyAnalyzer(rootPath);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Equal(1, graph.Nodes.Count(n => n.Name == "DiaCommon"));
+ Assert.ContainsSingle(n => n.Name == "DiaCommon", graph.Nodes);
var commonId = graph.Nodes.Single(n => n.Name == "DiaCommon").Id;
- Assert.Equal(2, graph.Edges.Count(e => e.TargetId == commonId));
+ Assert.AreEqual(2, graph.Edges.Count(e => e.TargetId == commonId));
}
///
@@ -366,22 +387,23 @@ public void Diamond_MergedOnIdentity()
/// assemblies as nodes, DGML links aggregated to assembly-pair edges, and the native
/// import modules at depth 1.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeAot_BuildsAssemblyAndImportGraph()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- using var a = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ using var a = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
var graph = DependencyGraphBuilder.Build(a);
- var root = Assert.Single(graph.Nodes, n => n.IsRoot);
- Assert.Equal("NativeAotConsole", root.Name);
- Assert.Contains(graph.Nodes, n =>
- n.Kind == GraphNodeKind.Assembly && n.Name == "System.Private.CoreLib");
- Assert.Contains(graph.Nodes, n => n.Kind == GraphNodeKind.NativeImport);
+ var root = Assert.ContainsSingle(n => n.IsRoot, graph.Nodes);
+ Assert.AreEqual("NativeAotConsole", root.Name);
+ Assert.Contains(n =>
+ n.Kind == GraphNodeKind.Assembly && n.Name == "System.Private.CoreLib", graph.Nodes);
+ Assert.Contains(n => n.Kind == GraphNodeKind.NativeImport, graph.Nodes);
- if (samples.NativeAotConsoleDgml is not null)
- Assert.Contains(graph.Edges, e => e.SourceId == root.Id && e.TypeRefCount > 0);
+ if (Samples.NativeAotConsoleDgml is not null)
+ Assert.Contains(e => e.SourceId == root.Id && e.TypeRefCount > 0, graph.Edges);
}
///
@@ -389,17 +411,18 @@ public void NativeAot_BuildsAssemblyAndImportGraph()
/// and their navigation contexts classify framework assemblies so the hide-framework
/// filter works on the AOT graph.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeAot_AssemblyNodesCarryIdentityAndFrameworkClassification()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- using var a = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ using var a = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
var graph = DependencyGraphBuilder.Build(a);
var corelib = graph.Nodes.First(n => n.Name == "System.Private.CoreLib");
- Assert.NotNull(corelib.Version);
- Assert.True(graph.NavigationById[corelib.Id].IsFrameworkAssembly);
+ Assert.IsNotNull(corelib.Version);
+ Assert.IsTrue(graph.NavigationById[corelib.Id].IsFrameworkAssembly);
}
///
@@ -407,46 +430,48 @@ public void NativeAot_AssemblyNodesCarryIdentityAndFrameworkClassification()
/// compiled-into-native-image provenance, so Enter degrades to a message instead of
/// reporting a missing context.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeAot_AllNodesHaveDegradedNavigationContexts()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- using var a = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ using var a = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
var graph = DependencyGraphBuilder.Build(a);
- Assert.All(graph.Nodes, n =>
+ TestAssert.All(graph.Nodes, n =>
{
- Assert.True(graph.NavigationById.ContainsKey(n.Id), $"{n.Name}: navigation context missing");
- Assert.Null(graph.NavigationById[n.Id].Resolved);
+ Assert.IsTrue(graph.NavigationById.ContainsKey(n.Id), $"{n.Name}: navigation context missing");
+ Assert.IsNull(graph.NavigationById[n.Id].Resolved);
});
- Assert.All(graph.Nodes.Where(n => !n.IsRoot), n =>
- Assert.Equal(AssemblyProvenance.CompiledIntoNativeImage, graph.NavigationById[n.Id].Provenance));
+ TestAssert.All(graph.Nodes.Where(n => !n.IsRoot), n =>
+ Assert.AreEqual(AssemblyProvenance.CompiledIntoNativeImage, graph.NavigationById[n.Id].Provenance));
}
///
/// A Native AOT binary with no sidecars falls back to the import star: the root plus its
/// native import modules, every edge sourced from the root.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeAot_WithoutSidecars_ReturnsRootPlusImportStar()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null || !File.Exists(samples.NativeAotConsoleExe),
+ TestSkip.When(Samples.NativeAotConsoleExe is null || !File.Exists(Samples.NativeAotConsoleExe),
"Native AOT sample is not available on this platform.");
var dir = Directory.CreateTempSubdirectory("dotsider-depgraph-");
try
{
- var exeCopy = Path.Combine(dir.FullName, Path.GetFileName(samples.NativeAotConsoleExe!));
- File.Copy(samples.NativeAotConsoleExe!, exeCopy);
+ var exeCopy = Path.Combine(dir.FullName, Path.GetFileName(Samples.NativeAotConsoleExe!));
+ File.Copy(Samples.NativeAotConsoleExe!, exeCopy);
using var a = new AssemblyAnalyzer(exeCopy);
var graph = DependencyGraphBuilder.Build(a);
- var root = Assert.Single(graph.Nodes, n => n.IsRoot);
- Assert.All(graph.Nodes.Where(n => !n.IsRoot), n =>
- Assert.Equal(GraphNodeKind.NativeImport, n.Kind));
- Assert.All(graph.Edges, e => Assert.Equal(root.Id, e.SourceId));
+ var root = Assert.ContainsSingle(n => n.IsRoot, graph.Nodes);
+ TestAssert.All(graph.Nodes.Where(n => !n.IsRoot), n =>
+ Assert.AreEqual(GraphNodeKind.NativeImport, n.Kind));
+ TestAssert.All(graph.Edges, e => Assert.AreEqual(root.Id, e.SourceId));
}
finally
{
@@ -459,29 +484,31 @@ public void NativeAot_WithoutSidecars_ReturnsRootPlusImportStar()
/// produces a root-only graph and does not throw — the guarantee the AOT branch must not
/// disturb.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeNonAot_ReturnsRootOnlyGraph()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldExe);
- Assert.Equal(BinaryKind.Native, a.BinaryKind);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldExe);
+ Assert.AreEqual(BinaryKind.Native, a.BinaryKind);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Single(graph.Nodes);
- Assert.True(graph.Nodes[0].IsRoot);
- Assert.Empty(graph.Edges);
+ Assert.ContainsSingle(graph.Nodes);
+ Assert.IsTrue(graph.Nodes[0].IsRoot);
+ Assert.IsEmpty(graph.Edges);
}
///
/// A bundle-backed analyzer opened via still yields a transitive
/// graph without throwing when the builder walks through bundle-extracted refs.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void BundleBacked_Traversal_Works()
{
- Assert.SkipWhen(samples.SelfContainedConsoleExe is null || !File.Exists(samples.SelfContainedConsoleExe),
+ TestSkip.When(Samples.SelfContainedConsoleExe is null || !File.Exists(Samples.SelfContainedConsoleExe),
"Self-contained sample is not available on this platform.");
- var result = AssemblyLoader.Open(samples.SelfContainedConsoleExe!);
+ var result = AssemblyLoader.Open(Samples.SelfContainedConsoleExe!);
using var analyzer = result switch
{
AssemblyOpenResult.Direct d => d.Analyzer,
@@ -491,8 +518,8 @@ public void BundleBacked_Traversal_Works()
};
var graph = DependencyGraphBuilder.Build(analyzer);
- Assert.Contains(graph.Nodes, n => n.IsRoot);
- Assert.Contains(graph.Nodes, n => n.Depth > 0);
+ Assert.Contains(n => n.IsRoot, graph.Nodes);
+ Assert.Contains(n => n.Depth > 0, graph.Nodes);
}
///
@@ -500,15 +527,16 @@ public void BundleBacked_Traversal_Works()
/// they are resolved outside of the shared runtime directory (e.g., from app-local copies
/// in a self-contained publish).
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FrameworkAssemblies_AreClassifiedAcrossDeploymentModels()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var systemRuntime = graph.Nodes.FirstOrDefault(n => n.Name == "System.Runtime");
- Assert.NotNull(systemRuntime);
- Assert.True(graph.NavigationById[systemRuntime!.Id].IsFrameworkAssembly);
+ Assert.IsNotNull(systemRuntime);
+ Assert.IsTrue(graph.NavigationById[systemRuntime!.Id].IsFrameworkAssembly);
}
}
diff --git a/tests/Dotsider.Tests/DependencyGraphRenderLayoutTests.cs b/tests/Dotsider.Tests/DependencyGraphRenderLayoutTests.cs
index 5079ea3f..91460595 100644
--- a/tests/Dotsider.Tests/DependencyGraphRenderLayoutTests.cs
+++ b/tests/Dotsider.Tests/DependencyGraphRenderLayoutTests.cs
@@ -10,27 +10,28 @@ namespace Dotsider.Tests;
/// every scope, framework, or viewport change, prunes islands created by mid-chain filtering,
/// and resolves a single hover winner across overlapping or adjacent boxes.
///
-[Collection("SampleAssemblies")]
-public sealed class DependencyGraphRenderLayoutTests(SampleAssemblyFixture samples)
+[TestClass]
+public sealed class DependencyGraphRenderLayoutTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Toggling scope from All to DirectOnly rebalances the rendered layout —
/// depth-1 nodes move (and deep-band nodes disappear) because the layout is computed
/// from the visible subgraph, not from stale full-graph coordinates.
///
- [Fact]
+ [TestMethod]
public void FilterChange_RebalancesVisibleGraph()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var disambig = EmptyDisambig();
var all = LayoutFor(graph, DependencyGraphScope.All, hideFramework: false, w: 200, h: 80);
var direct = LayoutFor(graph, DependencyGraphScope.DirectOnly, hideFramework: false, w: 200, h: 80);
- Assert.True(direct.Nodes.Count < all.Nodes.Count,
- "DirectOnly should drop transitive nodes");
- Assert.All(direct.Nodes, rn => Assert.True(rn.VisibleDepth <= 1));
+ Assert.IsLessThan(all.Nodes.Count, direct.Nodes.Count, "DirectOnly should drop transitive nodes");
+ TestAssert.All(direct.Nodes, rn => Assert.IsLessThanOrEqualTo(1, rn.VisibleDepth));
// Rebalance: the direct layout is vertically shorter because it loses the deeper
// bands entirely. If the view reused full-graph geometry, the direct layout's max Y
@@ -38,23 +39,22 @@ public void FilterChange_RebalancesVisibleGraph()
// layout was recomputed from the visible subgraph, not sliced from stale positions.
var allMaxY = all.Nodes.Max(rn => rn.Y);
var directMaxY = direct.Nodes.Max(rn => rn.Y);
- Assert.True(directMaxY < allMaxY,
- $"DirectOnly should be shorter; got direct max Y = {directMaxY} vs all = {allMaxY}");
+ Assert.IsLessThan(allMaxY, directMaxY, $"DirectOnly should be shorter; got direct max Y = {directMaxY} vs all = {allMaxY}");
}
///
/// Under DirectOnly there are at most two visible-depth bands: root (depth 0) and
/// the direct refs (depth 1). No node lives deeper.
///
- [Fact]
+ [TestMethod]
public void DirectOnly_UsesOnlyVisibleDepthBands()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var direct = LayoutFor(graph, DependencyGraphScope.DirectOnly, hideFramework: false, w: 180, h: 60);
- Assert.All(direct.Nodes, rn => Assert.InRange(rn.VisibleDepth, 0, 1));
- Assert.Single(direct.Nodes, rn => rn.VisibleDepth == 0);
+ TestAssert.All(direct.Nodes, rn => Assert.IsInRange(0, 1, rn.VisibleDepth));
+ Assert.ContainsSingle(rn => rn.VisibleDepth == 0, direct.Nodes);
}
///
@@ -64,7 +64,7 @@ public void DirectOnly_UsesOnlyVisibleDepthBands()
/// no further refs exercises the pruning path; with hideFramework on, the leaf
/// vanishes and nothing else remains at depth > 0.
///
- [Fact]
+ [TestMethod]
public void FrameworkFilter_PrunesDisconnectedVisibleIslands()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -80,16 +80,16 @@ public void FrameworkFilter_PrunesDisconnectedVisibleIslands()
graph.Nodes, graph.Edges, graph.NavigationById,
DependencyGraphScope.All, hideFramework: true);
- Assert.Single(visible.Nodes, n => n.IsRoot);
- Assert.DoesNotContain(visible.Nodes, n => !n.IsRoot);
- Assert.Empty(visible.Edges);
+ Assert.ContainsSingle(n => n.IsRoot, visible.Nodes);
+ Assert.DoesNotContain(n => !n.IsRoot, visible.Nodes);
+ Assert.IsEmpty(visible.Edges);
}
///
/// Doubling the viewport width reflows the layout and produces a valid, non-overlapping
/// arrangement. No two rendered boxes share any character cell in the larger layout.
///
- [Fact]
+ [TestMethod]
public void ViewportResize_ReflowsWithoutBoxOverlap()
{
using var scope = SyntheticAssemblyScope.Create();
@@ -110,7 +110,7 @@ public void ViewportResize_ReflowsWithoutBoxOverlap()
AssertNoOverlap(wide);
AssertNoOverlap(narrow);
- Assert.NotEqual>(
+ Assert.AreNotSequenceEqual(
[.. narrow.Nodes.Select(rn => (rn.X, rn.Y))],
[.. wide.Nodes.Select(rn => (rn.X, rn.Y))]);
}
@@ -120,7 +120,7 @@ [.. narrow.Nodes.Select(rn => (rn.X, rn.Y))],
/// returns exactly one winner — the closest box center to the mouse, with ties broken by
/// later draw order.
///
- [Fact]
+ [TestMethod]
public void Hover_OverlappingBoxes_SelectsSingleWinner()
{
// first box: X=10..30, center 20. second box: X=15..35, center 25.
@@ -131,15 +131,15 @@ public void Hover_OverlappingBoxes_SelectsSingleWinner()
// Column 24 is inside both boxes. Distance to first center (20) = 4; to second
// center (25) = 1. Second wins on closer-to-center.
var winner = DependencyGraphView.ResolveHoverWinner(nodes, mouseX: 24, mouseY: 6);
- Assert.Equal(1, winner);
+ Assert.AreEqual(1, winner);
// Column 12 is inside only the first box.
winner = DependencyGraphView.ResolveHoverWinner(nodes, mouseX: 12, mouseY: 6);
- Assert.Equal(0, winner);
+ Assert.AreEqual(0, winner);
// Outside both.
winner = DependencyGraphView.ResolveHoverWinner(nodes, mouseX: 0, mouseY: 0);
- Assert.Equal(-1, winner);
+ Assert.AreEqual(-1, winner);
}
///
@@ -148,7 +148,7 @@ public void Hover_OverlappingBoxes_SelectsSingleWinner()
/// view writes to GraphSelectedNode. Validates the single-source-of-truth contract
/// for hover.
///
- [Fact]
+ [TestMethod]
public void Hover_WinnerMatchesStatusBarAndHighlight()
{
// left box: X=0..12, center 6. right box: X=6..18, center 12.
@@ -160,15 +160,15 @@ public void Hover_WinnerMatchesStatusBarAndHighlight()
// Right wins on closer-to-center — and that is also what the status bar will show.
var winner = DependencyGraphView.ResolveHoverWinner(nodes, mouseX: 11, mouseY: 1);
- Assert.Equal(1, winner);
- Assert.Equal("overlap-right", nodes[winner].Node.Name);
+ Assert.AreEqual(1, winner);
+ Assert.AreEqual("overlap-right", nodes[winner].Node.Name);
}
///
/// Non-winning overlapping boxes do not end up treated as hovered. The winner is unique,
/// and any other boxes that also contained the mouse point are left at their base colors.
///
- [Fact]
+ [TestMethod]
public void Hover_NonWinningOverlapsStayUnhighlighted()
{
var nodes = MakeRenderNodes(
@@ -179,12 +179,12 @@ public void Hover_NonWinningOverlapsStayUnhighlighted()
// is at column 10. Closer to winning => winning wins, losing stays in neither
// hovered nor selected sets.
var winner = DependencyGraphView.ResolveHoverWinner(nodes, mouseX: 14, mouseY: 1);
- Assert.Equal(1, winner);
+ Assert.AreEqual(1, winner);
// Sanity: the losing box still contains the mouse point, but is not the winner.
var losing = nodes[0];
- Assert.True(14 >= losing.X && 14 < losing.X + losing.Width);
- Assert.NotEqual(0, winner);
+ Assert.IsTrue(14 >= losing.X && 14 < losing.X + losing.Width);
+ Assert.AreNotEqual(0, winner);
}
///
@@ -193,13 +193,13 @@ public void Hover_NonWinningOverlapsStayUnhighlighted()
/// the viewport height would be stranded off-screen; with scroll, the view slides until
/// any node's box lies entirely inside the viewport band.
///
- [Fact]
+ [TestMethod]
public void ContentExceedingViewport_RemainsReachableViaScroll()
{
var graph = BuildManyDirectRefsGraph(count: 80);
var layout = LayoutFor(graph, DependencyGraphScope.All, hideFramework: false, w: 80, h: 20);
- Assert.True(layout.ContentHeight > 20, "expected content to exceed viewport");
+ Assert.IsGreaterThan(20, layout.ContentHeight, "expected content to exceed viewport");
// For every node, there exists a scroll offset in [0, contentHeight - viewport] that
// lands the node's full box inside the viewport.
@@ -209,8 +209,7 @@ public void ContentExceedingViewport_RemainsReachableViaScroll()
needed = Math.Min(needed, layout.ContentHeight - 20);
var visibleTop = rn.Y - needed;
var visibleBottom = visibleTop + rn.Height;
- Assert.True(visibleTop >= 0 && visibleBottom <= 20,
- $"{rn.Node.Name}: no scroll offset reveals the full box");
+ Assert.IsTrue(visibleTop >= 0 && visibleBottom <= 20, $"{rn.Node.Name}: no scroll offset reveals the full box");
}
}
@@ -220,10 +219,10 @@ public void ContentExceedingViewport_RemainsReachableViaScroll()
/// which looks broken. Two adjacent bands should stay within a handful of rows of each
/// other regardless of viewport height.
///
- [Fact]
+ [TestMethod]
public void AdjacentDepthBands_SeparatedByFixedAestheticGap()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var narrow = LayoutFor(graph, DependencyGraphScope.DirectOnly, hideFramework: true, w: 200, h: 20);
@@ -233,8 +232,8 @@ public void AdjacentDepthBands_SeparatedByFixedAestheticGap()
var tallGap = GapBetweenRootAndFirstDirect(tall);
// Gap is small and does not scale with viewport height.
- Assert.InRange(tallGap, 1, 8);
- Assert.Equal(narrowGap, tallGap);
+ Assert.IsInRange(1, 8, tallGap);
+ Assert.AreEqual(narrowGap, tallGap);
}
private static int GapBetweenRootAndFirstDirect(GraphRenderLayout layout)
@@ -249,14 +248,14 @@ private static int GapBetweenRootAndFirstDirect(GraphRenderLayout layout)
/// plus the distributed inter-band padding — it reflects the actual layout footprint
/// so scroll-range and fits-vs-scrolls decisions can be made from one authoritative value.
///
- [Fact]
+ [TestMethod]
public void ContentHeight_MatchesRenderedExtent()
{
var graph = BuildManyDirectRefsGraph(count: 80);
var layout = LayoutFor(graph, DependencyGraphScope.All, hideFramework: false, w: 80, h: 20);
var bottom = layout.Nodes.Max(rn => rn.Y + rn.Height);
- Assert.Equal(bottom, layout.ContentHeight);
+ Assert.AreEqual(bottom, layout.ContentHeight);
}
private static DependencyGraphResult BuildManyDirectRefsGraph(int count)
@@ -297,7 +296,7 @@ private static void AssertNoOverlap(GraphRenderLayout layout)
var b = layout.Nodes[j];
var horizOverlap = a.X < b.X + b.Width && b.X < a.X + a.Width;
var vertOverlap = a.Y < b.Y + b.Height && b.Y < a.Y + a.Height;
- Assert.False(horizOverlap && vertOverlap,
+ Assert.IsFalse(horizOverlap && vertOverlap,
$"boxes overlap: {a.Node.Name} at ({a.X},{a.Y},{a.Width},{a.Height}) " +
$"and {b.Node.Name} at ({b.X},{b.Y},{b.Width},{b.Height})");
}
diff --git a/tests/Dotsider.Tests/DependencyGraphScopeTests.cs b/tests/Dotsider.Tests/DependencyGraphScopeTests.cs
index 79545ccd..0674a9ed 100644
--- a/tests/Dotsider.Tests/DependencyGraphScopeTests.cs
+++ b/tests/Dotsider.Tests/DependencyGraphScopeTests.cs
@@ -9,47 +9,49 @@ namespace Dotsider.Tests;
/// all operate on the same view. Transitive-only is intentionally not offered: hiding direct
/// parents would produce disconnected islands.
///
-[Collection("SampleAssemblies")]
-public sealed class DependencyGraphScopeTests(SampleAssemblyFixture samples)
+[TestClass]
+public sealed class DependencyGraphScopeTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// keeps only the root and depth-1 nodes
/// plus the edges between them; deeper transitive refs drop out.
///
- [Fact]
+ [TestMethod]
public void DirectOnly_KeepsOnlyRootAndDepthOne()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
- Assert.Contains(graph.Nodes, n => n.Depth > 1);
+ Assert.Contains(n => n.Depth > 1, graph.Nodes);
var visible = DependencyGraphView.BuildVisibleModel(
graph.Nodes, graph.Edges, graph.NavigationById,
DependencyGraphScope.DirectOnly, hideFramework: false);
- Assert.All(visible.Nodes, n => Assert.True(n.IsRoot || n.Depth == 1));
+ TestAssert.All(visible.Nodes, n => Assert.IsTrue(n.IsRoot || n.Depth == 1));
var rootId = visible.Nodes.First(n => n.IsRoot).Id;
- Assert.All(visible.Edges, e => Assert.Equal(rootId, e.SourceId));
- Assert.All(visible.Edges, e => Assert.Contains(visible.Nodes, n => n.Id == e.TargetId));
+ TestAssert.All(visible.Edges, e => Assert.AreEqual(rootId, e.SourceId));
+ TestAssert.All(visible.Edges, e => Assert.Contains(n => n.Id == e.TargetId, visible.Nodes));
}
///
/// is the default and returns the full closure
/// unchanged — anything the builder produced is visible, edges included.
///
- [Fact]
+ [TestMethod]
public void All_ReturnsFullClosure()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var visible = DependencyGraphView.BuildVisibleModel(
graph.Nodes, graph.Edges, graph.NavigationById,
DependencyGraphScope.All, hideFramework: false);
- Assert.Equal(graph.Nodes.Count, visible.Nodes.Count);
- Assert.Equal(graph.Edges.Count, visible.Edges.Count);
+ Assert.HasCount(graph.Nodes.Count, visible.Nodes);
+ Assert.HasCount(graph.Edges.Count, visible.Edges);
}
///
@@ -57,23 +59,23 @@ public void All_ReturnsFullClosure()
/// and the non-framework direct references. For RichLibrary that collapses to root plus
/// Newtonsoft.Json.
///
- [Fact]
+ [TestMethod]
public void DirectOnly_ComposesWithFrameworkFilter()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var visible = DependencyGraphView.BuildVisibleModel(
graph.Nodes, graph.Edges, graph.NavigationById,
DependencyGraphScope.DirectOnly, hideFramework: true);
- Assert.Contains(visible.Nodes, n => n.IsRoot);
- Assert.All(visible.Nodes, n => Assert.True(n.IsRoot || n.Depth == 1));
- Assert.All(visible.Nodes, n =>
+ Assert.Contains(n => n.IsRoot, visible.Nodes);
+ TestAssert.All(visible.Nodes, n => Assert.IsTrue(n.IsRoot || n.Depth == 1));
+ TestAssert.All(visible.Nodes, n =>
{
if (n.IsRoot) return;
var ctx = graph.NavigationById[n.Id];
- Assert.False(ctx.IsFrameworkAssembly);
+ Assert.IsFalse(ctx.IsFrameworkAssembly);
});
}
@@ -81,20 +83,20 @@ public void DirectOnly_ComposesWithFrameworkFilter()
/// Root stays visible under every combination of scope and framework filter, so the
/// anchor of the graph is never lost.
///
- [Theory]
- [InlineData(DependencyGraphScope.All, false)]
- [InlineData(DependencyGraphScope.All, true)]
- [InlineData(DependencyGraphScope.DirectOnly, false)]
- [InlineData(DependencyGraphScope.DirectOnly, true)]
+ [TestMethod]
+ [DataRow(DependencyGraphScope.All, false)]
+ [DataRow(DependencyGraphScope.All, true)]
+ [DataRow(DependencyGraphScope.DirectOnly, false)]
+ [DataRow(DependencyGraphScope.DirectOnly, true)]
public void Root_IsAlwaysVisible(DependencyGraphScope scope, bool hideFramework)
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var visible = DependencyGraphView.BuildVisibleModel(
graph.Nodes, graph.Edges, graph.NavigationById, scope, hideFramework);
- Assert.Contains(visible.Nodes, n => n.IsRoot);
+ Assert.Contains(n => n.IsRoot, visible.Nodes);
}
///
@@ -102,18 +104,18 @@ public void Root_IsAlwaysVisible(DependencyGraphScope scope, bool hideFramework)
/// so index-based consumers (search, selection, yank) cannot accidentally reach into
/// hidden nodes.
///
- [Fact]
+ [TestMethod]
public void VisibleIndex_MapsOnlyVisibleNodes()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var graph = DependencyGraphBuilder.Build(a);
var visible = DependencyGraphView.BuildVisibleModel(
graph.Nodes, graph.Edges, graph.NavigationById,
DependencyGraphScope.DirectOnly, hideFramework: false);
- Assert.Equal(visible.Nodes.Count, visible.IndexById.Count);
+ Assert.HasCount(visible.Nodes.Count, visible.IndexById);
foreach (var n in visible.Nodes)
- Assert.Equal(n.Id, visible.Nodes[visible.IndexById[n.Id]].Id);
+ Assert.AreEqual(n.Id, visible.Nodes[visible.IndexById[n.Id]].Id);
}
}
diff --git a/tests/Dotsider.Tests/DgmlReaderTests.cs b/tests/Dotsider.Tests/DgmlReaderTests.cs
index 9a99f70e..72c52d46 100644
--- a/tests/Dotsider.Tests/DgmlReaderTests.cs
+++ b/tests/Dotsider.Tests/DgmlReaderTests.cs
@@ -7,25 +7,28 @@ namespace Dotsider.Tests;
/// Tests for against the real dependency graph published next to the
/// NativeAOT sample, plus synthetic and malformed documents.
///
-[Collection("SampleAssemblies")]
-public class DgmlReaderTests(SampleAssemblyFixture samples)
+[TestClass]
+public class DgmlReaderTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Verifies the fixture's codegen graph parses with consistent nodes and links: every
/// indexed link endpoint resolves to a node.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_FixtureDgml_ParsesNodesAndLinks()
{
- Assert.SkipWhen(samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
- var graph = DgmlReader.Read(samples.NativeAotConsoleDgml!);
+ var graph = DgmlReader.Read(Samples.NativeAotConsoleDgml!);
- Assert.NotNull(graph);
- Assert.NotEmpty(graph.Nodes);
- Assert.NotEmpty(graph.Links);
+ Assert.IsNotNull(graph);
+ Assert.IsNotEmpty(graph.Nodes);
+ Assert.IsNotEmpty(graph.Links);
var ids = graph.Nodes.Select(n => n.Id).ToHashSet();
- Assert.All(graph.Links, l =>
+ TestAssert.All(graph.Links, l =>
{
Assert.Contains(l.SourceId, ids);
Assert.Contains(l.TargetId, ids);
@@ -36,51 +39,54 @@ public void Read_FixtureDgml_ParsesNodesAndLinks()
/// Verifies a chain from a real method node reaches a root: the walk ends at a node with
/// no dependers, starts the returned list with it, and ends the list at the queried node.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PathToRoot_KnownMethodLabel_ReachesRoot()
{
- Assert.SkipWhen(samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
- Assert.SkipWhen(samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleMstat is null, "mstat sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
- var graph = DgmlReader.Read(samples.NativeAotConsoleDgml!);
- var data = MstatReader.Read(samples.NativeAotConsoleMstat!);
- Assert.NotNull(graph);
- Assert.NotNull(data);
+ var graph = DgmlReader.Read(Samples.NativeAotConsoleDgml!);
+ var data = MstatReader.Read(Samples.NativeAotConsoleMstat!);
+ Assert.IsNotNull(graph);
+ Assert.IsNotNull(data);
// Any compiled method that is present in the graph will do; take the first join hit.
var nodeName = data.Methods
.Select(m => m.NodeName)
.FirstOrDefault(n => n is not null && graph.FindNodeByLabel(n) is not null);
- Assert.NotNull(nodeName);
+ Assert.IsNotNull(nodeName);
var path = graph.PathToRoot(nodeName!);
- Assert.NotEmpty(path);
- Assert.Equal(nodeName, path[^1].Label);
- Assert.Null(path[0].Reason);
+ Assert.IsNotEmpty(path);
+ Assert.AreEqual(nodeName, path[^1].Label);
+ Assert.IsNull(path[0].Reason);
var rootNode = graph.FindNodeByLabel(path[0].Label);
- Assert.NotNull(rootNode);
- Assert.DoesNotContain(graph.Links, l => l.TargetId == rootNode.Id);
+ Assert.IsNotNull(rootNode);
+ Assert.DoesNotContain(l => l.TargetId == rootNode.Id, graph.Links);
}
///
/// Verifies an unknown label yields an empty chain rather than throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PathToRoot_UnknownLabel_ReturnsEmpty()
{
- Assert.SkipWhen(samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
+ TestSkip.When(Samples.NativeAotConsoleDgml is null, "DGML sidecar was not produced");
- var graph = DgmlReader.Read(samples.NativeAotConsoleDgml!);
+ var graph = DgmlReader.Read(Samples.NativeAotConsoleDgml!);
- Assert.NotNull(graph);
- Assert.Empty(graph.PathToRoot("no-such-node-anywhere"));
+ Assert.IsNotNull(graph);
+ Assert.IsEmpty(graph.PathToRoot("no-such-node-anywhere"));
}
///
/// Verifies querying a root returns the single-step chain: the root explains itself.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PathToRoot_RootNode_ReturnsSingleStep()
{
var graph = DgmlReader.Read(ToStream("""
@@ -95,16 +101,17 @@ public void PathToRoot_RootNode_ReturnsSingleStep()
"""));
- Assert.NotNull(graph);
+ Assert.IsNotNull(graph);
var path = graph.PathToRoot("root");
- Assert.Single(path);
- Assert.Equal("root", path[0].Label);
+ Assert.ContainsSingle(path);
+ Assert.AreEqual("root", path[0].Label);
}
///
/// Verifies a multi-step synthetic chain reconstructs root-first with per-step reasons.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PathToRoot_SyntheticChain_ReturnsRootFirstWithReasons()
{
var graph = DgmlReader.Read(ToStream("""
@@ -121,19 +128,20 @@ public void PathToRoot_SyntheticChain_ReturnsRootFirstWithReasons()
"""));
- Assert.NotNull(graph);
+ Assert.IsNotNull(graph);
var path = graph.PathToRoot("Leaf");
- Assert.Equal(3, path.Count);
- Assert.Equal(("Main method", (string?)null), (path[0].Label, path[0].Reason));
- Assert.Equal(("Helper", "call"), (path[1].Label, path[1].Reason));
- Assert.Equal(("Leaf", "field access"), (path[2].Label, path[2].Reason));
+ Assert.HasCount(3, path);
+ Assert.AreEqual(("Main method", (string?)null), (path[0].Label, path[0].Reason));
+ Assert.AreEqual(("Helper", "call"), (path[1].Label, path[1].Reason));
+ Assert.AreEqual(("Leaf", "field access"), (path[2].Label, path[2].Reason));
}
///
/// Verifies a cycle with no root reports the queried node alone instead of spinning.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PathToRoot_PureCycle_ReturnsQueriedNodeAlone()
{
var graph = DgmlReader.Read(ToStream("""
@@ -149,17 +157,18 @@ public void PathToRoot_PureCycle_ReturnsQueriedNodeAlone()
"""));
- Assert.NotNull(graph);
+ Assert.IsNotNull(graph);
var path = graph.PathToRoot("a");
- Assert.Single(path);
- Assert.Equal("a", path[0].Label);
+ Assert.ContainsSingle(path);
+ Assert.AreEqual("a", path[0].Label);
}
///
/// Verifies a document without the dgml namespace still parses — matching is by local
/// element name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_NoNamespace_StillParses()
{
var graph = DgmlReader.Read(ToStream("""
@@ -169,15 +178,16 @@ public void Read_NoNamespace_StillParses()
"""));
- Assert.NotNull(graph);
- Assert.Single(graph.Nodes);
- Assert.Empty(graph.Links);
+ Assert.IsNotNull(graph);
+ Assert.ContainsSingle(graph.Nodes);
+ Assert.IsEmpty(graph.Links);
}
///
/// Verifies duplicate labels resolve to the first node without throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_DuplicateLabels_FirstWins()
{
var graph = DgmlReader.Read(ToStream("""
@@ -189,14 +199,17 @@ public void Read_DuplicateLabels_FirstWins()
"""));
- Assert.NotNull(graph);
- Assert.Equal(1, graph.FindNodeByLabel("dup")?.Id);
+ Assert.IsNotNull(graph);
+ var node = graph.FindNodeByLabel("dup");
+ Assert.IsNotNull(node);
+ Assert.AreEqual(1, node.Id);
}
///
/// Verifies malformed nodes and links are skipped while well-formed siblings survive.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_MalformedEntries_AreSkipped()
{
var graph = DgmlReader.Read(ToStream("""
@@ -212,45 +225,49 @@ public void Read_MalformedEntries_AreSkipped()
"""));
- Assert.NotNull(graph);
- Assert.Single(graph.Nodes);
- Assert.Single(graph.Links);
+ Assert.IsNotNull(graph);
+ Assert.ContainsSingle(graph.Nodes);
+ Assert.ContainsSingle(graph.Links);
}
///
/// Verifies a truncated document returns null rather than throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_TruncatedXml_ReturnsNull()
{
- Assert.Null(DgmlReader.Read(ToStream("
/// Verifies a document with the wrong root element returns null.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_WrongRootElement_ReturnsNull()
{
- Assert.Null(DgmlReader.Read(ToStream("")));
+ Assert.IsNull(DgmlReader.Read(ToStream("")));
}
///
/// Verifies an empty file returns null rather than throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_EmptyFile_ReturnsNull()
{
- Assert.Null(DgmlReader.Read(ToStream("")));
+ Assert.IsNull(DgmlReader.Read(ToStream("")));
}
///
/// Verifies a missing file returns null rather than throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Read_MissingFile_ReturnsNull()
{
- Assert.Null(DgmlReader.Read(Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}.dgml.xml")));
+ Assert.IsNull(DgmlReader.Read(Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}.dgml.xml")));
}
private static MemoryStream ToStream(string xml) => new(Encoding.UTF8.GetBytes(xml));
diff --git a/tests/Dotsider.Tests/DiffDecorationProviderTests.cs b/tests/Dotsider.Tests/DiffDecorationProviderTests.cs
index 0b8ba099..34bad245 100644
--- a/tests/Dotsider.Tests/DiffDecorationProviderTests.cs
+++ b/tests/Dotsider.Tests/DiffDecorationProviderTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
///
/// Tests for Diff Decoration Provider.
///
+[TestClass]
public class DiffDecorationProviderTests
{
// --- DiffSearchDecorationProvider ---
@@ -14,7 +15,7 @@ public class DiffDecorationProviderTests
///
/// Verifies diff search highlights all matches case insensitive.
///
- [Fact]
+ [TestMethod]
public void DiffSearch_HighlightsAllMatches_CaseInsensitive()
{
var provider = new DiffSearchDecorationProvider { Query = "rich" };
@@ -23,15 +24,15 @@ public void DiffSearch_HighlightsAllMatches_CaseInsensitive()
var spans = provider.GetDecorations(1, 3, doc);
// "Rich" on line 1, "rich" on line 3
- Assert.Equal(2, spans.Count);
- Assert.Equal(1, spans[0].Start.Line);
- Assert.Equal(3, spans[1].Start.Line);
+ Assert.HasCount(2, spans);
+ Assert.AreEqual(1, spans[0].Start.Line);
+ Assert.AreEqual(3, spans[1].Start.Line);
}
///
/// Verifies diff search matches own foreground and background colors.
///
- [Fact]
+ [TestMethod]
public void DiffSearch_MatchesOwnForegroundAndBackground()
{
var provider = new DiffSearchDecorationProvider { Query = "rich" };
@@ -39,9 +40,9 @@ public void DiffSearch_MatchesOwnForegroundAndBackground()
var spans = provider.GetDecorations(1, 1, doc);
- var span = Assert.Single(spans);
- Assert.NotNull(span.Decoration.Foreground);
- Assert.NotNull(span.Decoration.Background);
+ var span = Assert.ContainsSingle(spans);
+ Assert.IsNotNull(span.Decoration.Foreground);
+ Assert.IsNotNull(span.Decoration.Background);
AssertColorEquals(HighlightHelper.MatchFgColor, span.Decoration.Foreground.Value);
AssertColorEquals(HighlightHelper.MatchBgColor, span.Decoration.Background.Value);
}
@@ -49,7 +50,7 @@ public void DiffSearch_MatchesOwnForegroundAndBackground()
///
/// Verifies diff search multiple matches on same line.
///
- [Fact]
+ [TestMethod]
public void DiffSearch_MultipleMatchesOnSameLine()
{
var provider = new DiffSearchDecorationProvider { Query = "ab" };
@@ -57,43 +58,43 @@ public void DiffSearch_MultipleMatchesOnSameLine()
var spans = provider.GetDecorations(1, 1, doc);
- Assert.Equal(3, spans.Count);
+ Assert.HasCount(3, spans);
}
///
/// Verifies diff search null query returns empty.
///
- [Fact]
+ [TestMethod]
public void DiffSearch_NullQuery_ReturnsEmpty()
{
var provider = new DiffSearchDecorationProvider { Query = null };
var doc = new Hex1bDocument("some text");
- Assert.Empty(provider.GetDecorations(1, 1, doc));
+ Assert.IsEmpty(provider.GetDecorations(1, 1, doc));
}
///
/// Verifies diff search empty query returns empty.
///
- [Fact]
+ [TestMethod]
public void DiffSearch_EmptyQuery_ReturnsEmpty()
{
var provider = new DiffSearchDecorationProvider { Query = "" };
var doc = new Hex1bDocument("some text");
- Assert.Empty(provider.GetDecorations(1, 1, doc));
+ Assert.IsEmpty(provider.GetDecorations(1, 1, doc));
}
///
/// Verifies diff search no match returns empty.
///
- [Fact]
+ [TestMethod]
public void DiffSearch_NoMatch_ReturnsEmpty()
{
var provider = new DiffSearchDecorationProvider { Query = "xyz" };
var doc = new Hex1bDocument("abc def");
- Assert.Empty(provider.GetDecorations(1, 1, doc));
+ Assert.IsEmpty(provider.GetDecorations(1, 1, doc));
}
// --- DiffStatsDecorationProvider ---
@@ -101,7 +102,7 @@ public void DiffSearch_NoMatch_ReturnsEmpty()
///
/// Verifies diff stats colors stats lines.
///
- [Fact]
+ [TestMethod]
public void DiffStats_ColorsStatsLines()
{
var provider = new DiffStatsDecorationProvider();
@@ -110,13 +111,13 @@ public void DiffStats_ColorsStatsLines()
var spans = provider.GetDecorations(1, 2, doc);
// Each line has 3 colored values (+N, -N, ~N) = 6 total
- Assert.Equal(6, spans.Count);
+ Assert.HasCount(6, spans);
}
///
/// Verifies diff stats does not color size delta line.
///
- [Fact]
+ [TestMethod]
public void DiffStats_DoesNotColorSizeDeltaLine()
{
var provider = new DiffStatsDecorationProvider();
@@ -125,13 +126,13 @@ public void DiffStats_DoesNotColorSizeDeltaLine()
var spans = provider.GetDecorations(1, 1, doc);
// Line has no ~ so it's skipped entirely
- Assert.Empty(spans);
+ Assert.IsEmpty(spans);
}
///
/// Verifies diff stats ignores lines without tilde.
///
- [Fact]
+ [TestMethod]
public void DiffStats_IgnoresLinesWithoutTilde()
{
var provider = new DiffStatsDecorationProvider();
@@ -139,13 +140,13 @@ public void DiffStats_IgnoresLinesWithoutTilde()
var spans = provider.GetDecorations(1, 1, doc);
- Assert.Empty(spans);
+ Assert.IsEmpty(spans);
}
///
/// Verifies diff stats requires digit after prefix.
///
- [Fact]
+ [TestMethod]
public void DiffStats_RequiresDigitAfterPrefix()
{
var provider = new DiffStatsDecorationProvider();
@@ -154,25 +155,25 @@ public void DiffStats_RequiresDigitAfterPrefix()
var spans = provider.GetDecorations(1, 1, doc);
- Assert.Single(spans);
+ Assert.ContainsSingle(spans);
}
///
/// Verifies diff stats empty lines returns empty.
///
- [Fact]
+ [TestMethod]
public void DiffStats_EmptyLines_ReturnsEmpty()
{
var provider = new DiffStatsDecorationProvider();
var doc = new Hex1bDocument("\n\n");
- Assert.Empty(provider.GetDecorations(1, 3, doc));
+ Assert.IsEmpty(provider.GetDecorations(1, 3, doc));
}
private static void AssertColorEquals(Hex1bColor expected, Hex1bColor actual)
{
- Assert.Equal(expected.R, actual.R);
- Assert.Equal(expected.G, actual.G);
- Assert.Equal(expected.B, actual.B);
+ Assert.AreEqual(expected.R, actual.R);
+ Assert.AreEqual(expected.G, actual.G);
+ Assert.AreEqual(expected.B, actual.B);
}
}
diff --git a/tests/Dotsider.Tests/DiffModeViewTests.cs b/tests/Dotsider.Tests/DiffModeViewTests.cs
index e9e1a82c..52d96048 100644
--- a/tests/Dotsider.Tests/DiffModeViewTests.cs
+++ b/tests/Dotsider.Tests/DiffModeViewTests.cs
@@ -8,9 +8,11 @@ namespace Dotsider.Tests;
///
/// Tests for Diff Mode View.
///
-[Collection("SampleAssemblies")]
-public class DiffModeViewTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DiffModeViewTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -28,7 +30,7 @@ public class DiffModeViewTests(SampleAssemblyFixture samples) : IDisposable
_hex1bApp = new Hex1bApp(
ctx =>
{
- _state ??= new DiffState(_hex1bApp!, samples.RichLibraryDll, samples.RichLibraryV2Dll);
+ _state ??= new DiffState(_hex1bApp!, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
diffApp ??= new DiffApp(_state);
return Task.FromResult(diffApp.Build(ctx));
},
@@ -43,11 +45,12 @@ public class DiffModeViewTests(SampleAssemblyFixture samples) : IDisposable
///
/// Verifies diff app launches shows both assemblies.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffApp_Launches_ShowsBothAssemblies()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -67,11 +70,12 @@ public async Task DiffApp_Launches_ShowsBothAssemblies()
///
/// Verifies diff app shows diff entries.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffApp_ShowsDiffEntries()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -91,11 +95,12 @@ public async Task DiffApp_ShowsDiffEntries()
///
/// Verifies quit key exits diff app.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task QuitKey_ExitsDiffApp()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -107,17 +112,18 @@ public async Task QuitKey_ExitsDiffApp()
.ApplyAsync(terminal, ct);
var completed = await Task.WhenAny(runTask, Task.Delay(5000, ct));
- Assert.Equal(runTask, completed);
+ Assert.AreEqual(runTask, completed);
}
///
/// Verifies arrow keys cycle tabs.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ArrowKeys_CycleTabs()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -137,7 +143,7 @@ public async Task ArrowKeys_CycleTabs()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(1, _state!.CurrentTab);
+ Assert.AreEqual(1, _state!.CurrentTab);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -145,11 +151,12 @@ public async Task ArrowKeys_CycleTabs()
///
/// Verifies search activates and filters.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Search_ActivatesAndFilters()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -165,7 +172,7 @@ public async Task Search_ActivatesAndFilters()
.Build()
.ApplyAsync(terminal, ct);
- Assert.True(_state!.Search[1].IsActive);
+ Assert.IsTrue(_state!.Search[1].IsActive);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -173,11 +180,12 @@ public async Task Search_ActivatesAndFilters()
///
/// Verifies diff app shows references tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffApp_ShowsReferencesTab()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -194,7 +202,7 @@ public async Task DiffApp_ShowsReferencesTab()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(3, _state!.CurrentTab);
+ Assert.AreEqual(3, _state!.CurrentTab);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -202,11 +210,12 @@ public async Task DiffApp_ShowsReferencesTab()
///
/// Verifies diff app shows methods tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DiffApp_ShowsMethodsTab()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -223,7 +232,7 @@ public async Task DiffApp_ShowsMethodsTab()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(2, _state!.CurrentTab);
+ Assert.AreEqual(2, _state!.CurrentTab);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -231,11 +240,12 @@ public async Task DiffApp_ShowsMethodsTab()
///
/// Verifies left arrow does not go below zero.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task LeftArrow_DoesNotGoBelowZero()
{
var (terminal, app) = CreateDiffApp();
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -249,7 +259,7 @@ public async Task LeftArrow_DoesNotGoBelowZero()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(0, _state!.CurrentTab);
+ Assert.AreEqual(0, _state!.CurrentTab);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -259,10 +269,10 @@ public async Task LeftArrow_DoesNotGoBelowZero()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/DiffModeYankIntegrationTests.cs b/tests/Dotsider.Tests/DiffModeYankIntegrationTests.cs
index 0c6e408b..2c72c0c5 100644
--- a/tests/Dotsider.Tests/DiffModeYankIntegrationTests.cs
+++ b/tests/Dotsider.Tests/DiffModeYankIntegrationTests.cs
@@ -9,19 +9,22 @@ namespace Dotsider.Tests;
///
/// Tests for Diff Mode Yank Integration.
///
-[Collection("SampleAssemblies")]
-public class DiffModeYankIntegrationTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DiffModeYankIntegrationTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private ClipboardCapturingWorkloadAdapter? _clipboardAdapter;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
private DiffState? _state;
private CancellationTokenSource? _cts;
+ private Task? _runTask;
private (Hex1bTerminal terminal, Hex1bApp app, CancellationToken ct) Launch()
{
- _cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
_workload = new Hex1bAppWorkloadAdapter();
_clipboardAdapter = new ClipboardCapturingWorkloadAdapter(_workload);
_terminal = Hex1bTerminal.CreateBuilder()
@@ -33,7 +36,7 @@ public class DiffModeYankIntegrationTests(SampleAssemblyFixture samples) : IDisp
_hex1bApp = new Hex1bApp(
ctx =>
{
- _state ??= new DiffState(_hex1bApp!, samples.RichLibraryDll, samples.RichLibraryV2Dll);
+ _state ??= new DiffState(_hex1bApp!, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
diffApp ??= new DiffApp(_state);
return Task.FromResult(diffApp.Build(ctx));
},
@@ -45,16 +48,60 @@ public class DiffModeYankIntegrationTests(SampleAssemblyFixture samples) : IDisp
return (_terminal, _hex1bApp, _cts.Token);
}
+ private Task RunAppAsync(Hex1bApp app, CancellationToken ct)
+ {
+ _runTask = app.RunAsync(ct);
+ return _runTask;
+ }
+
+ private async Task WaitForStableYankPayloadAsync(CancellationToken ct)
+ {
+ string? last = null;
+ var stablePolls = 0;
+ var timeoutAt = DateTime.UtcNow + TimeSpan.FromSeconds(5);
+ while (DateTime.UtcNow < timeoutAt)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var current = _state is null ? null : YankHelper.GetYankText(_state);
+ if (current is not null && current == last)
+ {
+ stablePolls++;
+ if (stablePolls >= 2)
+ return current;
+ }
+ else
+ {
+ last = current;
+ stablePolls = current is null ? 0 : 1;
+ }
+
+ await Task.Delay(50, ct);
+ }
+
+ Assert.Fail("Timed out waiting for diff yank payload to settle.");
+ throw new InvalidOperationException("Unreachable.");
+ }
+
+ private bool TryWaitForAppExit()
+ {
+ if (_runTask is null) return true;
+ try { return _runTask.Wait(TimeSpan.FromSeconds(5)); }
+ catch (AggregateException ex) when (ex.InnerExceptions.All(static e => e is OperationCanceledException)) { return true; }
+ catch (OperationCanceledException) { return true; }
+ }
+
// --- Summary tab ---
///
/// Verifies summary tab cycles through editors.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Summary_TabCyclesThroughEditors()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -105,11 +152,12 @@ public async Task Summary_TabCyclesThroughEditors()
///
/// Verifies summary left right do not switch tabs when editor focused.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Summary_LeftRightDoNotSwitchTabsWhenEditorFocused()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -131,7 +179,7 @@ public async Task Summary_LeftRightDoNotSwitchTabsWhenEditorFocused()
.ApplyAsync(terminal, ct);
await Task.Delay(100, ct);
- Assert.Equal(tabBefore, _state.CurrentTab);
+ Assert.AreEqual(tabBefore, _state.CurrentTab);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -142,11 +190,12 @@ public async Task Summary_LeftRightDoNotSwitchTabsWhenEditorFocused()
///
/// Verifies types yank on focused row shows notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Types_YankOnFocusedRow_ShowsNotification()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -157,8 +206,6 @@ public async Task Types_YankOnFocusedRow_ShowsNotification()
.Build()
.ApplyAsync(terminal, ct);
- await Task.Delay(200, ct);
-
// If DiffFocusedKey is still null, try j to navigate in table
if (_state!.DiffFocusedKey is null)
{
@@ -169,26 +216,23 @@ public async Task Types_YankOnFocusedRow_ShowsNotification()
await Task.Delay(200, ct);
}
- Assert.NotNull(_state.DiffFocusedKey);
+ Assert.IsNotNull(_state.DiffFocusedKey);
- // Compute expected payload before yank
- var expectedPayload = YankHelper.GetYankText(_state);
- Assert.NotNull(expectedPayload);
+ // Compute expected payload after the table's focus callback has settled.
+ var expectedPayload = await WaitForStableYankPayloadAsync(ct);
Assert.Contains("\t", expectedPayload); // Tab-separated
// Yank
await new Hex1bTerminalInputSequenceBuilder()
.Type("y")
- .WaitUntil(s => s.ContainsText("Yanked:"), TimeSpan.FromSeconds(5))
+ .WaitUntil(_ => _state!.YankNotification is not null, TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
-
// Verify the actual OSC 52 clipboard payload emitted by ctx.CopyToClipboard
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out var actualClipboard),
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out var actualClipboard),
"CopyToClipboard should have emitted an OSC 52 sequence");
- Assert.Equal(expectedPayload, actualClipboard);
+ Assert.AreEqual(expectedPayload, actualClipboard);
// Notification auto-clears
await new Hex1bTerminalInputSequenceBuilder()
@@ -203,11 +247,12 @@ public async Task Types_YankOnFocusedRow_ShowsNotification()
///
/// Verifies types search with navigation cycles matches.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Types_SearchWithNavigation_CyclesMatches()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -251,7 +296,7 @@ await TestHelpers.WaitUntilAsync(
TimeSpan.FromSeconds(5));
var secondFocused = _state.DiffFocusedKey;
- Assert.NotEqual(firstFocused, secondFocused);
+ Assert.AreNotEqual(firstFocused, secondFocused);
// N → previous match (moves to a different row)
await new Hex1bTerminalInputSequenceBuilder()
@@ -263,7 +308,7 @@ await TestHelpers.WaitUntilAsync(
() => _state!.DiffFocusedKey is not null && !Equals(_state.DiffFocusedKey, secondFocused),
TimeSpan.FromSeconds(5));
- Assert.NotEqual(secondFocused, _state.DiffFocusedKey);
+ Assert.AreNotEqual(secondFocused, _state.DiffFocusedKey);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -272,11 +317,12 @@ await TestHelpers.WaitUntilAsync(
///
/// Verifies tab arrows work when editor not focused.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task TabArrows_WorkWhenEditorNotFocused()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -284,7 +330,7 @@ public async Task TabArrows_WorkWhenEditorNotFocused()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(0, _state!.CurrentTab); // Summary
+ Assert.AreEqual(0, _state!.CurrentTab); // Summary
// Right arrow switches to Types tab (tab index 1)
await new Hex1bTerminalInputSequenceBuilder()
@@ -293,7 +339,7 @@ public async Task TabArrows_WorkWhenEditorNotFocused()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(1, _state.CurrentTab);
+ Assert.AreEqual(1, _state.CurrentTab);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -304,11 +350,12 @@ public async Task TabArrows_WorkWhenEditorNotFocused()
///
/// Verifies summary selection yank shows notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Summary_SelectionYank_ShowsNotification()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -336,12 +383,10 @@ public async Task Summary_SelectionYank_ShowsNotification()
// Yank
await new Hex1bTerminalInputSequenceBuilder()
.Type("y")
- .WaitUntil(s => s.ContainsText("Yanked:"), TimeSpan.FromSeconds(5))
+ .WaitUntil(_ => _state!.YankNotification is not null, TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
-
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
}
@@ -349,11 +394,12 @@ public async Task Summary_SelectionYank_ShowsNotification()
///
/// Verifies summary right info yank shows notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Summary_RightInfoYank_ShowsNotification()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -376,12 +422,10 @@ public async Task Summary_RightInfoYank_ShowsNotification()
.Shift().Key(Hex1bKey.RightArrow)
.WaitUntil(_ => _state!.RightInfoEditorState!.Cursor.HasSelection, TimeSpan.FromSeconds(5))
.Type("y")
- .WaitUntil(s => s.ContainsText("Yanked:"), TimeSpan.FromSeconds(5))
+ .WaitUntil(_ => _state!.YankNotification is not null, TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
-
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
}
@@ -389,11 +433,12 @@ public async Task Summary_RightInfoYank_ShowsNotification()
///
/// Verifies summary change stats yank shows notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Summary_ChangeStatsYank_ShowsNotification()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -429,12 +474,10 @@ public async Task Summary_ChangeStatsYank_ShowsNotification()
.Shift().Key(Hex1bKey.RightArrow)
.WaitUntil(_ => _state!.ChangeStatsEditorState!.Cursor.HasSelection, TimeSpan.FromSeconds(5))
.Type("y")
- .WaitUntil(s => s.ContainsText("Yanked:"), TimeSpan.FromSeconds(5))
+ .WaitUntil(_ => _state!.YankNotification is not null, TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
-
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
}
@@ -444,11 +487,12 @@ public async Task Summary_ChangeStatsYank_ShowsNotification()
///
/// Verifies methods yank on focused row shows notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Methods_YankOnFocusedRow_ShowsNotification()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -462,12 +506,10 @@ public async Task Methods_YankOnFocusedRow_ShowsNotification()
await new Hex1bTerminalInputSequenceBuilder()
.Type("y")
- .WaitUntil(s => s.ContainsText("Yanked:"), TimeSpan.FromSeconds(5))
+ .WaitUntil(_ => _state!.YankNotification is not null, TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
-
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
}
@@ -477,11 +519,12 @@ public async Task Methods_YankOnFocusedRow_ShowsNotification()
///
/// Verifies refs yank on focused row shows notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Refs_YankOnFocusedRow_ShowsNotification()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -495,12 +538,10 @@ public async Task Refs_YankOnFocusedRow_ShowsNotification()
await new Hex1bTerminalInputSequenceBuilder()
.Type("y")
- .WaitUntil(s => s.ContainsText("Yanked:"), TimeSpan.FromSeconds(5))
+ .WaitUntil(_ => _state!.YankNotification is not null, TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
-
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
}
@@ -510,11 +551,12 @@ public async Task Refs_YankOnFocusedRow_ShowsNotification()
///
/// Verifies methods search navigation cycles matches.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Methods_SearchNavigation_CyclesMatches()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -535,6 +577,7 @@ public async Task Methods_SearchNavigation_CyclesMatches()
var s = _state!.Search[_state.CurrentTab];
return s.IsActive && s.IsConfirmed;
}, TimeSpan.FromSeconds(5))
+ .WaitUntil(s => s.ContainsText("n/N: navigate"), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
@@ -542,21 +585,21 @@ public async Task Methods_SearchNavigation_CyclesMatches()
await new Hex1bTerminalInputSequenceBuilder()
.Type("n")
+ .WaitUntil(_ => !Equals(_state!.DiffFocusedKey, first), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- await Task.Delay(100, ct);
var second = _state.DiffFocusedKey;
- Assert.NotEqual(first, second);
+ Assert.AreNotEqual(first, second);
// N → previous match (moves to a different row)
await new Hex1bTerminalInputSequenceBuilder()
.Shift().Key(Hex1bKey.N)
+ .WaitUntil(_ => !Equals(_state!.DiffFocusedKey, second), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- await Task.Delay(100, ct);
- Assert.NotEqual(second, _state.DiffFocusedKey);
+ Assert.AreNotEqual(second, _state.DiffFocusedKey);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -567,11 +610,12 @@ public async Task Methods_SearchNavigation_CyclesMatches()
///
/// Verifies refs search navigation cycles matches.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Refs_SearchNavigation_CyclesMatches()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -592,6 +636,7 @@ public async Task Refs_SearchNavigation_CyclesMatches()
var s = _state!.Search[_state.CurrentTab];
return s.IsActive && s.IsConfirmed;
}, TimeSpan.FromSeconds(5))
+ .WaitUntil(s => s.ContainsText("n/N: navigate"), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
@@ -599,21 +644,21 @@ public async Task Refs_SearchNavigation_CyclesMatches()
await new Hex1bTerminalInputSequenceBuilder()
.Type("n")
+ .WaitUntil(_ => !Equals(_state!.DiffFocusedKey, first), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- await Task.Delay(100, ct);
var second = _state.DiffFocusedKey;
- Assert.NotEqual(first, second);
+ Assert.AreNotEqual(first, second);
// N → previous match (moves to a different row)
await new Hex1bTerminalInputSequenceBuilder()
.Shift().Key(Hex1bKey.N)
+ .WaitUntil(_ => !Equals(_state!.DiffFocusedKey, second), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
- await Task.Delay(100, ct);
- Assert.NotEqual(second, _state.DiffFocusedKey);
+ Assert.AreNotEqual(second, _state.DiffFocusedKey);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -624,11 +669,12 @@ public async Task Refs_SearchNavigation_CyclesMatches()
///
/// Verifies types yank flash sets and clears.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Types_YankFlash_SetsAndClears()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -652,7 +698,7 @@ public async Task Types_YankFlash_SetsAndClears()
.Build()
.ApplyAsync(terminal, ct);
- Assert.False(_state!.YankFlashRow);
+ Assert.IsFalse(_state!.YankFlashRow);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -661,11 +707,12 @@ public async Task Types_YankFlash_SetsAndClears()
///
/// Verifies summary yy yanks current line.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Summary_YY_YanksCurrentLine()
{
var (terminal, app, ct) = Launch();
- var runTask = app.RunAsync(ct);
+ var runTask = RunAppAsync(app, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.InAlternateScreen, TimeSpan.FromSeconds(10))
@@ -688,10 +735,10 @@ public async Task Summary_YY_YanksCurrentLine()
.Build()
.ApplyAsync(terminal, ct);
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out var yankedText),
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out var yankedText),
"CopyToClipboard should have emitted an OSC 52 sequence");
- Assert.True(yankedText.Length > 0);
+ Assert.IsGreaterThan(0, yankedText.Length);
Assert.DoesNotContain("\n", yankedText);
_cts!.Cancel();
@@ -703,12 +750,18 @@ public async Task Summary_YY_YanksCurrentLine()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_cts?.Cancel();
+ if (!TryWaitForAppExit())
+ {
+ _hex1bApp?.Dispose();
+ _terminal?.Dispose();
+ _ = TryWaitForAppExit();
+ }
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
_cts?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/DiffStateTests.cs b/tests/Dotsider.Tests/DiffStateTests.cs
index f0dbb256..7ae49a85 100644
--- a/tests/Dotsider.Tests/DiffStateTests.cs
+++ b/tests/Dotsider.Tests/DiffStateTests.cs
@@ -6,9 +6,11 @@ namespace Dotsider.Tests;
///
/// Tests for Diff State.
///
-[Collection("SampleAssemblies")]
-public class DiffStateTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DiffStateTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _app;
@@ -30,60 +32,65 @@ private Hex1bApp CreateApp()
///
/// Verifies construct both analyzers accessible.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Construct_BothAnalyzersAccessible()
{
var app = CreateApp();
- using var state = new DiffState(app, samples.RichLibraryDll, samples.RichLibraryV2Dll);
- Assert.NotNull(state.Left);
- Assert.NotNull(state.Right);
- Assert.Equal("RichLibrary", state.Left.AssemblyName);
- Assert.Equal("RichLibrary", state.Right.AssemblyName);
+ using var state = new DiffState(app, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
+ Assert.IsNotNull(state.Left);
+ Assert.IsNotNull(state.Right);
+ Assert.AreEqual("RichLibrary", state.Left.AssemblyName);
+ Assert.AreEqual("RichLibrary", state.Right.AssemblyName);
}
///
/// Verifies construct diff result populated.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Construct_DiffResultPopulated()
{
var app = CreateApp();
- using var state = new DiffState(app, samples.RichLibraryDll, samples.RichLibraryV2Dll);
- Assert.NotNull(state.DiffResult);
- Assert.NotEmpty(state.DiffResult.TypeDiffs);
+ using var state = new DiffState(app, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
+ Assert.IsNotNull(state.DiffResult);
+ Assert.IsNotEmpty(state.DiffResult.TypeDiffs);
}
///
/// Verifies default filter mode is all.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void DefaultFilterMode_IsAll()
{
var app = CreateApp();
- using var state = new DiffState(app, samples.RichLibraryDll, samples.RichLibraryV2Dll);
- Assert.Equal(DiffFilterMode.All, state.FilterMode);
+ using var state = new DiffState(app, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
+ Assert.AreEqual(DiffFilterMode.All, state.FilterMode);
}
///
/// Verifies tab switching works.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TabSwitching_Works()
{
var app = CreateApp();
- using var state = new DiffState(app, samples.RichLibraryDll, samples.RichLibraryV2Dll);
+ using var state = new DiffState(app, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
state.CurrentTab = 2;
- Assert.Equal(2, state.CurrentTab);
+ Assert.AreEqual(2, state.CurrentTab);
}
///
/// Verifies dispose cleans up.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Dispose_CleansUp()
{
var app = CreateApp();
- var state = new DiffState(app, samples.RichLibraryDll, samples.RichLibraryV2Dll);
+ var state = new DiffState(app, Samples.RichLibraryDll, Samples.RichLibraryV2Dll);
state.Dispose();
state.Dispose(); // idempotent — should not throw
}
@@ -93,9 +100,9 @@ public void Dispose_CleansUp()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_app?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/DotNetRuntimeLocatorTests.cs b/tests/Dotsider.Tests/DotNetRuntimeLocatorTests.cs
index dc942576..1c2557dd 100644
--- a/tests/Dotsider.Tests/DotNetRuntimeLocatorTests.cs
+++ b/tests/Dotsider.Tests/DotNetRuntimeLocatorTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
/// Tests for covering .NET base path discovery
/// and shared framework assembly resolution.
///
+[TestClass]
public sealed class DotNetRuntimeLocatorTests : IDisposable
{
/// Initializes a new instance and clears the locator cache.
@@ -16,80 +17,88 @@ public sealed class DotNetRuntimeLocatorTests : IDisposable
public void Dispose() => DotNetRuntimeLocator.ClearCache();
/// Verifies that the .NET base path exists and contains a shared directory.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindDotNetBasePath_ReturnsValidDirectory()
{
var basePath = DotNetRuntimeLocator.FindDotNetBasePath();
- Assert.NotNull(basePath);
- Assert.True(Directory.Exists(basePath));
- Assert.True(Directory.Exists(Path.Combine(basePath, "shared")));
+ Assert.IsNotNull(basePath);
+ Assert.IsTrue(Directory.Exists(basePath));
+ Assert.IsTrue(Directory.Exists(Path.Combine(basePath, "shared")));
}
/// Verifies that System.Runtime can be found in the shared framework.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_SystemRuntime_ReturnsPathAndPack()
{
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework("System.Runtime", null);
- Assert.NotNull(result);
- Assert.True(File.Exists(result.Path));
+ Assert.IsNotNull(result);
+ Assert.IsTrue(File.Exists(result.Path));
Assert.EndsWith(".dll", result.Path);
- Assert.NotEmpty(result.RuntimePack);
+ Assert.IsNotEmpty(result.RuntimePack);
}
/// Verifies that System.Private.CoreLib can be found in the shared framework.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_SystemPrivateCoreLib_ReturnsPath()
{
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework("System.Private.CoreLib", null);
- Assert.NotNull(result);
- Assert.True(File.Exists(result.Path));
+ Assert.IsNotNull(result);
+ Assert.IsTrue(File.Exists(result.Path));
}
/// Verifies that a nonexistent assembly returns null.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_NonexistentAssembly_ReturnsNull()
{
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework("DoesNotExist.FakeAssembly", null);
- Assert.Null(result);
+ Assert.IsNull(result);
}
/// Verifies that targeting v10.0 returns a path containing "10.".
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_WithTargetFramework_MatchesVersion()
{
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework(
"System.Runtime", ".NETCoreApp,Version=v10.0");
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
Assert.Contains("10.", result.Path);
}
/// Verifies that NETCore.App preferred pack is probed first.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_PreferredPack_NETCoreApp_ProbesFirst()
{
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework(
"System.Runtime", null, "Microsoft.NETCore.App");
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
Assert.Contains("Microsoft.NETCore.App", result.Path);
- Assert.Equal("Microsoft.NETCore.App", result.RuntimePack);
+ Assert.AreEqual("Microsoft.NETCore.App", result.RuntimePack);
}
/// Verifies that WindowsDesktop.App preferred pack is probed first on Windows.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_PreferredPack_WindowsDesktopApp_ProbesFirst()
{
- Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
+ TestSkip.Unless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows),
"WindowsDesktop.App is only available on Windows");
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework(
"WindowsBase", null, "Microsoft.WindowsDesktop.App");
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
Assert.Contains("Microsoft.WindowsDesktop.App", result.Path);
- Assert.Equal("Microsoft.WindowsDesktop.App", result.RuntimePack);
+ Assert.AreEqual("Microsoft.WindowsDesktop.App", result.RuntimePack);
}
/// Verifies that AspNetCore.App preferred pack is probed first when available.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindAssemblyInSharedFramework_PreferredPack_AspNetCoreApp_ProbesFirst()
{
var result = DotNetRuntimeLocator.FindAssemblyInSharedFramework(
@@ -97,7 +106,7 @@ public void FindAssemblyInSharedFramework_PreferredPack_AspNetCoreApp_ProbesFirs
if (result is not null)
{
Assert.Contains("Microsoft.AspNetCore.App", result.Path);
- Assert.Equal("Microsoft.AspNetCore.App", result.RuntimePack);
+ Assert.AreEqual("Microsoft.AspNetCore.App", result.RuntimePack);
}
}
}
diff --git a/tests/Dotsider.Tests/Dotsider.Tests.csproj b/tests/Dotsider.Tests/Dotsider.Tests.csproj
index 8b9bf6d9..e2e7aa59 100644
--- a/tests/Dotsider.Tests/Dotsider.Tests.csproj
+++ b/tests/Dotsider.Tests/Dotsider.Tests.csproj
@@ -1,29 +1,19 @@
-
-
-
- net10.0
- enable
- enable
- true
- true
-
+
-
-
-
+
+ net10.0
+ enable
+ enable
+ true
+ true
+
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/tests/Dotsider.Tests/DotsiderHexRendererTests.cs b/tests/Dotsider.Tests/DotsiderHexRendererTests.cs
index a384d3e6..b7b23768 100644
--- a/tests/Dotsider.Tests/DotsiderHexRendererTests.cs
+++ b/tests/Dotsider.Tests/DotsiderHexRendererTests.cs
@@ -5,6 +5,7 @@ namespace Dotsider.Tests;
///
/// Tests for Dotsider Hex Renderer.
///
+[TestClass]
public class DotsiderHexRendererTests
{
// --- CalculateLayout tests ---
@@ -12,23 +13,25 @@ public class DotsiderHexRendererTests
///
/// Verifies calculate layout snaps to expected bytes per row.
///
- [Theory(Timeout = 30_000)]
- [InlineData(140, 32)] // Wide terminal: max snap point (4*32+11=139)
- [InlineData(120, 16)] // 120 cols: (120-11)/4=27, snaps to 16
- [InlineData(80, 16)] // Standard terminal
- [InlineData(50, 8)] // Narrow terminal
- [InlineData(20, 1)] // Very narrow: minimum snap
- [InlineData(11, 1)] // Minimum viable width (4*1+11=15, so 1 fits barely)
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow(140, 32)] // Wide terminal: max snap point (4*32+11=139)
+ [DataRow(120, 16)] // 120 cols: (120-11)/4=27, snaps to 16
+ [DataRow(80, 16)] // Standard terminal
+ [DataRow(50, 8)] // Narrow terminal
+ [DataRow(20, 1)] // Very narrow: minimum snap
+ [DataRow(11, 1)] // Minimum viable width (4*1+11=15, so 1 fits barely)
public void CalculateLayout_SnapsToExpectedBytesPerRow(int width, int expectedBytesPerRow)
{
var result = DotsiderHexRenderer.CalculateLayout(width);
- Assert.Equal(expectedBytesPerRow, result);
+ Assert.AreEqual(expectedBytesPerRow, result);
}
///
/// Verifies calculate layout always returns snap point.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void CalculateLayout_AlwaysReturnsSnapPoint()
{
int[] snaps = [1, 8, 16, 32];
@@ -42,24 +45,26 @@ public void CalculateLayout_AlwaysReturnsSnapPoint()
///
/// Verifies calculate layout never exceeds max32.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void CalculateLayout_NeverExceedsMax32()
{
var result = DotsiderHexRenderer.CalculateLayout(10_000);
- Assert.Equal(32, result);
+ Assert.AreEqual(32, result);
}
///
/// Verifies calculate layout monotonically increases.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void CalculateLayout_MonotonicallyIncreases()
{
var prev = DotsiderHexRenderer.CalculateLayout(1);
for (var width = 2; width <= 300; width++)
{
var current = DotsiderHexRenderer.CalculateLayout(width);
- Assert.True(current >= prev, $"Width {width}: {current} < {prev}");
+ Assert.IsGreaterThanOrEqualTo(prev, current, $"Width {width}: {current} < {prev}");
prev = current;
}
}
@@ -69,18 +74,20 @@ public void CalculateLayout_MonotonicallyIncreases()
///
/// Verifies get byte category fg ansi null byte returns null color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetByteCategoryFgAnsi_NullByte_ReturnsNullColor()
{
var result = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x00);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
Assert.Contains("\x1b[", result); // ANSI escape
}
///
/// Verifies get byte category fg ansi printable ascii returns printable color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetByteCategoryFgAnsi_PrintableAscii_ReturnsPrintableColor()
{
var letterA = DotsiderHexRenderer.GetByteCategoryFgAnsi((byte)'A');
@@ -88,55 +95,59 @@ public void GetByteCategoryFgAnsi_PrintableAscii_ReturnsPrintableColor()
var tilde = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x7E);
// All printable bytes should get the same color
- Assert.Equal(letterA, digit0);
- Assert.Equal(letterA, tilde);
+ Assert.AreEqual(letterA, digit0);
+ Assert.AreEqual(letterA, tilde);
}
///
/// Verifies get byte category fg ansi whitespace returns whitespace color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetByteCategoryFgAnsi_Whitespace_ReturnsWhitespaceColor()
{
var tab = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x09);
var lf = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x0A);
var cr = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x0D);
- Assert.Equal(tab, lf);
- Assert.Equal(tab, cr);
+ Assert.AreEqual(tab, lf);
+ Assert.AreEqual(tab, cr);
}
///
/// Verifies get byte category fg ansi control returns control color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetByteCategoryFgAnsi_Control_ReturnsControlColor()
{
var bel = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x07);
var esc = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x1B);
// Control chars (excluding whitespace) get the same color
- Assert.Equal(bel, esc);
+ Assert.AreEqual(bel, esc);
}
///
/// Verifies get byte category fg ansi high byte returns high byte color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetByteCategoryFgAnsi_HighByte_ReturnsHighByteColor()
{
var result = DotsiderHexRenderer.GetByteCategoryFgAnsi(0xFF);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
// Should differ from printable
var printable = DotsiderHexRenderer.GetByteCategoryFgAnsi((byte)'A');
- Assert.NotEqual(printable, result);
+ Assert.AreNotEqual(printable, result);
}
///
/// Verifies get byte category fg ansi all categories are distinct.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetByteCategoryFgAnsi_AllCategories_AreDistinct()
{
var nullColor = DotsiderHexRenderer.GetByteCategoryFgAnsi(0x00);
@@ -146,6 +157,6 @@ public void GetByteCategoryFgAnsi_AllCategories_AreDistinct()
var high = DotsiderHexRenderer.GetByteCategoryFgAnsi(0xFF);
var colors = new HashSet { nullColor, whitespace, control, printable, high };
- Assert.Equal(5, colors.Count);
+ Assert.HasCount(5, colors);
}
}
diff --git a/tests/Dotsider.Tests/DotsiderStateTests.cs b/tests/Dotsider.Tests/DotsiderStateTests.cs
index ab322d15..473fead2 100644
--- a/tests/Dotsider.Tests/DotsiderStateTests.cs
+++ b/tests/Dotsider.Tests/DotsiderStateTests.cs
@@ -7,9 +7,11 @@ namespace Dotsider.Tests;
///
/// Tests for Dotsider State.
///
-[Collection("SampleAssemblies")]
-public class DotsiderStateTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DotsiderStateTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _app;
@@ -31,298 +33,321 @@ private Hex1bApp CreateApp()
///
/// Verifies construct from hello world has correct file name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ConstructFromHelloWorld_HasCorrectFileName()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
}
///
/// Verifies has entry point true for exe.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HasEntryPoint_TrueForExe()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.True(state.HasEntryPoint);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.IsTrue(state.HasEntryPoint);
}
///
/// Verifies has entry point false for library.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HasEntryPoint_FalseForLibrary()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
- Assert.False(state.HasEntryPoint);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
+ Assert.IsFalse(state.HasEntryPoint);
}
///
/// Verifies has entry point true for complex app.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HasEntryPoint_TrueForComplexApp()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.ComplexAppDll);
- Assert.True(state.HasEntryPoint);
+ using var state = new DotsiderState(app, Samples.ComplexAppDll);
+ Assert.IsTrue(state.HasEntryPoint);
}
///
/// Verifies has entry point false for empty lib.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HasEntryPoint_FalseForEmptyLib()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.EmptyLibDll);
- Assert.False(state.HasEntryPoint);
+ using var state = new DotsiderState(app, Samples.EmptyLibDll);
+ Assert.IsFalse(state.HasEntryPoint);
}
///
/// Verifies has entry point false for native lib.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HasEntryPoint_FalseForNativeLib()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.NativeLibDll);
- Assert.False(state.HasEntryPoint);
+ using var state = new DotsiderState(app, Samples.NativeLibDll);
+ Assert.IsFalse(state.HasEntryPoint);
}
///
- /// Verifies is native aot false for all managed samples.
+ /// Verifies is native aot false for all managed Samples.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IsNativeAot_FalseForAllManagedSamples()
{
var app = CreateApp();
- string[] paths = [samples.HelloWorldDll, samples.RichLibraryDll, samples.ComplexAppDll,
- samples.MinimalApiDll, samples.NativeLibDll, samples.EmptyLibDll];
+ string[] paths = [Samples.HelloWorldDll, Samples.RichLibraryDll, Samples.ComplexAppDll,
+ Samples.MinimalApiDll, Samples.NativeLibDll, Samples.EmptyLibDll];
foreach (var path in paths)
{
using var state = new DotsiderState(app, path);
- Assert.False(state.IsNativeAot, $"IsNativeAot should be false for {Path.GetFileName(path)}");
+ Assert.IsFalse(state.IsNativeAot, $"IsNativeAot should be false for {Path.GetFileName(path)}");
}
}
///
/// Verifies push assembly changes analyzer.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_ChangesAnalyzer()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.True(state.PushAssembly(samples.RichLibraryDll));
- Assert.Equal("RichLibrary.dll", state.Analyzer.FileName);
- Assert.Single(state.NavigationStack);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.IsTrue(state.PushAssembly(Samples.RichLibraryDll));
+ Assert.AreEqual("RichLibrary.dll", state.Analyzer.FileName);
+ Assert.ContainsSingle(state.NavigationStack);
}
///
/// Verifies pop assembly restores previous.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PopAssembly_RestoresPrevious()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.True(state.PushAssembly(samples.RichLibraryDll));
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.IsTrue(state.PushAssembly(Samples.RichLibraryDll));
state.PopAssembly();
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.Empty(state.NavigationStack);
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.IsEmpty(state.NavigationStack);
}
///
/// Verifies push assembly invalid path returns false and sets error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_InvalidPath_ReturnsFalseAndSetsError()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.False(state.PushAssembly("/nonexistent/fake.dll"));
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.Empty(state.NavigationStack);
- Assert.NotNull(state.NavigationError);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.IsFalse(state.PushAssembly("/nonexistent/fake.dll"));
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.IsNotNull(state.NavigationError);
}
///
/// Verifies push assembly depth limit returns false at max.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_DepthLimit_ReturnsFalseAtMax()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
// Push to the limit (alternating two assemblies)
for (var i = 0; i < DotsiderState.MaxNavigationDepth; i++)
{
- var path = i % 2 == 0 ? samples.RichLibraryDll : samples.EmptyLibDll;
- Assert.True(state.PushAssembly(path), $"Push {i + 1} should succeed");
+ var path = i % 2 == 0 ? Samples.RichLibraryDll : Samples.EmptyLibDll;
+ Assert.IsTrue(state.PushAssembly(path), $"Push {i + 1} should succeed");
}
// Next push should fail
- Assert.False(state.PushAssembly(samples.ComplexAppDll));
- Assert.Contains("depth limit", state.NavigationError);
+ Assert.IsFalse(state.PushAssembly(Samples.ComplexAppDll));
+ Assert.Contains("depth limit", state.NavigationError!);
}
///
/// Verifies push assembly success clears error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_SuccessClearsError()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
// Trigger an error first
state.PushAssembly("/nonexistent/fake.dll");
- Assert.NotNull(state.NavigationError);
+ Assert.IsNotNull(state.NavigationError);
// Successful push clears it
- Assert.True(state.PushAssembly(samples.RichLibraryDll));
- Assert.Null(state.NavigationError);
+ Assert.IsTrue(state.PushAssembly(Samples.RichLibraryDll));
+ Assert.IsNull(state.NavigationError);
}
///
/// Verifies pop assembly clears navigation error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PopAssembly_ClearsNavigationError()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.True(state.PushAssembly(samples.RichLibraryDll));
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.IsTrue(state.PushAssembly(Samples.RichLibraryDll));
// Set an error, then pop
state.PushAssembly("/nonexistent/fake.dll");
- Assert.NotNull(state.NavigationError);
+ Assert.IsNotNull(state.NavigationError);
state.PopAssembly();
- Assert.Null(state.NavigationError);
+ Assert.IsNull(state.NavigationError);
}
///
/// Verifies push assembly bad image returns false and preserves state.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_BadImage_ReturnsFalseAndPreservesState()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.False(state.PushAssembly(samples.NonDotNetBinaryPath));
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.Empty(state.NavigationStack);
- Assert.Contains("Cannot open assembly", state.NavigationError);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.IsFalse(state.PushAssembly(Samples.NonDotNetBinaryPath));
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.Contains("Cannot open assembly", state.NavigationError!);
}
///
/// Verifies push assembly unauthorized access returns false.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_UnauthorizedAccess_ReturnsFalse()
{
// File.ReadAllBytes on a directory throws UnauthorizedAccessException on all platforms
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
- Assert.False(state.PushAssembly(Path.GetTempPath()));
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.Empty(state.NavigationStack);
- Assert.NotNull(state.NavigationError);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
+ Assert.IsFalse(state.PushAssembly(Path.GetTempPath()));
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.IsNotNull(state.NavigationError);
}
///
/// Verifies pop assembly empty stack is no op.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PopAssembly_EmptyStack_IsNoOp()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
state.PopAssembly();
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.Empty(state.NavigationStack);
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.IsEmpty(state.NavigationStack);
}
///
/// Verifies get active strings returns non empty.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetActiveStrings_ReturnsNonEmpty()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.StringsSourceTab = 1; // Metadata strings
var strings = state.GetActiveStrings();
- Assert.NotEmpty(strings);
+ Assert.IsNotEmpty(strings);
}
///
/// Verifies format size zero.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FormatSize_Zero()
{
- Assert.Equal("0 B", DotsiderState.FormatSize(0));
+ Assert.AreEqual("0 B", DotsiderState.FormatSize(0));
}
///
/// Verifies format size kb.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FormatSize_KB()
{
- Assert.Equal("1.0 KB", DotsiderState.FormatSize(1024));
+ Assert.AreEqual("1.0 KB", DotsiderState.FormatSize(1024));
}
///
/// Verifies format size mb.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FormatSize_MB()
{
- Assert.Equal("1.0 MB", DotsiderState.FormatSize(1048576));
+ Assert.AreEqual("1.0 MB", DotsiderState.FormatSize(1048576));
}
///
/// Verifies format size bytes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FormatSize_Bytes()
{
- Assert.Equal("500 B", DotsiderState.FormatSize(500));
+ Assert.AreEqual("500 B", DotsiderState.FormatSize(500));
}
///
/// Verifies construct from analyzer works.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ConstructFromAnalyzer_Works()
{
var app = CreateApp();
- var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
using var state = new DotsiderState(app, analyzer);
- Assert.Equal("RichLibrary.dll", state.Analyzer.FileName);
- Assert.NotNull(state.IlDisassembler);
- Assert.NotNull(state.StringExtractor);
+ Assert.AreEqual("RichLibrary.dll", state.Analyzer.FileName);
+ Assert.IsNotNull(state.IlDisassembler);
+ Assert.IsNotNull(state.StringExtractor);
}
///
/// Verifies all project types construct without error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllProjectTypes_ConstructWithoutError()
{
var app = CreateApp();
- string[] paths = [samples.HelloWorldDll, samples.RichLibraryDll, samples.ComplexAppDll,
- samples.MinimalApiDll, samples.NativeLibDll, samples.EmptyLibDll, samples.RichLibraryV2Dll];
+ string[] paths = [Samples.HelloWorldDll, Samples.RichLibraryDll, Samples.ComplexAppDll,
+ Samples.MinimalApiDll, Samples.NativeLibDll, Samples.EmptyLibDll, Samples.RichLibraryV2Dll];
foreach (var path in paths)
{
using var state = new DotsiderState(app, path);
- Assert.NotNull(state.Analyzer);
- Assert.NotNull(state.IlDisassembler);
- Assert.NotNull(state.StringExtractor);
+ Assert.IsNotNull(state.Analyzer);
+ Assert.IsNotNull(state.IlDisassembler);
+ Assert.IsNotNull(state.StringExtractor);
}
}
@@ -331,50 +356,54 @@ public void AllProjectTypes_ConstructWithoutError()
///
/// Verifies navigate to tab switches current tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToTab_SwitchesCurrentTab()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.General;
state.NavigateToTab(TabId.IlInspector);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
}
///
/// Verifies navigate to tab same tab no op.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToTab_SameTab_NoOp()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.HexDump;
state.NavigateToTab(TabId.HexDump);
- Assert.Equal(TabId.HexDump, state.CurrentTab);
+ Assert.AreEqual(TabId.HexDump, state.CurrentTab);
}
///
/// Verifies navigate to tab to il inspector switches tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToTab_ToIlInspector_SwitchesTab()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
state.NavigateToTab(TabId.IlInspector);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
}
///
/// Verifies navigate to tab il round trip preserves editor state.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToTab_IlRoundTrip_PreservesEditorState()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.General;
state.NavigateToTab(TabId.IlInspector);
@@ -386,8 +415,8 @@ public void NavigateToTab_IlRoundTrip_PreservesEditorState()
state.NavigateToTab(TabId.IlInspector);
// Editor state survives round-trip (Responsive preserves nodes)
- Assert.NotNull(state.IlEditorState);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.IsNotNull(state.IlEditorState);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
}
///
@@ -396,55 +425,58 @@ public void NavigateToTab_IlRoundTrip_PreservesEditorState()
/// IL tab-entry focus behavior — the table uses this key to deterministically
/// focus the correct row.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToIlMethod_SetsIlFocusedTreeKey()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// Initially null
- Assert.Null(state.IlFocusedTreeKey);
+ Assert.IsNull(state.IlFocusedTreeKey);
state.CurrentTab = TabId.PeMetadata;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
// Focused key must point to the jumped-to method row
- Assert.Equal($"method:{method.Token}", state.IlFocusedTreeKey);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual($"method:{method.Token}", state.IlFocusedTreeKey);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
}
///
/// Verifies navigate to il method sets state and switches tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToIlMethod_SetsStateAndSwitchesTab()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
state.PeSubTab = PeSubTabId.MethodDef;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal(method, state.IlSelectedMethod);
- Assert.NotNull(state.CrossViewBackTarget);
- Assert.Equal(TabId.PeMetadata, state.CrossViewBackTarget!.Value.Tab);
- Assert.Equal(PeSubTabId.MethodDef, state.CrossViewBackTarget!.Value.SubTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(method, state.IlSelectedMethod);
+ Assert.IsNotNull(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.PeMetadata, state.CrossViewBackTarget!.Value.Tab);
+ Assert.AreEqual(PeSubTabId.MethodDef, state.CrossViewBackTarget!.Value.SubTab);
// Focused tree key must point to the jumped-to method row
- Assert.Equal($"method:{method.Token}", state.IlFocusedTreeKey);
+ Assert.AreEqual($"method:{method.Token}", state.IlFocusedTreeKey);
}
///
/// Verifies navigate to il method expands tree nodes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToIlMethod_ExpandsTreeNodes()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
@@ -452,18 +484,19 @@ public void NavigateToIlMethod_ExpandsTreeNodes()
var typeDef = state.Analyzer.TypeDefs.First(t => t.FullName == method.DeclaringType);
var ns = string.IsNullOrEmpty(typeDef.Namespace) ? "(global)" : typeDef.Namespace;
- Assert.True(state.IlTreeExpansionState[$"ns:{ns}"]);
- Assert.True(state.IlTreeExpansionState[$"type:{method.DeclaringType}"]);
+ Assert.IsTrue(state.IlTreeExpansionState[$"ns:{ns}"]);
+ Assert.IsTrue(state.IlTreeExpansionState[$"type:{method.DeclaringType}"]);
}
///
/// Verifies navigate to il method clears stale il search.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToIlMethod_ClearsStaleIlSearch()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
var ilSearch = state.Search[TabId.IlInspector];
@@ -475,84 +508,89 @@ public void NavigateToIlMethod_ClearsStaleIlSearch()
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.False(ilSearch.IsActive);
- Assert.False(ilSearch.IsConfirmed);
- Assert.Null(ilSearch.Query);
- Assert.Equal(-1, ilSearch.MatchCount);
+ Assert.IsFalse(ilSearch.IsActive);
+ Assert.IsFalse(ilSearch.IsConfirmed);
+ Assert.IsNull(ilSearch.Query);
+ Assert.AreEqual(-1, ilSearch.MatchCount);
}
///
/// Verifies rva to file offset returns correct offset.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RvaToFileOffset_ReturnsCorrectOffset()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// Find a method with an RVA that falls within a section
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
var offset = state.RvaToFileOffset(method.Rva);
- Assert.True(offset >= 0, "RVA should resolve to a valid file offset");
+ Assert.IsGreaterThanOrEqualTo(0, offset, "RVA should resolve to a valid file offset");
// Verify the offset falls within the raw data range of some section
var foundSection = state.Analyzer.Sections.Any(s =>
offset >= s.RawDataOffset && offset < s.RawDataOffset + s.RawDataSize);
- Assert.True(foundSection, "File offset should be within a section's raw data");
+ Assert.IsTrue(foundSection, "File offset should be within a section's raw data");
}
///
/// Verifies rva to file offset invalid rva returns negative.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RvaToFileOffset_InvalidRva_ReturnsNegative()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
- Assert.Equal(-1, state.RvaToFileOffset(0x7FFFFFFF));
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
+ Assert.AreEqual(-1, state.RvaToFileOffset(0x7FFFFFFF));
}
///
/// Verifies navigate to hex offset sets state and switches tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToHexOffset_SetsStateAndSwitchesTab()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToHexOffset(method.Rva);
- Assert.Equal(TabId.HexDump, state.CurrentTab);
- Assert.NotNull(state.HexScrollTarget);
- Assert.NotNull(state.CrossViewBackTarget);
- Assert.Equal(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
+ Assert.AreEqual(TabId.HexDump, state.CurrentTab);
+ Assert.IsNotNull(state.HexScrollTarget);
+ Assert.IsNotNull(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
}
///
/// Verifies navigate to hex offset sets cursor position.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToHexOffset_SetsCursorPosition()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
var expectedOffset = state.RvaToFileOffset(method.Rva);
state.NavigateToHexOffset(method.Rva);
- Assert.Equal((int)expectedOffset, state.HexEditorState.ByteCursorOffset);
- Assert.Equal(expectedOffset, state.HexScrollTarget);
+ Assert.AreEqual((int)expectedOffset, state.HexEditorState.ByteCursorOffset);
+ Assert.AreEqual(expectedOffset, state.HexScrollTarget);
}
///
/// Verifies a raw Wasm function can jump to its file-backed bytes in the Hex Dump.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToHexFileOffset_WasmFunction_SetsCursorPosition()
{
var wasmPath = GetWasmNativePath();
@@ -564,132 +602,138 @@ public void NavigateToHexFileOffset_WasmFunction_SetsCursorPosition()
state.IlSelectedNativeSymbol = symbol;
state.NavigateToHexFileOffset(symbol.FileOffset!.Value);
- Assert.Equal(TabId.HexDump, state.CurrentTab);
- Assert.Equal((int)symbol.FileOffset.Value, state.HexEditorState.ByteCursorOffset);
- Assert.Equal(symbol.FileOffset.Value, state.HexScrollTarget);
- Assert.NotNull(state.CrossViewBackTarget);
- Assert.Equal(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
+ Assert.AreEqual(TabId.HexDump, state.CurrentTab);
+ Assert.AreEqual((int)symbol.FileOffset.Value, state.HexEditorState.ByteCursorOffset);
+ Assert.AreEqual(symbol.FileOffset.Value, state.HexScrollTarget);
+ Assert.IsNotNull(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
}
///
/// Verifies navigate to hex offset invalid rva no tab switch.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToHexOffset_InvalidRva_NoTabSwitch()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
state.NavigateToHexOffset(0x7FFFFFFF);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
/// Verifies navigate back restores previous tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateBack_RestoresPreviousTab()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
state.PeSubTab = PeSubTabId.TypeDef;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
state.NavigateBack();
- Assert.Equal(TabId.PeMetadata, state.CurrentTab);
- Assert.Equal(PeSubTabId.TypeDef, state.PeSubTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.PeMetadata, state.CurrentTab);
+ Assert.AreEqual(PeSubTabId.TypeDef, state.PeSubTab);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
/// Verifies navigate back no target no op.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateBack_NoTarget_NoOp()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
state.NavigateBack();
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
}
///
/// Verifies push assembly clears cross view back target.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_ClearsCrossViewBackTarget()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
// Create a cross-view back target
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.NotNull(state.CrossViewBackTarget);
+ Assert.IsNotNull(state.CrossViewBackTarget);
// Push a new assembly — should clear the stale back target
- state.PushAssembly(samples.HelloWorldDll);
- Assert.Null(state.CrossViewBackTarget);
+ state.PushAssembly(Samples.HelloWorldDll);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
/// Verifies pop assembly clears cross view back target.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PopAssembly_ClearsCrossViewBackTarget()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// Push first, then create back target
- state.PushAssembly(samples.HelloWorldDll);
+ state.PushAssembly(Samples.HelloWorldDll);
state.CurrentTab = TabId.PeMetadata;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.NotNull(state.CrossViewBackTarget);
+ Assert.IsNotNull(state.CrossViewBackTarget);
// Pop — should clear back target
state.PopAssembly();
- Assert.Null(state.CrossViewBackTarget);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
/// Verifies navigate to il method then hex then back restores il.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToIlMethod_ThenHex_ThenBack_RestoresIl()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
state.PeSubTab = PeSubTabId.MethodDef;
// PE → IL Inspector
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
// IL Inspector → Hex Dump
state.NavigateToHexOffset(method.Rva);
- Assert.Equal(TabId.HexDump, state.CurrentTab);
+ Assert.AreEqual(TabId.HexDump, state.CurrentTab);
// Back target should be IL Inspector (most recent navigation)
- Assert.Equal(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
+ Assert.AreEqual(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
// Back → IL Inspector
state.NavigateBack();
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
// The PE Metadata frame underneath must remain reachable via a second
// Esc — chained cross-view jumps unwind one frame at a time, with the
// exact origin sub-tab preserved.
- Assert.Equal((TabId.PeMetadata, PeSubTabId.MethodDef), state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.PeMetadata, PeSubTabId.MethodDef), state.CrossViewBackTarget);
}
// --- Apphost Detection ---
@@ -697,107 +741,114 @@ public void NavigateToIlMethod_ThenHex_ThenBack_RestoresIl()
///
/// Verifies construct from apphost exe sets apphost dialog state.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ConstructFromApphostExe_SetsApphostDialogState()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldExe);
+ using var state = new DotsiderState(app, Samples.HelloWorldExe);
- Assert.True(state.ApphostDialogOpen);
- Assert.NotNull(state.ApphostCompanionDllPath);
+ Assert.IsTrue(state.ApphostDialogOpen);
+ Assert.IsNotNull(state.ApphostCompanionDllPath);
Assert.EndsWith(".dll", state.ApphostCompanionDllPath!, StringComparison.OrdinalIgnoreCase);
}
///
/// Verifies construct from managed dll no apphost dialog.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ConstructFromManagedDll_NoApphostDialog()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
- Assert.False(state.ApphostDialogOpen);
- Assert.Null(state.ApphostCompanionDllPath);
+ Assert.IsFalse(state.ApphostDialogOpen);
+ Assert.IsNull(state.ApphostCompanionDllPath);
}
///
/// Verifies push assembly from apphost to companion dll works.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void PushAssembly_FromApphostToCompanionDll_Works()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldExe);
- Assert.False(state.Analyzer.HasMetadata);
+ using var state = new DotsiderState(app, Samples.HelloWorldExe);
+ Assert.IsFalse(state.Analyzer.HasMetadata);
- Assert.True(state.PushAssembly(state.ApphostCompanionDllPath!));
+ Assert.IsTrue(state.PushAssembly(state.ApphostCompanionDllPath!));
- Assert.True(state.Analyzer.HasMetadata);
- Assert.Equal("HelloWorld.dll", state.Analyzer.FileName);
- Assert.Single(state.NavigationStack);
+ Assert.IsTrue(state.Analyzer.HasMetadata);
+ Assert.AreEqual("HelloWorld.dll", state.Analyzer.FileName);
+ Assert.ContainsSingle(state.NavigationStack);
}
///
/// Verifies tab 3 keeps its managed label for ordinary IL inspection.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IlInspectorTabLabel_ManagedAssembly_IsIlInspector()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.HelloWorldDll);
+ using var state = new DotsiderState(app, Samples.HelloWorldDll);
- Assert.Equal(IlInspectorTabLabel.IlInspector, IlInspectorTabLabel.For(state));
+ Assert.AreEqual(IlInspectorTabLabel.IlInspector, IlInspectorTabLabel.For(state));
}
///
/// Verifies tab 3 names the native-only AOT surface as disassembly.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IlInspectorTabLabel_NativeAotWithoutAttachment_IsDisassembly()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
var app = CreateApp();
- using var state = new DotsiderState(app, samples.NativeAotConsoleExe!);
+ using var state = new DotsiderState(app, Samples.NativeAotConsoleExe!);
- Assert.Equal(IlInspectorTabLabel.Disassembly, IlInspectorTabLabel.For(state));
+ Assert.AreEqual(IlInspectorTabLabel.Disassembly, IlInspectorTabLabel.For(state));
}
///
/// Verifies tab 3 names ReadyToRun as an IL plus native surface.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IlInspectorTabLabel_ReadyToRun_IsIlAndNative()
{
- Assert.SkipWhen(samples.ReadyToRunConsoleDll is null, "ReadyToRun crossgen2 publish did not run on this leg.");
+ TestSkip.When(Samples.ReadyToRunConsoleDll is null, "ReadyToRun crossgen2 publish did not run on this leg.");
var app = CreateApp();
- using var state = new DotsiderState(app, samples.ReadyToRunConsoleDll!);
+ using var state = new DotsiderState(app, Samples.ReadyToRunConsoleDll!);
- Assert.Equal(IlInspectorTabLabel.IlAndNative, IlInspectorTabLabel.For(state));
+ Assert.AreEqual(IlInspectorTabLabel.IlAndNative, IlInspectorTabLabel.For(state));
}
///
/// Verifies the pre-ILC sidecar toggle switches tab 3 between paired IL/native and native disassembly.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IlInspectorTabLabel_PreIlcAttachment_FollowsTreeToggle()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
- Assert.SkipWhen(samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleManagedDll is null, "pre-ILC companion was not produced");
var app = CreateApp();
- using var state = new DotsiderState(app, samples.NativeAotConsoleExe!);
+ using var state = new DotsiderState(app, Samples.NativeAotConsoleExe!);
- Assert.True(state.AttachPreIlc());
- Assert.Equal(IlInspectorTabLabel.IlAndNative, IlInspectorTabLabel.For(state));
+ Assert.IsTrue(state.AttachPreIlc());
+ Assert.AreEqual(IlInspectorTabLabel.IlAndNative, IlInspectorTabLabel.For(state));
state.IlAotTreeNativeView = true;
- Assert.Equal(IlInspectorTabLabel.Disassembly, IlInspectorTabLabel.For(state));
+ Assert.AreEqual(IlInspectorTabLabel.Disassembly, IlInspectorTabLabel.For(state));
}
///
@@ -805,17 +856,17 @@ public void IlInspectorTabLabel_PreIlcAttachment_FollowsTreeToggle()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_app?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
- GC.SuppressFinalize(this);
}
- private string GetWasmNativePath()
+ private static string GetWasmNativePath()
{
- Assert.SkipWhen(samples.WasmConsoleNativeWasm is null && samples.ReadyToRunConsoleWasmNativeWasm is null,
+ TestSkip.When(Samples.WasmConsoleNativeWasm is null && Samples.ReadyToRunConsoleWasmNativeWasm is null,
"browser-wasm sample publish did not produce dotnet.native.wasm on this leg.");
- return samples.WasmConsoleNativeWasm ?? samples.ReadyToRunConsoleWasmNativeWasm!;
+ return Samples.WasmConsoleNativeWasm ?? Samples.ReadyToRunConsoleWasmNativeWasm!;
}
}
diff --git a/tests/Dotsider.Tests/DotsiderThemeTests.cs b/tests/Dotsider.Tests/DotsiderThemeTests.cs
index 109dbacb..f83ab5b5 100644
--- a/tests/Dotsider.Tests/DotsiderThemeTests.cs
+++ b/tests/Dotsider.Tests/DotsiderThemeTests.cs
@@ -5,6 +5,7 @@ namespace Dotsider.Tests;
///
/// Tests for Dotsider Theme.
///
+[TestClass]
public class DotsiderThemeTests
{
///
@@ -14,14 +15,14 @@ public class DotsiderThemeTests
/// (transparent cells inherit; explicit cells don't), causing broken row highlighting
/// in the live demo's xterm.js terminal.
///
- [Fact]
+ [TestMethod]
public void Theme_DoesNotSetGlobalBackground()
{
var theme = DotsiderTheme.Create();
var bg = theme.GetGlobalBackground();
- Assert.True(bg.IsDefault, "GlobalTheme.BackgroundColor must not be set — " +
+ Assert.IsTrue(bg.IsDefault, "GlobalTheme.BackgroundColor must not be set — " +
"it breaks table row highlighting in xterm.js by preventing transparent " +
"background compositing. The terminal background is set by the terminal " +
"emulator itself (xterm.js theme / native terminal profile).");
diff --git a/tests/Dotsider.Tests/DwarfLineProgramTests.cs b/tests/Dotsider.Tests/DwarfLineProgramTests.cs
index 9d0d127b..ee0f33d7 100644
--- a/tests/Dotsider.Tests/DwarfLineProgramTests.cs
+++ b/tests/Dotsider.Tests/DwarfLineProgramTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
/// file-table shapes, the row state machine (special, standard, and extended opcodes), and the
/// decl-primary source attribution — driven with hand-built .debug_line blobs.
///
+[TestClass]
public class DwarfLineProgramTests
{
private static readonly byte[] StandardLengths = [0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1];
@@ -41,7 +42,8 @@ private static byte[] Program(
/// Verifies a v4 program joins directories into file names, decodes copy/advance rows, ends
/// coverage at the end-sequence row, and attributes decl-first with row fallback.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_V4_RowsAndDeclPrimaryAttribution()
{
var ops = new DwarfBlob()
@@ -58,25 +60,25 @@ public void Parse_V4_RowsAndDeclPrimaryAttribution()
var line = Program(4, ops, ["src"], [("main.cs", 1), ("/abs/other.cs", 1)]);
var program = DwarfLineProgram.Parse(Sections(line), 0);
- Assert.NotNull(program);
- Assert.Equal("src/main.cs", program.FileName(1));
- Assert.Equal("/abs/other.cs", program.FileName(2)); // rooted names are not joined
- Assert.Null(program.FileName(0)); // v4 tables are 1-based
- Assert.Null(program.FileName(3));
-
- Assert.True(program.TryFindLine(0x1000, out var file, out var lineNo));
- Assert.Equal("src/main.cs", file);
- Assert.Equal(10, lineNo);
- Assert.True(program.TryFindLine(0x101F, out _, out lineNo));
- Assert.Equal(10, lineNo);
- Assert.True(program.TryFindLine(0x102F, out _, out lineNo));
- Assert.Equal(15, lineNo);
- Assert.False(program.TryFindLine(0x1030, out _, out _)); // past the sequence
- Assert.False(program.TryFindLine(0x0FFF, out _, out _)); // before it
-
- Assert.Equal(("src/main.cs", 42), program.ResolveSource(1, 42, 0x1000)); // decl primary
- Assert.Equal(("src/main.cs", 15), program.ResolveSource(-1, 0, 0x1024)); // row fallback
- Assert.Equal(("src/main.cs", 10), program.ResolveSource(1, 0, 0x1000)); // mixed
+ Assert.IsNotNull(program);
+ Assert.AreEqual("src/main.cs", program.FileName(1));
+ Assert.AreEqual("/abs/other.cs", program.FileName(2)); // rooted names are not joined
+ Assert.IsNull(program.FileName(0)); // v4 tables are 1-based
+ Assert.IsNull(program.FileName(3));
+
+ Assert.IsTrue(program.TryFindLine(0x1000, out var file, out var lineNo));
+ Assert.AreEqual("src/main.cs", file);
+ Assert.AreEqual(10, lineNo);
+ Assert.IsTrue(program.TryFindLine(0x101F, out _, out lineNo));
+ Assert.AreEqual(10, lineNo);
+ Assert.IsTrue(program.TryFindLine(0x102F, out _, out lineNo));
+ Assert.AreEqual(15, lineNo);
+ Assert.IsFalse(program.TryFindLine(0x1030, out _, out _)); // past the sequence
+ Assert.IsFalse(program.TryFindLine(0x0FFF, out _, out _)); // before it
+
+ Assert.AreEqual(("src/main.cs", 42), program.ResolveSource(1, 42, 0x1000)); // decl primary
+ Assert.AreEqual(("src/main.cs", 15), program.ResolveSource(-1, 0, 0x1024)); // row fallback
+ Assert.AreEqual(("src/main.cs", 10), program.ResolveSource(1, 0, 0x1000)); // mixed
}
///
@@ -84,7 +86,8 @@ public void Parse_V4_RowsAndDeclPrimaryAttribution()
/// fixed_advance_pc, and that an unknown standard opcode is skipped by its declared
/// operand count.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_SpecialAndPcOpcodes_AdvanceExactly()
{
// opcode_base 14: op 13 is an unknown standard opcode declared with 2 operands.
@@ -103,18 +106,19 @@ public void Parse_SpecialAndPcOpcodes_AdvanceExactly()
lineBase: -3, lineRange: 6, opcodeBase: 14, standardLengths: lengths);
var program = DwarfLineProgram.Parse(Sections(line), 0);
- Assert.NotNull(program);
- Assert.True(program.TryFindLine(0x2008, out var file, out var lineNo));
- Assert.Equal("a.c", file); // directory index 0 resolves to the bare name
- Assert.Equal(3, lineNo);
- Assert.True(program.TryFindLine(0x203F, out _, out lineNo));
- Assert.Equal(3, lineNo);
- Assert.True(program.TryFindLine(0x2040, out _, out lineNo));
- Assert.Equal(4, lineNo);
+ Assert.IsNotNull(program);
+ Assert.IsTrue(program.TryFindLine(0x2008, out var file, out var lineNo));
+ Assert.AreEqual("a.c", file); // directory index 0 resolves to the bare name
+ Assert.AreEqual(3, lineNo);
+ Assert.IsTrue(program.TryFindLine(0x203F, out _, out lineNo));
+ Assert.AreEqual(3, lineNo);
+ Assert.IsTrue(program.TryFindLine(0x2040, out _, out lineNo));
+ Assert.AreEqual(4, lineNo);
}
/// Verifies a v2 header (no maximum-operations field) still parses and decodes rows.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_V2Header_DecodesRows()
{
var ops = new DwarfBlob()
@@ -125,16 +129,17 @@ public void Parse_V2Header_DecodesRows()
var program = DwarfLineProgram.Parse(Sections(Program(2, ops, [], [("f.c", 0)])), 0);
- Assert.NotNull(program);
- Assert.True(program.TryFindLine(0x100, out var file, out _));
- Assert.Equal("f.c", file);
+ Assert.IsNotNull(program);
+ Assert.IsTrue(program.TryFindLine(0x100, out var file, out _));
+ Assert.AreEqual("f.c", file);
}
///
/// Verifies the v5 form-described tables: directories via line_strp, files carrying
/// path, directory index, and skipped timestamp/MD5 columns, with 0-based numbering.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_V5Tables_ResolveFormsAndZeroBasedFiles()
{
var lineStr = new DwarfBlob().CStr("proj").CStr("src").ToArray(); // offsets 0 and 5
@@ -169,19 +174,20 @@ public void Parse_V5Tables_ResolveFormsAndZeroBasedFiles()
var program = DwarfLineProgram.Parse(Sections(line, lineStr: lineStr), 0);
- Assert.NotNull(program);
- Assert.Equal("proj/app.cs", program.FileName(0));
- Assert.Equal("src/util.cs", program.FileName(1));
- Assert.True(program.TryFindLine(0x9008, out var file, out var lineNo));
- Assert.Equal("src/util.cs", file);
- Assert.Equal(1, lineNo);
+ Assert.IsNotNull(program);
+ Assert.AreEqual("proj/app.cs", program.FileName(0));
+ Assert.AreEqual("src/util.cs", program.FileName(1));
+ Assert.IsTrue(program.TryFindLine(0x9008, out var file, out var lineNo));
+ Assert.AreEqual("src/util.cs", file);
+ Assert.AreEqual(1, lineNo);
}
///
/// Verifies DW_LNE_define_file appends to the v4 file table mid-program and rows can
/// attribute to the added file.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_DefineFile_AppendsToTable()
{
var define = new DwarfBlob().CStr("gen.c").ULeb(1).ULeb(0).ULeb(0).ToArray();
@@ -195,14 +201,15 @@ public void Parse_DefineFile_AppendsToTable()
var program = DwarfLineProgram.Parse(Sections(Program(4, ops, ["inc"], [("main.c", 0)])), 0);
- Assert.NotNull(program);
- Assert.Equal("inc/gen.c", program.FileName(2));
- Assert.True(program.TryFindLine(0x100, out var file, out _));
- Assert.Equal("inc/gen.c", file);
+ Assert.IsNotNull(program);
+ Assert.AreEqual("inc/gen.c", program.FileName(2));
+ Assert.IsTrue(program.TryFindLine(0x100, out var file, out _));
+ Assert.AreEqual("inc/gen.c", file);
}
/// Verifies a DWARF64 header (escaped length, 8-byte header length) parses.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_Dwarf64Header_DecodesRows()
{
var tail = new DwarfBlob().U8(1).U8(1).U8(1)
@@ -221,27 +228,28 @@ public void Parse_Dwarf64Header_DecodesRows()
var program = DwarfLineProgram.Parse(Sections(line), 0);
- Assert.NotNull(program);
- Assert.True(program.TryFindLine(0x100, out var file, out _));
- Assert.Equal("f.c", file);
+ Assert.IsNotNull(program);
+ Assert.IsTrue(program.TryFindLine(0x100, out var file, out _));
+ Assert.AreEqual("f.c", file);
}
///
/// Verifies malformed inputs: out-of-range offsets and bad headers yield no program, and a
/// program whose body breaks mid-opcode keeps the rows decoded before the damage.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Parse_Malformed_FailsClosedOrKeepsPartialRows()
{
- Assert.Null(DwarfLineProgram.Parse(Sections([1, 2, 3]), 0x100));
+ Assert.IsNull(DwarfLineProgram.Parse(Sections([1, 2, 3]), 0x100));
var badVersion = Program(4, [], [], []);
badVersion[4] = 9; // version u16 low byte
- Assert.Null(DwarfLineProgram.Parse(Sections(badVersion), 0));
+ Assert.IsNull(DwarfLineProgram.Parse(Sections(badVersion), 0));
var zeroRange = Program(4, [], [], []);
zeroRange[14] = 0; // line_range: length(4) + version(2) + header_length(4) + 4 machine bytes
- Assert.Null(DwarfLineProgram.Parse(Sections(zeroRange), 0));
+ Assert.IsNull(DwarfLineProgram.Parse(Sections(zeroRange), 0));
// A row, then an extended op whose declared length overruns the unit: row survives.
var ops = new DwarfBlob()
@@ -250,13 +258,13 @@ public void Parse_Malformed_FailsClosedOrKeepsPartialRows()
.U8(0).ULeb(200)
.ToArray();
var program = DwarfLineProgram.Parse(Sections(Program(4, ops, [], [("k.c", 0)])), 0);
- Assert.NotNull(program);
- Assert.True(program.TryFindLine(0x500, out var file, out _));
- Assert.Equal("k.c", file);
+ Assert.IsNotNull(program);
+ Assert.IsTrue(program.TryFindLine(0x500, out var file, out _));
+ Assert.AreEqual("k.c", file);
// An empty program parses but covers nothing.
var empty = DwarfLineProgram.Parse(Sections(Program(4, [], [], [("e.c", 0)])), 0);
- Assert.NotNull(empty);
- Assert.False(empty.TryFindLine(0, out _, out _));
+ Assert.IsNotNull(empty);
+ Assert.IsFalse(empty.TryFindLine(0, out _, out _));
}
}
diff --git a/tests/Dotsider.Tests/DwarfRangeListsTests.cs b/tests/Dotsider.Tests/DwarfRangeListsTests.cs
index f9038651..cbb8ac4d 100644
--- a/tests/Dotsider.Tests/DwarfRangeListsTests.cs
+++ b/tests/Dotsider.Tests/DwarfRangeListsTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
/// .debug_rnglists RLE walk — driven with hand-built section blobs covering every entry
/// opcode, the base-address escapes, and the rnglistx offset-array indirection.
///
+[TestClass]
public class DwarfRangeListsTests
{
private static DwarfReader.UnitContext Unit(
@@ -30,7 +31,8 @@ private static byte[] AddrTable(params ulong[] entries)
/// Verifies the v4 walk sums begin/end pairs against the CU base, honors the (-1, base)
/// base-address escape, and stops at the (0, 0) terminator.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryResolve_V4Pairs_SumsAndRebases()
{
var ranges = new DwarfBlob()
@@ -43,13 +45,14 @@ public void TryResolve_V4Pairs_SumsAndRebases()
var ok = DwarfRangeLists.TryResolve(Sections(ranges: ranges), 0, isRnglistx: false,
Unit(4, baseAddress: 0x1000), out var start, out var size);
- Assert.True(ok);
- Assert.Equal(0x1010UL, start);
- Assert.Equal(0x50UL, size);
+ Assert.IsTrue(ok);
+ Assert.AreEqual(0x1010UL, start);
+ Assert.AreEqual(0x50UL, size);
}
/// Verifies the v4 walk uses 4-byte entries and the 32-bit escape for 32-bit units.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryResolve_V4FourByteAddresses_UsesNarrowEscape()
{
var ranges = new DwarfBlob()
@@ -62,16 +65,17 @@ public void TryResolve_V4FourByteAddresses_UsesNarrowEscape()
var ok = DwarfRangeLists.TryResolve(Sections(ranges: ranges), 0, isRnglistx: false,
Unit(4, addressSize: 4, baseAddress: 0x100), out var start, out var size);
- Assert.True(ok);
- Assert.Equal(0x110UL, start);
- Assert.Equal(0x18UL, size);
+ Assert.IsTrue(ok);
+ Assert.AreEqual(0x110UL, start);
+ Assert.AreEqual(0x18UL, size);
}
///
/// Verifies the v5 walk decodes every RLE opcode: base switches (direct and via
/// .debug_addr), offset pairs, start/end, start/length, and the startx forms.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryResolve_V5AllOpcodes_SumsCoveredBytes()
{
var list = new DwarfBlob()
@@ -90,16 +94,17 @@ public void TryResolve_V5AllOpcodes_SumsCoveredBytes()
Sections(rngLists: list, addr: AddrTable(0x7000, 0x8000)),
0, isRnglistx: false, Unit(5), out var start, out var size);
- Assert.True(ok);
- Assert.Equal(0x5010UL, start);
- Assert.Equal(0x20UL + 0x25 + 8 + 0x1000 + 4 + 0x10, size);
+ Assert.IsTrue(ok);
+ Assert.AreEqual(0x5010UL, start);
+ Assert.AreEqual(0x20UL + 0x25 + 8 + 0x1000 + 4 + 0x10, size);
}
///
/// Verifies a rnglistx index resolves through the CU's offset array — each entry an
/// offset from the array base to its list — before walking the list.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryResolve_Rnglistx_ResolvesThroughOffsetArray()
{
var list0 = new DwarfBlob().U8(0x07).U64(0x111).ULeb(1).U8(0x00).ToArray();
@@ -113,51 +118,52 @@ public void TryResolve_Rnglistx_ResolvesThroughOffsetArray()
var ok = DwarfRangeLists.TryResolve(Sections(rngLists: section), 1, isRnglistx: true,
Unit(5), out var start, out var size);
- Assert.True(ok);
- Assert.Equal(0x2000UL, start);
- Assert.Equal(0x30UL, size);
+ Assert.IsTrue(ok);
+ Assert.AreEqual(0x2000UL, start);
+ Assert.AreEqual(0x30UL, size);
- Assert.True(DwarfRangeLists.TryResolve(Sections(rngLists: section), 0, isRnglistx: true,
+ Assert.IsTrue(DwarfRangeLists.TryResolve(Sections(rngLists: section), 0, isRnglistx: true,
Unit(5), out start, out size));
- Assert.Equal(0x111UL, start);
- Assert.Equal(1UL, size);
+ Assert.AreEqual(0x111UL, start);
+ Assert.AreEqual(1UL, size);
}
///
/// Verifies malformed inputs fail closed: out-of-range offsets, unknown opcodes, a
/// startx without .debug_addr, and lists that cover nothing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryResolve_Malformed_ReturnsFalse()
{
// Offset beyond the section.
- Assert.False(DwarfRangeLists.TryResolve(Sections(ranges: [1, 2]), 0x100, false,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(ranges: [1, 2]), 0x100, false,
Unit(4), out _, out _));
// Unknown v5 opcode.
- Assert.False(DwarfRangeLists.TryResolve(Sections(rngLists: [0xAA]), 0, false,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(rngLists: [0xAA]), 0, false,
Unit(5), out _, out _));
// startx with no .debug_addr to resolve against.
var startx = new DwarfBlob().U8(0x03).ULeb(0).ULeb(8).U8(0x00).ToArray();
- Assert.False(DwarfRangeLists.TryResolve(Sections(rngLists: startx), 0, false,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(rngLists: startx), 0, false,
Unit(5), out _, out _));
// Immediate terminator: nothing covered.
- Assert.False(DwarfRangeLists.TryResolve(Sections(rngLists: [0x00]), 0, false,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(rngLists: [0x00]), 0, false,
Unit(5), out _, out _));
var emptyV4 = new DwarfBlob().U64(0).U64(0).ToArray();
- Assert.False(DwarfRangeLists.TryResolve(Sections(ranges: emptyV4), 0, false,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(ranges: emptyV4), 0, false,
Unit(4), out _, out _));
// rnglistx index beyond the offset array.
var tiny = new DwarfBlob().U32(0).U16(5).U8(8).U8(0).U32(0).ToArray();
- Assert.False(DwarfRangeLists.TryResolve(Sections(rngLists: tiny), 5, true,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(rngLists: tiny), 5, true,
Unit(5), out _, out _));
// Truncated v4 list (no terminator) keeps nothing rather than throwing.
var truncated = new DwarfBlob().U64(0x10).U64(0x20).ToArray();
- Assert.False(DwarfRangeLists.TryResolve(Sections(ranges: truncated), 0, false,
+ Assert.IsFalse(DwarfRangeLists.TryResolve(Sections(ranges: truncated), 0, false,
Unit(4), out _, out _));
}
}
diff --git a/tests/Dotsider.Tests/DwarfReaderTests.cs b/tests/Dotsider.Tests/DwarfReaderTests.cs
index 54e76113..520e3343 100644
--- a/tests/Dotsider.Tests/DwarfReaderTests.cs
+++ b/tests/Dotsider.Tests/DwarfReaderTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
/// hand-built .debug_info/.debug_abbrev blobs so every form the decoder claims
/// (string indirection, indexed addresses, DWARF64, references, skips) is pinned byte-for-byte.
///
+[TestClass]
public class DwarfReaderTests
{
/// Writes one abbreviation declaration (code, tag, children, attribute/form pairs).
@@ -68,7 +69,8 @@ private static byte[] AddrTable(params ulong[] entries)
/// high_pc, and decl file/line, and that the unit context captures the CU's base
/// address and line-program offset.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_V4_ReadsNameAddressSizeAndDeclInfo()
{
var abbrev = new DwarfBlob();
@@ -87,16 +89,16 @@ public void ReadFunctions_V4_ReadsNameAddressSizeAndDeclInfo()
var result = DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray()));
- var (function, unit) = Assert.Single(result);
- Assert.Equal("main", function.Name);
- Assert.Equal(0x401000UL, function.LowPc);
- Assert.Equal(0x40UL, function.Size);
- Assert.Equal(3, function.DeclFile);
- Assert.Equal(42, function.DeclLine);
- Assert.Equal(0x77, function.StmtListOffset);
- Assert.Equal(4, unit.Version);
- Assert.Equal(0x400000UL, unit.BaseAddress);
- Assert.Equal(0x77, unit.StmtListOffset);
+ var (function, unit) = Assert.ContainsSingle(result);
+ Assert.AreEqual("main", function.Name);
+ Assert.AreEqual(0x401000UL, function.LowPc);
+ Assert.AreEqual(0x40UL, function.Size);
+ Assert.AreEqual(3, function.DeclFile);
+ Assert.AreEqual(42, function.DeclLine);
+ Assert.AreEqual(0x77, function.StmtListOffset);
+ Assert.AreEqual(4, unit.Version);
+ Assert.AreEqual(0x400000UL, unit.BaseAddress);
+ Assert.AreEqual(0x77, unit.StmtListOffset);
}
///
@@ -104,7 +106,8 @@ public void ReadFunctions_V4_ReadsNameAddressSizeAndDeclInfo()
/// when resolving strx1/addrx1, and takes offset-class high_pc as the
/// size directly.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_V5_ExplicitBasesAndOffsetHighPc()
{
var abbrev = new DwarfBlob();
@@ -126,25 +129,26 @@ public void ReadFunctions_V5_ExplicitBasesAndOffsetHighPc()
strOffsets: StrOffsetsTable(999, 1),
addr: AddrTable(0xDEAD, 0x2000));
- var (function, unit) = Assert.Single(DwarfReader.ReadFunctions(sections));
- Assert.Equal("EntryPoint", function.Name);
- Assert.Equal(0x2000UL, function.LowPc);
- Assert.Equal(0x30UL, function.Size);
- Assert.Equal(5, unit.Version);
+ var (function, unit) = Assert.ContainsSingle(DwarfReader.ReadFunctions(sections));
+ Assert.AreEqual("EntryPoint", function.Name);
+ Assert.AreEqual(0x2000UL, function.LowPc);
+ Assert.AreEqual(0x30UL, function.Size);
+ Assert.AreEqual(5, unit.Version);
}
///
/// Verifies every string form resolves the name: strp, line_strp, and the five
/// strx encodings through the default v5 .debug_str_offsets base.
///
- [Theory(Timeout = 30_000)]
- [InlineData(DwarfForm.Strp)]
- [InlineData(DwarfForm.LineStrp)]
- [InlineData(DwarfForm.Strx)]
- [InlineData(DwarfForm.Strx1)]
- [InlineData(DwarfForm.Strx2)]
- [InlineData(DwarfForm.Strx3)]
- [InlineData(DwarfForm.Strx4)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow(DwarfForm.Strp)]
+ [DataRow(DwarfForm.LineStrp)]
+ [DataRow(DwarfForm.Strx)]
+ [DataRow(DwarfForm.Strx1)]
+ [DataRow(DwarfForm.Strx2)]
+ [DataRow(DwarfForm.Strx3)]
+ [DataRow(DwarfForm.Strx4)]
public void ReadFunctions_StringForms_ResolveName(ulong form)
{
var abbrev = new DwarfBlob();
@@ -170,21 +174,22 @@ public void ReadFunctions_StringForms_ResolveName(ulong form)
var sections = Sections(Cu(5, dies.ToArray()), abbrev.ToArray(),
str: strings, lineStr: strings, strOffsets: StrOffsetsTable(0, 1));
- var (function, _) = Assert.Single(DwarfReader.ReadFunctions(sections));
- Assert.Equal("fn", function.Name);
- Assert.Equal(0x1000UL, function.LowPc);
+ var (function, _) = Assert.ContainsSingle(DwarfReader.ReadFunctions(sections));
+ Assert.AreEqual("fn", function.Name);
+ Assert.AreEqual(0x1000UL, function.LowPc);
}
///
/// Verifies every indexed address form resolves low_pc through the default v5
/// .debug_addr base.
///
- [Theory(Timeout = 30_000)]
- [InlineData(DwarfForm.Addrx)]
- [InlineData(DwarfForm.Addrx1)]
- [InlineData(DwarfForm.Addrx2)]
- [InlineData(DwarfForm.Addrx3)]
- [InlineData(DwarfForm.Addrx4)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow(DwarfForm.Addrx)]
+ [DataRow(DwarfForm.Addrx1)]
+ [DataRow(DwarfForm.Addrx2)]
+ [DataRow(DwarfForm.Addrx3)]
+ [DataRow(DwarfForm.Addrx4)]
public void ReadFunctions_AddressIndexForms_ResolveLowPc(ulong form)
{
var abbrev = new DwarfBlob();
@@ -207,15 +212,16 @@ public void ReadFunctions_AddressIndexForms_ResolveLowPc(ulong form)
var sections = Sections(Cu(5, dies.ToArray()), abbrev.ToArray(), addr: AddrTable(0, 0x4000));
- var (function, _) = Assert.Single(DwarfReader.ReadFunctions(sections));
- Assert.Equal(0x4000UL, function.LowPc);
+ var (function, _) = Assert.ContainsSingle(DwarfReader.ReadFunctions(sections));
+ Assert.AreEqual(0x4000UL, function.LowPc);
}
///
/// Verifies a DWARF64 unit parses: 0xFFFFFFFF-escaped length, 8-byte abbrev offset, and
/// 8-byte strp section offsets.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_Dwarf64_ParsesUnitAndWideOffsets()
{
var abbrev = new DwarfBlob();
@@ -233,10 +239,10 @@ public void ReadFunctions_Dwarf64_ParsesUnitAndWideOffsets()
var sections = Sections(Cu(4, dies.ToArray(), is64: true), abbrev.ToArray(),
str: new DwarfBlob().U8(0).CStr("fn").ToArray());
- var (function, unit) = Assert.Single(DwarfReader.ReadFunctions(sections));
- Assert.True(unit.Is64);
- Assert.Equal("fn", function.Name);
- Assert.Equal(0x20UL, function.Size);
+ var (function, unit) = Assert.ContainsSingle(DwarfReader.ReadFunctions(sections));
+ Assert.IsTrue(unit.Is64);
+ Assert.AreEqual("fn", function.Name);
+ Assert.AreEqual(0x20UL, function.Size);
}
///
@@ -271,35 +277,38 @@ private static DwarfSections SpecificationPair(ulong refAttribute, ulong refForm
/// Verifies a nameless definition resolves its name through a unit-relative
/// specification reference, preferring the declaration's linkage name.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_SpecificationRef4_ResolvesUnitRelativeName()
{
var sections = SpecificationPair(DwarfForm.AtSpecification, DwarfForm.Ref4, refValue: 12);
- var (function, _) = Assert.Single(DwarfReader.ReadFunctions(sections));
- Assert.Equal("_ZN6Widget3RunEv", function.Name);
- Assert.Equal(0x5000UL, function.LowPc);
- Assert.Equal(0x10UL, function.Size);
+ var (function, _) = Assert.ContainsSingle(DwarfReader.ReadFunctions(sections));
+ Assert.AreEqual("_ZN6Widget3RunEv", function.Name);
+ Assert.AreEqual(0x5000UL, function.LowPc);
+ Assert.AreEqual(0x10UL, function.Size);
}
///
/// Verifies abstract_origin in the ref_addr form resolves as a
/// section-absolute offset, not unit-relative.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_AbstractOriginRefAddr_ResolvesSectionAbsoluteName()
{
var sections = SpecificationPair(DwarfForm.AtAbstractOrigin, DwarfForm.RefAddr, refValue: 24);
- var (function, _) = Assert.Single(DwarfReader.ReadFunctions(sections));
- Assert.Equal("_ZN6Widget3RunEv", function.Name);
+ var (function, _) = Assert.ContainsSingle(DwarfReader.ReadFunctions(sections));
+ Assert.AreEqual("_ZN6Widget3RunEv", function.Name);
}
///
/// Verifies a v4 range-based subprogram (no low_pc) is kept with its
/// .debug_ranges offset recorded as a section offset.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_V4Ranges_RecordsSectionOffset()
{
var abbrev = new DwarfBlob();
@@ -315,18 +324,19 @@ public void ReadFunctions_V4Ranges_RecordsSectionOffset()
var result = DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray()));
- var (function, unit) = Assert.Single(result);
- Assert.Equal("ranged", function.Name);
- Assert.Equal(0x40, function.RangesOffset);
- Assert.False(function.RangesIsRnglistx);
- Assert.Equal(0x400000UL, unit.BaseAddress);
+ var (function, unit) = Assert.ContainsSingle(result);
+ Assert.AreEqual("ranged", function.Name);
+ Assert.AreEqual(0x40, function.RangesOffset);
+ Assert.IsFalse(function.RangesIsRnglistx);
+ Assert.AreEqual(0x400000UL, unit.BaseAddress);
}
///
/// Verifies a v5 rnglistx range reference is recorded as an index and the CU's
/// explicit rnglists_base is captured.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_V5Rnglistx_RecordsIndexAndBase()
{
var abbrev = new DwarfBlob();
@@ -342,10 +352,10 @@ public void ReadFunctions_V5Rnglistx_RecordsIndexAndBase()
var result = DwarfReader.ReadFunctions(Sections(Cu(5, dies.ToArray()), abbrev.ToArray()));
- var (function, unit) = Assert.Single(result);
- Assert.Equal(2, function.RangesOffset);
- Assert.True(function.RangesIsRnglistx);
- Assert.Equal(0x20, unit.RnglistsBase);
+ var (function, unit) = Assert.ContainsSingle(result);
+ Assert.AreEqual(2, function.RangesOffset);
+ Assert.IsTrue(function.RangesIsRnglistx);
+ Assert.AreEqual(0x20, unit.RnglistsBase);
}
///
@@ -353,7 +363,8 @@ public void ReadFunctions_V5Rnglistx_RecordsIndexAndBase()
/// data16, implicit_const, indirect, and an unresolvable strx2 —
/// advances the cursor exactly, so the subprogram after it still parses.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_SkipsEveryFormOnUnrelatedDie()
{
var abbrev = new DwarfBlob();
@@ -421,17 +432,18 @@ public void ReadFunctions_SkipsEveryFormOnUnrelatedDie()
var result = DwarfReader.ReadFunctions(Sections(Cu(5, dies.ToArray()), abbrev.ToArray()));
- var (function, _) = Assert.Single(result);
- Assert.Equal("after", function.Name);
- Assert.Equal(0x9000UL, function.LowPc);
+ var (function, _) = Assert.ContainsSingle(result);
+ Assert.AreEqual("after", function.Name);
+ Assert.AreEqual(0x9000UL, function.LowPc);
}
///
/// Verifies the linkage name (plain or MIPS-vendor) is preferred over the source name.
///
- [Theory(Timeout = 30_000)]
- [InlineData(DwarfForm.AtLinkageName)]
- [InlineData(DwarfForm.AtMipsLinkageName)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow(DwarfForm.AtLinkageName)]
+ [DataRow(DwarfForm.AtMipsLinkageName)]
public void ReadFunctions_LinkageName_PreferredOverSourceName(ulong linkageAttribute)
{
var abbrev = new DwarfBlob();
@@ -446,16 +458,16 @@ public void ReadFunctions_LinkageName_PreferredOverSourceName(ulong linkageAttri
.ULeb(2).CStr("plain").CStr("_Zmangled").U64(0x1000)
.ULeb(0);
- var (function, _) = Assert.Single(
- DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray())));
- Assert.Equal("_Zmangled", function.Name);
+ var (function, _) = Assert.ContainsSingle(DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray())));
+ Assert.AreEqual("_Zmangled", function.Name);
}
///
/// Verifies an address-class high_pc below low_pc yields size 0 rather than
/// wrapping negative.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_HighPcBelowLowPc_SizeZero()
{
var abbrev = new DwarfBlob();
@@ -470,16 +482,16 @@ public void ReadFunctions_HighPcBelowLowPc_SizeZero()
.ULeb(2).CStr("f").U64(0x2000).U64(0x1000)
.ULeb(0);
- var (function, _) = Assert.Single(
- DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray())));
- Assert.Equal(0UL, function.Size);
+ var (function, _) = Assert.ContainsSingle(DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray())));
+ Assert.AreEqual(0UL, function.Size);
}
///
/// Verifies a second unit whose declared length overruns the section ends the walk, keeping
/// the first unit's functions.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_TruncatedSecondUnit_KeepsFirstFunction()
{
var abbrev = new DwarfBlob();
@@ -496,15 +508,16 @@ public void ReadFunctions_TruncatedSecondUnit_KeepsFirstFunction()
var result = DwarfReader.ReadFunctions(Sections([.. info], abbrev.ToArray()));
- var (function, _) = Assert.Single(result);
- Assert.Equal("ok", function.Name);
+ var (function, _) = Assert.ContainsSingle(result);
+ Assert.AreEqual("ok", function.Name);
}
///
/// Verifies an unsupported-version unit is skipped by its declared length and the walk
/// continues into the next unit.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_UnsupportedVersionUnit_SkipsToNextUnit()
{
var abbrev = new DwarfBlob();
@@ -520,15 +533,16 @@ public void ReadFunctions_UnsupportedVersionUnit_SkipsToNextUnit()
var result = DwarfReader.ReadFunctions(Sections([.. info], abbrev.ToArray()));
- var (function, _) = Assert.Single(result);
- Assert.Equal("ok", function.Name);
+ var (function, _) = Assert.ContainsSingle(result);
+ Assert.AreEqual("ok", function.Name);
}
///
/// Verifies a v5 skeleton unit (unit type 4) contributes no functions even though it holds a
/// well-formed subprogram.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_SkeletonUnit_Skipped()
{
var abbrev = new DwarfBlob();
@@ -542,14 +556,15 @@ public void ReadFunctions_SkeletonUnit_Skipped()
var result = DwarfReader.ReadFunctions(
Sections(Cu(5, dies.ToArray(), unitType: 4), abbrev.ToArray()));
- Assert.Empty(result);
+ Assert.IsEmpty(result);
}
///
/// Verifies an abbreviation code missing from the table ends that unit's walk, keeping the
/// functions parsed before it.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_UnknownAbbrevCode_KeepsEarlierFunctions()
{
var abbrev = new DwarfBlob();
@@ -562,31 +577,33 @@ public void ReadFunctions_UnknownAbbrevCode_KeepsEarlierFunctions()
var result = DwarfReader.ReadFunctions(Sections(Cu(4, dies.ToArray()), abbrev.ToArray()));
- var (function, _) = Assert.Single(result);
- Assert.Equal("first", function.Name);
+ var (function, _) = Assert.ContainsSingle(result);
+ Assert.AreEqual("first", function.Name);
}
/// Verifies empty or absent sections yield no functions.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadFunctions_EmptyOrZeroLengthInfo_ReturnsEmpty()
{
- Assert.Empty(DwarfReader.ReadFunctions(Sections([], [])));
+ Assert.IsEmpty(DwarfReader.ReadFunctions(Sections([], [])));
var zeroLength = new DwarfBlob().U32(0).Bytes(new byte[16]).ToArray();
- Assert.Empty(DwarfReader.ReadFunctions(Sections(zeroLength, [1])));
+ Assert.IsEmpty(DwarfReader.ReadFunctions(Sections(zeroLength, [1])));
}
///
/// Verifies maps base names through the lookup and
/// tolerates absent sections as empty bytes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Collect_MapsBaseNamesAndToleratesAbsent()
{
var sections = DwarfSections.Collect(name => name == "info" ? [0xAB] : null);
- Assert.Equal([0xAB], sections.Info);
- Assert.Empty(sections.Abbrev);
- Assert.False(sections.HasInfo); // DIEs without abbreviations are unreadable
+ Assert.AreSequenceEqual(new byte[] { 0xAB }, sections.Info);
+ Assert.IsEmpty(sections.Abbrev);
+ Assert.IsFalse(sections.HasInfo); // DIEs without abbreviations are unreadable
}
}
diff --git a/tests/Dotsider.Tests/DynamicAnalysisAccessibilityTests.cs b/tests/Dotsider.Tests/DynamicAnalysisAccessibilityTests.cs
index 811197c9..930054ae 100644
--- a/tests/Dotsider.Tests/DynamicAnalysisAccessibilityTests.cs
+++ b/tests/Dotsider.Tests/DynamicAnalysisAccessibilityTests.cs
@@ -6,23 +6,24 @@ namespace Dotsider.Tests;
///
/// Tests for Dynamic Analysis Accessibility.
///
+[TestClass]
public class DynamicAnalysisAccessibilityTests
{
///
/// Verifies category colors all categories have entries.
///
- [Fact]
+ [TestMethod]
public void CategoryColors_AllCategoriesHaveEntries()
{
foreach (var category in Enum.GetValues())
- Assert.True(DynamicAnalysisView.CategoryColors.ContainsKey(category),
+ Assert.IsTrue(DynamicAnalysisView.CategoryColors.ContainsKey(category),
$"Missing color for {category}");
}
///
/// Verifies category colors no duplicate colors.
///
- [Fact]
+ [TestMethod]
public void CategoryColors_NoDuplicateColors()
{
var seen = new Dictionary<(byte R, byte G, byte B), TraceEventCategory>();
@@ -34,7 +35,7 @@ public void CategoryColors_NoDuplicateColors()
continue;
var key = (color.R, color.G, color.B);
- Assert.False(seen.TryGetValue(key, out var existing),
+ Assert.IsFalse(seen.TryGetValue(key, out var existing),
$"{category} shares color ({color.R},{color.G},{color.B}) with {existing}");
seen[key] = category;
}
@@ -43,12 +44,12 @@ public void CategoryColors_NoDuplicateColors()
///
/// Verifies socket and http have different colors.
///
- [Fact]
+ [TestMethod]
public void SocketAndHttp_HaveDifferentColors()
{
var httpColor = DynamicAnalysisView.CategoryColors[TraceEventCategory.Http];
var socketColor = DynamicAnalysisView.CategoryColors[TraceEventCategory.Socket];
- Assert.False(
+ Assert.IsFalse(
httpColor.R == socketColor.R && httpColor.G == socketColor.G && httpColor.B == socketColor.B,
"Http and Socket must have distinct colors for accessibility");
}
@@ -58,36 +59,36 @@ public void SocketAndHttp_HaveDifferentColors()
///
/// Verifies try parse jit detail valid format returns true with components.
///
- [Theory]
- [InlineData("System.String.Concat", "System.String", "Concat")]
- [InlineData("MyApp.Services.UserService.GetUser", "MyApp.Services.UserService", "GetUser")]
- [InlineData("GlobalType.Run", "GlobalType", "Run")]
+ [TestMethod]
+ [DataRow("System.String.Concat", "System.String", "Concat")]
+ [DataRow("MyApp.Services.UserService.GetUser", "MyApp.Services.UserService", "GetUser")]
+ [DataRow("GlobalType.Run", "GlobalType", "Run")]
public void TryParseJitDetail_ValidFormat_ReturnsTrueWithComponents(
string detail, string expectedType, string expectedMethod)
{
- Assert.True(DynamicAnalysisView.TryParseJitDetail(detail, out var declaringType, out var methodName));
- Assert.Equal(expectedType, declaringType);
- Assert.Equal(expectedMethod, methodName);
+ Assert.IsTrue(DynamicAnalysisView.TryParseJitDetail(detail, out var declaringType, out var methodName));
+ Assert.AreEqual(expectedType, declaringType);
+ Assert.AreEqual(expectedMethod, methodName);
}
///
/// Verifies try parse jit detail invalid format returns false.
///
- [Theory]
- [InlineData("")]
- [InlineData("NoDotHere")]
- [InlineData(".LeadingDot")]
+ [TestMethod]
+ [DataRow("")]
+ [DataRow("NoDotHere")]
+ [DataRow(".LeadingDot")]
public void TryParseJitDetail_InvalidFormat_ReturnsFalse(string detail)
{
- Assert.False(DynamicAnalysisView.TryParseJitDetail(detail, out _, out _));
+ Assert.IsFalse(DynamicAnalysisView.TryParseJitDetail(detail, out _, out _));
}
///
/// Verifies try parse jit detail trailing dot returns false.
///
- [Fact]
+ [TestMethod]
public void TryParseJitDetail_TrailingDot_ReturnsFalse()
{
- Assert.False(DynamicAnalysisView.TryParseJitDetail("System.String.", out _, out _));
+ Assert.IsFalse(DynamicAnalysisView.TryParseJitDetail("System.String.", out _, out _));
}
}
diff --git a/tests/Dotsider.Tests/DynamicFocusColorTests.cs b/tests/Dotsider.Tests/DynamicFocusColorTests.cs
index f13ac0b8..52072afb 100644
--- a/tests/Dotsider.Tests/DynamicFocusColorTests.cs
+++ b/tests/Dotsider.Tests/DynamicFocusColorTests.cs
@@ -12,9 +12,11 @@ namespace Dotsider.Tests;
/// Reproduces #92: the focused row on the Dynamic tab's Events table shows
/// category-colored text (yellow, green, etc.) instead of black on teal.
///
-[Collection("SampleAssemblies")]
-public class DynamicFocusColorTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DynamicFocusColorTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -32,7 +34,7 @@ public class DynamicFocusColorTests(SampleAssemblyFixture samples) : IDisposable
_hex1bApp = new Hex1bApp(
ctx =>
{
- _state ??= new DotsiderState(_hex1bApp!, samples.HelloWorldDll);
+ _state ??= new DotsiderState(_hex1bApp!, Samples.HelloWorldDll);
dotsiderApp ??= new DotsiderApp(_state);
return Task.FromResult(dotsiderApp.Build(ctx));
},
@@ -47,11 +49,12 @@ public class DynamicFocusColorTests(SampleAssemblyFixture samples) : IDisposable
///
/// Verifies events focused row category cell uses black foreground.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Events_FocusedRow_CategoryCellUsesBlackForeground()
{
var (terminal, app) = CreateDotsiderApp();
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -105,7 +108,7 @@ await auto.WaitUntilAsync(s =>
}, description: "teal focus background to appear");
var focusedKey = _state!.DynamicEventsFocusedKey;
- Assert.NotNull(focusedKey);
+ Assert.IsNotNull(focusedKey);
// The focused row's category cell must NOT have any of the known category
// colors. When focused, it should be black (0,0,0) or null (theme default).
@@ -116,14 +119,13 @@ await auto.WaitUntilAsync(s =>
if (categoryFg is not null)
{
var fg = categoryFg.Value;
- Assert.False(knownCategoryColors.Contains((fg.R, fg.G, fg.B)),
+ Assert.DoesNotContain((fg.R, fg.G, fg.B), knownCategoryColors,
$"Focused row category cell still has category color ({fg.R},{fg.G},{fg.B}) — should be black on focus");
// WCAG 2.1 AA requires >= 4.5:1 contrast ratio for normal text.
// Verify the foreground color against the teal focus background.
var ratio = ContrastRatio(fg.R, fg.G, fg.B, teal.R, teal.G, teal.B);
- Assert.True(ratio >= 4.5,
- $"Focused category cell fails WCAG AA: contrast ratio {ratio:F2}:1 " +
+ Assert.IsGreaterThanOrEqualTo(4.5, ratio, $"Focused category cell fails WCAG AA: contrast ratio {ratio:F2}:1 " +
$"(fg={fg.R},{fg.G},{fg.B} bg={teal.R},{teal.G},{teal.B}), minimum is 4.5:1");
}
@@ -159,10 +161,10 @@ private static double Linearize(double c) =>
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/DynamicTabGuardTests.cs b/tests/Dotsider.Tests/DynamicTabGuardTests.cs
index fb21f3bb..0992aafc 100644
--- a/tests/Dotsider.Tests/DynamicTabGuardTests.cs
+++ b/tests/Dotsider.Tests/DynamicTabGuardTests.cs
@@ -9,9 +9,11 @@ namespace Dotsider.Tests;
///
/// Tests for Dynamic Tab Guard.
///
-[Collection("SampleAssemblies")]
-public class DynamicTabGuardTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DynamicTabGuardTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -60,10 +62,10 @@ private Hex1bApp CreateApp()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
- GC.SuppressFinalize(this);
}
// --- Unit tests ---
@@ -71,14 +73,15 @@ public void Dispose()
///
/// Verifies is net framework true for net fx console.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IsNetFramework_TrueForNetFxConsole()
{
- Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "net48 sample is Windows-only");
+ TestSkip.Unless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "net48 sample is Windows-only");
var app = CreateApp();
- using var state = new DotsiderState(app, samples.NetFxConsoleExe!);
- Assert.True(state.IsNetFramework);
- Assert.Contains(".NETFramework", state.Analyzer.TargetFramework);
+ using var state = new DotsiderState(app, Samples.NetFxConsoleExe!);
+ Assert.IsTrue(state.IsNetFramework);
+ Assert.Contains(".NETFramework", state.Analyzer.TargetFramework!);
}
///
@@ -88,16 +91,17 @@ public void IsNetFramework_TrueForNetFxConsole()
/// inferred state — otherwise EventPipe tracing would be wrongly enabled and the General tab
/// would show "(unknown)".
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IsNetFramework_TrueForClr2RootWithoutTfa_AndDisplayShowsInferredLabel()
{
- Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "net35 sample is Windows-only");
- Assert.NotNull(samples.NetFxBindingRedirectsClr2Exe);
+ TestSkip.Unless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "net35 sample is Windows-only");
+ Assert.IsNotNull(Samples.NetFxBindingRedirectsClr2Exe);
var app = CreateApp();
- using var state = new DotsiderState(app, samples.NetFxBindingRedirectsClr2Exe!);
+ using var state = new DotsiderState(app, Samples.NetFxBindingRedirectsClr2Exe!);
- Assert.True(state.IsNetFramework);
- Assert.Null(state.Analyzer.TargetFramework); // no TFA on the assembly
+ Assert.IsTrue(state.IsNetFramework);
+ Assert.IsNull(state.Analyzer.TargetFramework); // no TFA on the assembly
var display = state.EffectiveTargetFrameworkDisplay;
Assert.Contains("CLR v2.0", display);
Assert.Contains("inferred", display, StringComparison.OrdinalIgnoreCase);
@@ -106,27 +110,29 @@ public void IsNetFramework_TrueForClr2RootWithoutTfa_AndDisplayShowsInferredLabe
///
/// Verifies is net framework false for core apps.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IsNetFramework_FalseForCoreApps()
{
var app = CreateApp();
- string[] paths = [samples.HelloWorldDll, samples.RichLibraryDll, samples.ComplexAppDll];
+ string[] paths = [Samples.HelloWorldDll, Samples.RichLibraryDll, Samples.ComplexAppDll];
foreach (var path in paths)
{
using var state = new DotsiderState(app, path);
- Assert.False(state.IsNetFramework, $"IsNetFramework should be false for {Path.GetFileName(path)}");
+ Assert.IsFalse(state.IsNetFramework, $"IsNetFramework should be false for {Path.GetFileName(path)}");
}
}
///
/// Verifies is native aot true for native aot console.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IsNativeAot_TrueForNativeAotConsole()
{
var app = CreateApp();
- using var state = new DotsiderState(app, samples.NativeAotConsoleExe!);
- Assert.True(state.IsNativeAot);
+ using var state = new DotsiderState(app, Samples.NativeAotConsoleExe!);
+ Assert.IsTrue(state.IsNativeAot);
}
// --- Input sequence tests ---
@@ -134,12 +140,13 @@ public void IsNativeAot_TrueForNativeAotConsole()
///
/// Verifies tab8 net framework shows guard message.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Tab8_NetFramework_ShowsGuardMessage()
{
- Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "net48 sample is Windows-only");
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
- var (terminal, app) = CreateDotsiderApp(samples.NetFxConsoleExe!);
+ TestSkip.Unless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "net48 sample is Windows-only");
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
+ var (terminal, app) = CreateDotsiderApp(Samples.NetFxConsoleExe!);
var runTask = app.RunAsync(cts.Token);
await Task.Delay(100, cts.Token);
@@ -157,7 +164,7 @@ public async Task Tab8_NetFramework_ShowsGuardMessage()
.ApplyAsync(terminal, cts.Token);
// Verify tracer was NOT created
- Assert.Null(_state!.Tracer);
+ Assert.IsNull(_state!.Tracer);
cts.Cancel();
await runTask;
@@ -166,11 +173,12 @@ public async Task Tab8_NetFramework_ShowsGuardMessage()
///
/// Verifies tab8 native aot shows idle view not guard.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Tab8_NativeAot_ShowsIdleViewNotGuard()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
- var (terminal, app) = CreateDotsiderApp(samples.NativeAotConsoleExe!);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
+ var (terminal, app) = CreateDotsiderApp(Samples.NativeAotConsoleExe!);
var runTask = app.RunAsync(cts.Token);
await Task.Delay(100, cts.Token);
@@ -199,11 +207,12 @@ public async Task Tab8_NativeAot_ShowsIdleViewNotGuard()
///
/// Verifies native aot navigate all tabs no crash.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task NativeAot_NavigateAllTabs_NoCrash()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
- var (terminal, app) = CreateDotsiderApp(samples.NativeAotConsoleExe!);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
+ var (terminal, app) = CreateDotsiderApp(Samples.NativeAotConsoleExe!);
var runTask = app.RunAsync(cts.Token);
await Task.Delay(100, cts.Token);
@@ -234,7 +243,7 @@ public async Task NativeAot_NavigateAllTabs_NoCrash()
.Build()
.ApplyAsync(terminal, cts.Token);
- Assert.Equal(TabId.Strings, _state!.CurrentTab);
+ Assert.AreEqual(TabId.Strings, _state!.CurrentTab);
cts.Cancel();
await runTask;
diff --git a/tests/Dotsider.Tests/DynamicYankIntegrationTests.cs b/tests/Dotsider.Tests/DynamicYankIntegrationTests.cs
index 1932aa5b..fc16dc8f 100644
--- a/tests/Dotsider.Tests/DynamicYankIntegrationTests.cs
+++ b/tests/Dotsider.Tests/DynamicYankIntegrationTests.cs
@@ -10,9 +10,11 @@ namespace Dotsider.Tests;
/// Integration tests for yank flash, readonly editor yank, and focus management
/// on the Dynamic tab (issue #103).
///
-[Collection("SampleAssemblies")]
-public class DynamicYankIntegrationTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class DynamicYankIntegrationTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private ClipboardCapturingWorkloadAdapter? _clipboardAdapter;
private Hex1bTerminal? _terminal;
@@ -22,7 +24,7 @@ public class DynamicYankIntegrationTests(SampleAssemblyFixture samples) : IDispo
private (Hex1bTerminal terminal, Hex1bApp app, CancellationToken ct) Launch(string dllPath)
{
- _cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
_workload = new Hex1bAppWorkloadAdapter();
_clipboardAdapter = new ClipboardCapturingWorkloadAdapter(_workload);
_terminal = Hex1bTerminal.CreateBuilder()
@@ -84,22 +86,22 @@ private Hex1bTerminalInputSequenceBuilder NavigateToCounters()
.Key(Hex1bKey.RightArrow)
.WaitUntil(_ => _state!.DynamicSubTab == DynamicSubTabId.Counters, TimeSpan.FromSeconds(5))
.WaitUntil(_ => _state!.DynamicCpuEditorState is not null, TimeSpan.FromSeconds(5))
- .WaitUntil(_ => IsFocusedOnEditor(), TimeSpan.FromSeconds(5));
+ .WaitUntil(_ => IsFocusedOnEditor(_state!.DynamicCpuEditorState), TimeSpan.FromSeconds(5));
}
/// Moves focus out of the Counters editors to the subtab strip.
private async Task TabOutOfCountersEditorsAsync(Hex1bTerminal terminal, CancellationToken ct)
{
- // During a live trace, render cycles can be slow because each frame
- // processes EventPipe events. Instead of tabbing through editors one
- // at a time (which requires one render cycle per Tab), directly request
- // focus on the subtab strip and wait for it.
- _state!.App.RequestFocus(node =>
- node.GetType().Name.StartsWith("TabPanelNode"));
- _state.App.Invalidate();
-
await new Hex1bTerminalInputSequenceBuilder()
- .WaitUntil(_ => !IsFocusedOnEditor(), TimeSpan.FromSeconds(10))
+ .WaitUntil(_ => IsFocusedOnEditor(_state!.DynamicCpuEditorState), TimeSpan.FromSeconds(5))
+ .Key(Hex1bKey.Tab)
+ .WaitUntil(_ => IsFocusedOnEditor(_state!.DynamicMemoryEditorState), TimeSpan.FromSeconds(5))
+ .Key(Hex1bKey.Tab)
+ .WaitUntil(_ => IsFocusedOnEditor(_state!.DynamicGcEditorState), TimeSpan.FromSeconds(5))
+ .Key(Hex1bKey.Tab)
+ .WaitUntil(_ => IsFocusedOnEditor(_state!.DynamicThreadingEditorState), TimeSpan.FromSeconds(5))
+ .Key(Hex1bKey.Tab)
+ .WaitUntil(_ => !IsFocusedOnEditor(), TimeSpan.FromSeconds(5))
.Build()
.ApplyAsync(terminal, ct);
}
@@ -119,10 +121,11 @@ private Hex1bTerminalInputSequenceBuilder NavigateFromCountersToSummary()
///
/// Verifies dynamic events yank on focused row copies payload and flashes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Events_YankOnFocusedRow_CopiesPayload_AndFlashes()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit()
@@ -134,8 +137,8 @@ await LaunchTraceAndWaitForExit()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
+ Assert.IsNotNull(_state!.YankNotification);
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
"CopyToClipboard should have emitted an OSC 52 sequence");
_cts!.Cancel();
@@ -145,10 +148,11 @@ await LaunchTraceAndWaitForExit()
///
/// Verifies dynamic output yank on focused row copies payload and flashes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Output_YankOnFocusedRow_CopiesPayload_AndFlashes()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit().Build().ApplyAsync(terminal, ct);
@@ -168,8 +172,8 @@ public async Task Dynamic_Output_YankOnFocusedRow_CopiesPayload_AndFlashes()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
+ Assert.IsNotNull(_state!.YankNotification);
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
"CopyToClipboard should have emitted an OSC 52 sequence");
_cts!.Cancel();
@@ -179,10 +183,11 @@ public async Task Dynamic_Output_YankOnFocusedRow_CopiesPayload_AndFlashes()
///
/// Verifies dynamic events yank flash during search.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Events_YankFlashDuringSearch()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit()
@@ -201,8 +206,8 @@ await LaunchTraceAndWaitForExit()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
+ Assert.IsNotNull(_state!.YankNotification);
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
"CopyToClipboard should have emitted an OSC 52 sequence");
_cts!.Cancel();
@@ -212,10 +217,11 @@ await LaunchTraceAndWaitForExit()
///
/// Verifies dynamic counters selection yank works.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Counters_SelectionYank_Works()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit().Build().ApplyAsync(terminal, ct);
@@ -226,8 +232,8 @@ await NavigateToCounters()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
+ Assert.IsNotNull(_state!.YankNotification);
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
"CopyToClipboard should have emitted an OSC 52 sequence");
_cts!.Cancel();
@@ -237,10 +243,11 @@ await NavigateToCounters()
///
/// Verifies dynamic summary selection yank works.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Summary_SelectionYank_Works()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit().Build().ApplyAsync(terminal, ct);
@@ -253,8 +260,8 @@ await NavigateFromCountersToSummary()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.YankNotification);
- Assert.True(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
+ Assert.IsNotNull(_state!.YankNotification);
+ Assert.IsTrue(_clipboardAdapter!.ClipboardWrites.TryDequeue(out _),
"CopyToClipboard should have emitted an OSC 52 sequence");
_cts!.Cancel();
@@ -264,16 +271,17 @@ await NavigateFromCountersToSummary()
///
/// Verifies dynamic left right navigate sub tabs from editor focus.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_LeftRightNavigateSubTabsFromEditorFocus()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit().Build().ApplyAsync(terminal, ct);
await NavigateToCounters().Build().ApplyAsync(terminal, ct);
- Assert.Equal(DynamicSubTabId.Counters, _state!.DynamicSubTab);
+ Assert.AreEqual(DynamicSubTabId.Counters, _state!.DynamicSubTab);
// Left from Counters → Events
await new Hex1bTerminalInputSequenceBuilder()
@@ -282,7 +290,7 @@ public async Task Dynamic_LeftRightNavigateSubTabsFromEditorFocus()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(DynamicSubTabId.Events, _state.DynamicSubTab);
+ Assert.AreEqual(DynamicSubTabId.Events, _state.DynamicSubTab);
// Right from Events → Counters
await new Hex1bTerminalInputSequenceBuilder()
@@ -291,7 +299,7 @@ public async Task Dynamic_LeftRightNavigateSubTabsFromEditorFocus()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(DynamicSubTabId.Counters, _state.DynamicSubTab);
+ Assert.AreEqual(DynamicSubTabId.Counters, _state.DynamicSubTab);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -300,18 +308,19 @@ public async Task Dynamic_LeftRightNavigateSubTabsFromEditorFocus()
///
/// Verifies dynamic tab from editor focuses subtab strip stays on sub tab.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_TabFromEditor_FocusesSubtabStrip_StaysOnSubTab()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit().Build().ApplyAsync(terminal, ct);
await NavigateToCounters().Build().ApplyAsync(terminal, ct);
await TabOutOfCountersEditorsAsync(terminal, ct);
- Assert.Equal(TabId.Dynamic, _state!.CurrentTab);
- Assert.Equal(DynamicSubTabId.Counters, _state.DynamicSubTab);
+ Assert.AreEqual(TabId.Dynamic, _state!.CurrentTab);
+ Assert.AreEqual(DynamicSubTabId.Counters, _state.DynamicSubTab);
await new Hex1bTerminalInputSequenceBuilder()
.Key(Hex1bKey.RightArrow)
@@ -319,7 +328,7 @@ public async Task Dynamic_TabFromEditor_FocusesSubtabStrip_StaysOnSubTab()
.Build()
.ApplyAsync(terminal, ct);
- Assert.Equal(TabId.Dynamic, _state.CurrentTab);
+ Assert.AreEqual(TabId.Dynamic, _state.CurrentTab);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -328,10 +337,11 @@ public async Task Dynamic_TabFromEditor_FocusesSubtabStrip_StaysOnSubTab()
///
/// Verifies dynamic rerun clears dynamic editor caches.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Rerun_ClearsDynamicEditorCaches()
{
- var (terminal, app, ct) = Launch(samples.HelloWorldDll);
+ var (terminal, app, ct) = Launch(Samples.HelloWorldDll);
var runTask = app.RunAsync(ct);
await LaunchTraceAndWaitForExit().Build().ApplyAsync(terminal, ct);
@@ -339,8 +349,8 @@ public async Task Dynamic_Rerun_ClearsDynamicEditorCaches()
await TabOutOfCountersEditorsAsync(terminal, ct);
await NavigateFromCountersToSummary().Build().ApplyAsync(terminal, ct);
- Assert.NotNull(_state!.DynamicCpuEditorState);
- Assert.NotNull(_state.DynamicSummaryEditorState);
+ Assert.IsNotNull(_state!.DynamicCpuEditorState);
+ Assert.IsNotNull(_state.DynamicSummaryEditorState);
await new Hex1bTerminalInputSequenceBuilder()
.Key(Hex1bKey.Tab)
@@ -356,10 +366,10 @@ _state.DynamicCpuEditorState is null
.Build()
.ApplyAsync(terminal, ct);
- Assert.Null(_state.DynamicCpuEditorState);
- Assert.Null(_state.DynamicCpuEditorText);
- Assert.Null(_state.DynamicSummaryEditorState);
- Assert.Null(_state.DynamicSummaryEditorText);
+ Assert.IsNull(_state.DynamicCpuEditorState);
+ Assert.IsNull(_state.DynamicCpuEditorText);
+ Assert.IsNull(_state.DynamicSummaryEditorState);
+ Assert.IsNull(_state.DynamicSummaryEditorText);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -368,10 +378,11 @@ _state.DynamicCpuEditorState is null
///
/// Verifies dynamic counters live update preserves selection while focused.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Counters_LiveUpdate_PreservesSelectionWhileFocused()
{
- var (terminal, app, ct) = Launch(samples.MinimalApiDll);
+ var (terminal, app, ct) = Launch(Samples.MinimalApiDll);
var runTask = app.RunAsync(ct);
await new Hex1bTerminalInputSequenceBuilder()
@@ -395,7 +406,7 @@ public async Task Dynamic_Counters_LiveUpdate_PreservesSelectionWhileFocused()
.ApplyAsync(terminal, ct);
await Task.Delay(1500, ct);
- Assert.Same(editorStateBefore, _state.DynamicCpuEditorState);
+ Assert.AreSame(editorStateBefore, _state.DynamicCpuEditorState);
_state.Tracer?.Stop();
_cts!.Cancel();
@@ -405,10 +416,11 @@ public async Task Dynamic_Counters_LiveUpdate_PreservesSelectionWhileFocused()
///
/// Verifies dynamic summary live update preserves selection while focused.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Summary_LiveUpdate_PreservesSelectionWhileFocused()
{
- var (terminal, app, ct) = Launch(samples.MinimalApiDll);
+ var (terminal, app, ct) = Launch(Samples.MinimalApiDll);
var runTask = app.RunAsync(ct);
await new Hex1bTerminalInputSequenceBuilder()
@@ -437,7 +449,7 @@ await NavigateFromCountersToSummary()
.ApplyAsync(terminal, ct);
await Task.Delay(1500, ct);
- Assert.Same(editorStateBefore, _state.DynamicSummaryEditorState);
+ Assert.AreSame(editorStateBefore, _state.DynamicSummaryEditorState);
_state.Tracer?.Stop();
_cts!.Cancel();
@@ -447,10 +459,11 @@ await NavigateFromCountersToSummary()
///
/// Verifies dynamic summary post exit refresh updates while focused.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Dynamic_Summary_PostExitRefresh_UpdatesWhileFocused()
{
- var (terminal, app, ct) = Launch(samples.MinimalApiDll);
+ var (terminal, app, ct) = Launch(Samples.MinimalApiDll);
var runTask = app.RunAsync(ct);
// Launch trace, wait for running, switch to Summary (via Counters)
@@ -473,7 +486,7 @@ await NavigateFromCountersToSummary()
.Build()
.ApplyAsync(terminal, ct);
- Assert.True(IsFocusedOnEditor());
+ Assert.IsTrue(IsFocusedOnEditor());
// Capture the frozen EditorState reference while the process is running.
// The freeze mechanism keeps this exact instance alive while focused+running.
@@ -494,7 +507,7 @@ await NavigateFromCountersToSummary()
await auto.WaitUntilAsync(_ => _state.DynamicSummaryEditorState != frozenState,
description: "editor state to update after freeze lifts");
- Assert.NotSame(frozenState, _state.DynamicSummaryEditorState);
+ Assert.AreNotSame(frozenState, _state.DynamicSummaryEditorState);
_cts!.Cancel();
try { await runTask; } catch (OperationCanceledException) { }
@@ -505,6 +518,7 @@ await auto.WaitUntilAsync(_ => _state.DynamicSummaryEditorState != frozenState,
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_state?.Tracer?.Stop();
_state?.Dispose();
_hex1bApp?.Dispose();
@@ -512,6 +526,5 @@ public void Dispose()
_clipboardAdapter?.Dispose();
_workload?.Dispose();
_cts?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/EhFrameReaderTests.cs b/tests/Dotsider.Tests/EhFrameReaderTests.cs
index 39d8e6c2..fba66854 100644
--- a/tests/Dotsider.Tests/EhFrameReaderTests.cs
+++ b/tests/Dotsider.Tests/EhFrameReaderTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
/// .eh_frame blobs covering the pointer-encoding matrix (absolute, PC-relative, fixed,
/// LEB128, signed), CIE augmentation shapes, and the terminator and damage behaviors.
///
+[TestClass]
public class EhFrameReaderTests
{
private static byte[] Entry(DwarfBlob content) =>
@@ -59,7 +60,8 @@ private static byte[] Image(ulong ehFrameAddress, params byte[][] entries)
///
/// Verifies absolute 64-bit pointers decode and the boundary maps to its containing section.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadBoundaries_Absptr_DecodesAndMapsSection()
{
var cie = Cie(0x00);
@@ -67,20 +69,21 @@ public void ReadBoundaries_Absptr_DecodesAndMapsSection()
var boundaries = EhFrameReader.ReadBoundaries(Image(0x500000, cie, fde));
- var b = Assert.Single(boundaries);
- Assert.Equal("sub_401010", b.Name);
- Assert.Equal(0x401010UL, b.VirtualAddress);
- Assert.Equal(0x40, b.Size);
- Assert.Equal(".text", b.Section);
- Assert.True(b.IsBoundary);
- Assert.NotNull(b.FileOffset);
+ var b = Assert.ContainsSingle(boundaries);
+ Assert.AreEqual("sub_401010", b.Name);
+ Assert.AreEqual(0x401010UL, b.VirtualAddress);
+ Assert.AreEqual(0x40, b.Size);
+ Assert.AreEqual(".text", b.Section);
+ Assert.IsTrue(b.IsBoundary);
+ Assert.IsNotNull(b.FileOffset);
}
///
/// Verifies the common pcrel|sdata4 encoding: the location is relative to the
/// pointer field's own address, and the range is a plain length.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadBoundaries_PcrelSdata4_ResolvesAgainstFieldAddress()
{
const ulong ehFrameAddress = 0x500000;
@@ -97,49 +100,52 @@ public void ReadBoundaries_PcrelSdata4_ResolvesAgainstFieldAddress()
var boundaries = EhFrameReader.ReadBoundaries(Image(ehFrameAddress, cie, fde1, fde2));
- Assert.Equal(2, boundaries.Count);
- Assert.Equal(0x401010UL, boundaries[0].VirtualAddress);
- Assert.Equal(0x40, boundaries[0].Size);
- Assert.Equal(0x401080UL, boundaries[1].VirtualAddress);
- Assert.Equal(0x10, boundaries[1].Size);
+ Assert.HasCount(2, boundaries);
+ Assert.AreEqual(0x401010UL, boundaries[0].VirtualAddress);
+ Assert.AreEqual(0x40, boundaries[0].Size);
+ Assert.AreEqual(0x401080UL, boundaries[1].VirtualAddress);
+ Assert.AreEqual(0x10, boundaries[1].Size);
}
/// Verifies the fixed udata4 and variable ULEB128 formats decode absolutely.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadBoundaries_Udata4AndUleb_Decode()
{
var cie4 = Cie(0x03); // udata4 absolute
var fde4 = Fde((uint)(cie4.Length + 4), new DwarfBlob().U32(0x401020).U32(0x20));
- var one = Assert.Single(EhFrameReader.ReadBoundaries(Image(0x500000, cie4, fde4)));
- Assert.Equal(0x401020UL, one.VirtualAddress);
- Assert.Equal(0x20, one.Size);
+ var one = Assert.ContainsSingle(EhFrameReader.ReadBoundaries(Image(0x500000, cie4, fde4)));
+ Assert.AreEqual(0x401020UL, one.VirtualAddress);
+ Assert.AreEqual(0x20, one.Size);
var cieLeb = Cie(0x01); // uleb128 absolute
var fdeLeb = Fde((uint)(cieLeb.Length + 4), new DwarfBlob().ULeb(0x401030).ULeb(0x30));
- var leb = Assert.Single(EhFrameReader.ReadBoundaries(Image(0x500000, cieLeb, fdeLeb)));
- Assert.Equal(0x401030UL, leb.VirtualAddress);
- Assert.Equal(0x30, leb.Size);
+ var leb = Assert.ContainsSingle(EhFrameReader.ReadBoundaries(Image(0x500000, cieLeb, fdeLeb)));
+ Assert.AreEqual(0x401030UL, leb.VirtualAddress);
+ Assert.AreEqual(0x30, leb.Size);
}
///
/// Verifies a zPLR augmentation with a personality pointer parses the encoding behind
/// it, on a version-3 CIE whose return register is a ULEB.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadBoundaries_ZplrVersion3_ReadsEncodingBehindPersonality()
{
var cie = Cie(0x03, augmentation: "zPLR", version: 3);
var fde = Fde((uint)(cie.Length + 4), new DwarfBlob().U32(0x401040).U32(0x18));
- var b = Assert.Single(EhFrameReader.ReadBoundaries(Image(0x500000, cie, fde)));
- Assert.Equal(0x401040UL, b.VirtualAddress);
- Assert.Equal(0x18, b.Size);
+ var b = Assert.ContainsSingle(EhFrameReader.ReadBoundaries(Image(0x500000, cie, fde)));
+ Assert.AreEqual(0x401040UL, b.VirtualAddress);
+ Assert.AreEqual(0x18, b.Size);
}
///
/// Verifies the zero-length terminator stops the walk, and entries after it are ignored.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadBoundaries_Terminator_StopsWalk()
{
var cie = Cie(0x00);
@@ -152,21 +158,22 @@ public void ReadBoundaries_Terminator_StopsWalk()
var image = SyntheticImageBuilders.BuildElf((".eh_frame", 0x500000, section.ToArray()));
- Assert.Single(EhFrameReader.ReadBoundaries(image));
+ Assert.ContainsSingle(EhFrameReader.ReadBoundaries(image));
}
///
/// Verifies defective entries are skipped, not misread: a zero location, an FDE naming an
/// unknown CIE, and a truncated tail after a good FDE.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadBoundaries_DefectiveEntries_SkippedOrKeptPartial()
{
var cie = Cie(0x00);
var zeroLocation = Fde((uint)(cie.Length + 4), new DwarfBlob().U64(0).U64(0x40));
var orphan = Fde(2, new DwarfBlob().U64(0x401050).U64(0x40)); // points into no CIE
- Assert.Empty(EhFrameReader.ReadBoundaries(Image(0x500000, cie, zeroLocation)));
- Assert.Empty(EhFrameReader.ReadBoundaries(Image(0x500000, orphan)));
+ Assert.IsEmpty(EhFrameReader.ReadBoundaries(Image(0x500000, cie, zeroLocation)));
+ Assert.IsEmpty(EhFrameReader.ReadBoundaries(Image(0x500000, orphan)));
var good = Fde((uint)(cie.Length + 4), new DwarfBlob().U64(0x401010).U64(0x40));
var truncated = new DwarfBlob().U32(0xFFF0).U32(0).ToArray(); // claims more than present
@@ -176,9 +183,9 @@ public void ReadBoundaries_DefectiveEntries_SkippedOrKeptPartial()
section.AddRange(truncated);
var image = SyntheticImageBuilders.BuildElf((".eh_frame", 0x500000, section.ToArray()));
- Assert.Single(EhFrameReader.ReadBoundaries(image));
+ Assert.ContainsSingle(EhFrameReader.ReadBoundaries(image));
- Assert.Empty(EhFrameReader.ReadBoundaries(
+ Assert.IsEmpty(EhFrameReader.ReadBoundaries(
SyntheticImageBuilders.BuildElf((".text", 0x401000, new byte[8]))));
}
}
diff --git a/tests/Dotsider.Tests/ElfSectionTests.cs b/tests/Dotsider.Tests/ElfSectionTests.cs
index 3f4997d9..02d3240a 100644
--- a/tests/Dotsider.Tests/ElfSectionTests.cs
+++ b/tests/Dotsider.Tests/ElfSectionTests.cs
@@ -7,13 +7,15 @@ namespace Dotsider.Tests;
/// — the section-header walk that hands DWARF,
/// symbol-table, and build-id readers their bytes — driven with synthetic ELF images.
///
+[TestClass]
public class ElfSectionTests
{
///
/// Verifies the walk returns every named section with its address, file offset, and size,
/// alongside the null section and .shstrtab the builder always emits.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadSections_ReturnsNamedSectionsWithLocation()
{
var image = SyntheticImageBuilders.BuildElf(
@@ -22,36 +24,38 @@ public void ReadSections_ReturnsNamedSectionsWithLocation()
var sections = ElfImageReader.ReadSections(image);
- Assert.Equal(4, sections.Count); // null + .text + .debug_info + .shstrtab
- var text = Assert.Single(sections, s => s.Name == ".text");
- Assert.Equal(0x401000UL, text.Address);
- Assert.Equal(4, text.Size);
- Assert.Equal(1, image[text.FileOffset]);
+ Assert.HasCount(4, sections); // null + .text + .debug_info + .shstrtab
+ var text = Assert.ContainsSingle(s => s.Name == ".text", sections);
+ Assert.AreEqual(0x401000UL, text.Address);
+ Assert.AreEqual(4, text.Size);
+ Assert.AreEqual(1, image[text.FileOffset]);
- var info = Assert.Single(sections, s => s.Name == ".debug_info");
- Assert.Equal(2, info.Size);
- Assert.Equal(9, image[info.FileOffset]);
+ var info = Assert.ContainsSingle(s => s.Name == ".debug_info", sections);
+ Assert.AreEqual(2, info.Size);
+ Assert.AreEqual(9, image[info.FileOffset]);
}
/// Verifies the name lookup finds a present section and reports an absent one.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryGetSection_FindsPresentAndReportsAbsent()
{
var image = SyntheticImageBuilders.BuildElf((".debug_str", 0, new byte[] { 0, (byte)'a', 0 }));
- Assert.True(ElfImageReader.TryGetSection(image, ".debug_str", out var section));
- Assert.Equal(3, section.Size);
- Assert.False(ElfImageReader.TryGetSection(image, ".debug_line", out _));
+ Assert.IsTrue(ElfImageReader.TryGetSection(image, ".debug_str", out var section));
+ Assert.AreEqual(3, section.Size);
+ Assert.IsFalse(ElfImageReader.TryGetSection(image, ".debug_line", out _));
}
/// Verifies non-ELF bytes and truncated images yield no sections rather than throwing.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadSections_RejectsNonElfAndTruncated()
{
- Assert.Empty(ElfImageReader.ReadSections([0x4D, 0x5A, 0, 0]));
+ Assert.IsEmpty(ElfImageReader.ReadSections([0x4D, 0x5A, 0, 0]));
var truncated = SyntheticImageBuilders.BuildElf((".text", 0, new byte[] { 1 }))[..70];
- Assert.Empty(ElfImageReader.ReadSections(truncated));
+ Assert.IsEmpty(ElfImageReader.ReadSections(truncated));
}
///
@@ -59,7 +63,8 @@ public void ReadSections_RejectsNonElfAndTruncated()
/// inflates to the original bytes, plain sections pass through, and unsupported or
/// malformed compression reads as absent rather than as garbage.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadSectionBytes_InflatesCompressedSections()
{
byte[] payload = [.. Enumerable.Range(0, 512).Select(i => (byte)(i * 7))];
@@ -69,21 +74,21 @@ public void ReadSectionBytes_InflatesCompressedSections()
var sections = ElfImageReader.ReadSections(image);
var compressed = sections.Single(s => s.Name == ".debug_info");
- Assert.Equal(payload, ElfImageReader.ReadSectionBytes(image, compressed));
+ Assert.AreSequenceEqual(payload, ElfImageReader.ReadSectionBytes(image, compressed));
var plain = sections.Single(s => s.Name == ".debug_abbrev");
- Assert.Equal(payload, ElfImageReader.ReadSectionBytes(image, plain));
+ Assert.AreSequenceEqual(payload, ElfImageReader.ReadSectionBytes(image, plain));
// Unsupported compression type (zstd = 2): absent, not misread.
var zstd = SyntheticImageBuilders.CompressDebugSection(payload);
zstd[0] = 2;
var zstdImage = SyntheticImageBuilders.BuildElf((".debug_info", 0, zstd, 1u, 0u, 0x800UL));
- Assert.Null(ElfImageReader.ReadSectionBytes(
+ Assert.IsNull(ElfImageReader.ReadSectionBytes(
zstdImage, ElfImageReader.ReadSections(zstdImage).Single(s => s.Name == ".debug_info")));
// Truncated header: absent.
var tiny = SyntheticImageBuilders.BuildElf((".debug_info", 0, new byte[8], 1u, 0u, 0x800UL));
- Assert.Null(ElfImageReader.ReadSectionBytes(
+ Assert.IsNull(ElfImageReader.ReadSectionBytes(
tiny, ElfImageReader.ReadSections(tiny).Single(s => s.Name == ".debug_info")));
}
}
diff --git a/tests/Dotsider.Tests/ElfSidecarIdentityTests.cs b/tests/Dotsider.Tests/ElfSidecarIdentityTests.cs
index b42ca56f..c0aa7401 100644
--- a/tests/Dotsider.Tests/ElfSidecarIdentityTests.cs
+++ b/tests/Dotsider.Tests/ElfSidecarIdentityTests.cs
@@ -7,6 +7,7 @@ namespace Dotsider.Tests;
/// all-present-signals-must-match rule that decides whether a .dbg sidecar belongs to a
/// stripped image.
///
+[TestClass]
public class ElfSidecarIdentityTests
{
private static readonly byte[] IdA = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
@@ -22,68 +23,74 @@ private static byte[] SidecarWithBuildId(byte[] id) =>
(".debug_info", 0, new byte[] { 1 }));
/// Verifies the standard CRC-32 check value.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Crc32_KnownAnswer()
{
- Assert.Equal(0xCBF43926U, Crc32.Compute("123456789"u8));
- Assert.Equal(0U, Crc32.Compute([]));
+ Assert.AreEqual(0xCBF43926U, Crc32.Compute("123456789"u8));
+ Assert.AreEqual(0U, Crc32.Compute([]));
}
/// Verifies the build-id note parses, including walking past a foreign note first.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryReadBuildId_ParsesGnuNoteBehindForeignNote()
{
var image = SyntheticImageBuilders.BuildElf(
(".note.gnu.build-id", 0, SyntheticImageBuilders.GnuBuildIdNote(IdA, precedeWithForeignNote: true)));
- Assert.True(ElfImageReader.TryReadBuildId(image, out var id));
- Assert.Equal(IdA, id);
+ Assert.IsTrue(ElfImageReader.TryReadBuildId(image, out var id));
+ Assert.AreSequenceEqual(IdA, id);
var plain = SyntheticImageBuilders.BuildElf((".text", 0, new byte[] { 1 }));
- Assert.False(ElfImageReader.TryReadBuildId(plain, out _));
+ Assert.IsFalse(ElfImageReader.TryReadBuildId(plain, out _));
var malformed = SyntheticImageBuilders.BuildElf((".note.gnu.build-id", 0, new byte[] { 1, 2, 3 }));
- Assert.False(ElfImageReader.TryReadBuildId(malformed, out _));
+ Assert.IsFalse(ElfImageReader.TryReadBuildId(malformed, out _));
}
/// Verifies the debuglink reader returns the file name and the 4-aligned CRC.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void TryReadDebugLink_ParsesNameAndAlignedCrc()
{
// "myapp.dbg" + NUL = 10 bytes -> CRC 4-aligned at offset 12.
var image = SyntheticImageBuilders.BuildElf(
(".gnu_debuglink", 0, SyntheticImageBuilders.GnuDebugLink("myapp.dbg", 0x1234_5678)));
- Assert.True(ElfImageReader.TryReadDebugLink(image, out var name, out var crc));
- Assert.Equal("myapp.dbg", name);
- Assert.Equal(0x1234_5678U, crc);
+ Assert.IsTrue(ElfImageReader.TryReadDebugLink(image, out var name, out var crc));
+ Assert.AreEqual("myapp.dbg", name);
+ Assert.AreEqual(0x1234_5678U, crc);
var absent = SyntheticImageBuilders.BuildElf((".text", 0, new byte[] { 1 }));
- Assert.False(ElfImageReader.TryReadDebugLink(absent, out _, out _));
+ Assert.IsFalse(ElfImageReader.TryReadDebugLink(absent, out _, out _));
}
/// Verifies matching build ids accept the sidecar.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Check_BuildIdMatch_Accepts()
{
- Assert.Equal(ElfSidecarMatch.Matched,
+ Assert.AreEqual(ElfSidecarMatch.Matched,
ElfSidecarIdentity.Check(ImageWithBuildId(IdA), SidecarWithBuildId(IdA)));
}
/// Verifies a differing or absent sidecar build id rejects the sidecar.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Check_BuildIdMismatchOrAbsent_Rejects()
{
- Assert.Equal(ElfSidecarMatch.Mismatched,
+ Assert.AreEqual(ElfSidecarMatch.Mismatched,
ElfSidecarIdentity.Check(ImageWithBuildId(IdA), SidecarWithBuildId(IdB)));
var noIdSidecar = SyntheticImageBuilders.BuildElf((".debug_info", 0, new byte[] { 1 }));
- Assert.Equal(ElfSidecarMatch.Mismatched,
+ Assert.AreEqual(ElfSidecarMatch.Mismatched,
ElfSidecarIdentity.Check(ImageWithBuildId(IdA), noIdSidecar));
}
/// Verifies the debuglink CRC alone can accept, and a CRC failure rejects.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Check_DebugLinkCrc_AcceptsOnMatchRejectsOnFailure()
{
var sidecar = SyntheticImageBuilders.BuildElf((".debug_info", 0, new byte[] { 1 }));
@@ -91,18 +98,19 @@ public void Check_DebugLinkCrc_AcceptsOnMatchRejectsOnFailure()
var matching = SyntheticImageBuilders.BuildElf(
(".gnu_debuglink", 0, SyntheticImageBuilders.GnuDebugLink("app.dbg", crc)));
- Assert.Equal(ElfSidecarMatch.Matched, ElfSidecarIdentity.Check(matching, sidecar));
+ Assert.AreEqual(ElfSidecarMatch.Matched, ElfSidecarIdentity.Check(matching, sidecar));
var failing = SyntheticImageBuilders.BuildElf(
(".gnu_debuglink", 0, SyntheticImageBuilders.GnuDebugLink("app.dbg", crc ^ 1)));
- Assert.Equal(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(failing, sidecar));
+ Assert.AreEqual(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(failing, sidecar));
}
///
/// Verifies every present signal must pass: one failing signal rejects even when the other
/// matches, in both directions.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Check_BothSignals_OneFailureRejects()
{
var sidecar = SidecarWithBuildId(IdA);
@@ -112,28 +120,29 @@ static byte[] Image(byte[] id, uint expectedCrc) => SyntheticImageBuilders.Build
(".note.gnu.build-id", 0, SyntheticImageBuilders.GnuBuildIdNote(id)),
(".gnu_debuglink", 0, SyntheticImageBuilders.GnuDebugLink("app.dbg", expectedCrc)));
- Assert.Equal(ElfSidecarMatch.Matched, ElfSidecarIdentity.Check(Image(IdA, crc), sidecar));
- Assert.Equal(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(Image(IdA, crc ^ 1), sidecar));
- Assert.Equal(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(Image(IdB, crc), sidecar));
+ Assert.AreEqual(ElfSidecarMatch.Matched, ElfSidecarIdentity.Check(Image(IdA, crc), sidecar));
+ Assert.AreEqual(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(Image(IdA, crc ^ 1), sidecar));
+ Assert.AreEqual(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(Image(IdB, crc), sidecar));
}
///
/// Verifies a signal-free image accepts only loosely — same machine and real debug info —
/// and rejects when the machine differs or the sidecar lacks .debug_info.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Check_NoSignals_LooseChecksDecide()
{
var image = SyntheticImageBuilders.BuildElf((".text", 0, new byte[] { 1 }));
var sidecar = SyntheticImageBuilders.BuildElf((".debug_info", 0, new byte[] { 1 }));
- Assert.Equal(ElfSidecarMatch.LooseMatch, ElfSidecarIdentity.Check(image, sidecar));
+ Assert.AreEqual(ElfSidecarMatch.LooseMatch, ElfSidecarIdentity.Check(image, sidecar));
var foreignMachine = SyntheticImageBuilders.BuildElf((".debug_info", 0, new byte[] { 1 }));
foreignMachine[18] = 0x28; // EM_ARM
- Assert.Equal(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(image, foreignMachine));
+ Assert.AreEqual(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(image, foreignMachine));
var noDebugInfo = SyntheticImageBuilders.BuildElf((".text", 0, new byte[] { 1 }));
- Assert.Equal(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(image, noDebugInfo));
+ Assert.AreEqual(ElfSidecarMatch.Mismatched, ElfSidecarIdentity.Check(image, noDebugInfo));
}
}
diff --git a/tests/Dotsider.Tests/ElfSymtabReaderTests.cs b/tests/Dotsider.Tests/ElfSymtabReaderTests.cs
index eb6f1a82..04a3a881 100644
--- a/tests/Dotsider.Tests/ElfSymtabReaderTests.cs
+++ b/tests/Dotsider.Tests/ElfSymtabReaderTests.cs
@@ -8,6 +8,7 @@ namespace Dotsider.Tests;
/// STT_OBJECT entries surface with their exact st_size while functions, undefined,
/// unnamed, and reserved-section entries are skipped.
///
+[TestClass]
public class ElfSymtabReaderTests
{
private static byte[] Sym(uint nameOffset, byte type, ushort shndx, ulong value, ulong size)
@@ -32,7 +33,8 @@ private static byte[] Concat(params byte[][] parts)
/// Verifies the object pass: an STT_OBJECT keeps its exact size and maps to its
/// section, while STT_FUNC, undefined, unnamed, and absolute entries are skipped.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadDataSymbols_KeepsNamedObjectsWithExactSizes()
{
var strtab = "\0_ZTV6Widget\0main\0"u8.ToArray();
@@ -52,28 +54,29 @@ public void ReadDataSymbols_KeepsNamedObjectsWithExactSizes()
var symbols = ElfSymtabReader.ReadDataSymbols(image, ElfImageReader.ReadSections(image));
- var s = Assert.Single(symbols);
- Assert.Equal("_ZTV6Widget", s.Name);
- Assert.Equal(0x2010UL, s.VirtualAddress);
- Assert.Equal(0x18, s.Size); // exact st_size, not nearest-next
- Assert.Equal(".data", s.Section);
- Assert.True(s.IsData);
- Assert.NotNull(s.FileOffset);
+ var s = Assert.ContainsSingle(symbols);
+ Assert.AreEqual("_ZTV6Widget", s.Name);
+ Assert.AreEqual(0x2010UL, s.VirtualAddress);
+ Assert.AreEqual(0x18, s.Size); // exact st_size, not nearest-next
+ Assert.AreEqual(".data", s.Section);
+ Assert.IsTrue(s.IsData);
+ Assert.IsNotNull(s.FileOffset);
}
/// Verifies malformed tables yield nothing rather than throwing.
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReadDataSymbols_Malformed_ReturnsEmpty()
{
// No symbol table at all.
var plain = SyntheticImageBuilders.BuildElf((".text", 0x1000, new byte[8]));
- Assert.Empty(ElfSymtabReader.ReadDataSymbols(plain, ElfImageReader.ReadSections(plain)));
+ Assert.IsEmpty(ElfSymtabReader.ReadDataSymbols(plain, ElfImageReader.ReadSections(plain)));
// Link points past the section table.
var symtab = Concat(Sym(0, 0, 0, 0, 0), Sym(1, 1, 1, 0x1000, 8));
var badLink = SyntheticImageBuilders.BuildElf(
(".text", 0x1000, new byte[8], 1u, 0u),
(".symtab", 0, symtab, 2u, 99u));
- Assert.Empty(ElfSymtabReader.ReadDataSymbols(badLink, ElfImageReader.ReadSections(badLink)));
+ Assert.IsEmpty(ElfSymtabReader.ReadDataSymbols(badLink, ElfImageReader.ReadSections(badLink)));
}
}
diff --git a/tests/Dotsider.Tests/EscapeTimeoutPresentationAdapterTests.cs b/tests/Dotsider.Tests/EscapeTimeoutPresentationAdapterTests.cs
index a2177875..21c132d6 100644
--- a/tests/Dotsider.Tests/EscapeTimeoutPresentationAdapterTests.cs
+++ b/tests/Dotsider.Tests/EscapeTimeoutPresentationAdapterTests.cs
@@ -10,9 +10,11 @@ namespace Dotsider.Tests;
///
/// Tests for Escape Timeout Presentation Adapter.
///
-[Collection("SampleAssemblies")]
-public class EscapeTimeoutPresentationAdapterTests(SampleAssemblyFixture samples)
+[TestClass]
+public class EscapeTimeoutPresentationAdapterTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
// ========================================
// Test infrastructure
// ========================================
@@ -113,10 +115,11 @@ public ValueTask DisposeAsync()
///
/// Verifies standalone escape produces escape event.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task StandaloneEscape_ProducesEscapeEvent()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -154,10 +157,11 @@ await TestHelpers.WaitUntilAsync(
///
/// Verifies split escape sequence within timeout produces up arrow.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task SplitEscapeSequence_WithinTimeout_ProducesUpArrow()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -198,10 +202,11 @@ await TestHelpers.WaitUntilAsync(
///
/// Verifies complete escape sequence single read produces up arrow.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CompleteEscapeSequence_SingleRead_ProducesUpArrow()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -241,10 +246,11 @@ await TestHelpers.WaitUntilAsync(
///
/// Verifies coalesced prefix and esc i before escape.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task CoalescedPrefixAndEsc_IBeforeEscape()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -282,16 +288,17 @@ await TestHelpers.WaitUntilAsync(
var eventList = events.ToArray();
var iIndex = Array.IndexOf(eventList, "i");
var escIndex = Array.IndexOf(eventList, "escape");
- Assert.True(iIndex < escIndex, $"Expected 'i' before 'escape', got i={iIndex} esc={escIndex}");
+ Assert.IsLessThan(escIndex, iIndex, $"Expected 'i' before 'escape', got i={iIndex} esc={escIndex}");
}
///
/// Verifies standalone esc then literal bracket not combined into csi.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task StandaloneEsc_ThenLiteralBracket_NotCombinedIntoCsi()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -340,10 +347,11 @@ await TestHelpers.WaitUntilAsync(
///
/// Verifies standalone escape followed by mouse event produces escape event.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task StandaloneEscape_FollowedByMouseEvent_ProducesEscapeEvent()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -388,11 +396,11 @@ await TestHelpers.WaitUntilAsync(
var escIndex = Array.IndexOf(eventList, "escape");
var mouseIndex = Array.IndexOf(eventList, "mouse-click");
var upIndex = Array.IndexOf(eventList, "up");
- Assert.True(escIndex >= 0, "Expected escape event");
- Assert.True(mouseIndex >= 0, "Expected mouse-click event (mouse packet not silently dropped)");
- Assert.True(upIndex >= 0, "Expected up event");
- Assert.True(escIndex < mouseIndex, $"Expected escape before mouse-click, got esc={escIndex} mouse={mouseIndex}");
- Assert.True(mouseIndex < upIndex, $"Expected mouse-click before up, got mouse={mouseIndex} up={upIndex}");
+ Assert.IsGreaterThanOrEqualTo(0, escIndex, "Expected escape event");
+ Assert.IsGreaterThanOrEqualTo(0, mouseIndex, "Expected mouse-click event (mouse packet not silently dropped)");
+ Assert.IsGreaterThanOrEqualTo(0, upIndex, "Expected up event");
+ Assert.IsLessThan(mouseIndex, escIndex, $"Expected escape before mouse-click, got esc={escIndex} mouse={mouseIndex}");
+ Assert.IsLessThan(upIndex, mouseIndex, $"Expected mouse-click before up, got mouse={mouseIndex} up={upIndex}");
}
///
@@ -415,10 +423,11 @@ await TestHelpers.WaitUntilAsync(
/// mouse event therefore proves the overlay is active.
///
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task WithMouse_EnablesMouseCursorOverlay()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
@@ -449,7 +458,7 @@ await TestHelpers.WaitUntilAsync(
() => queued.OutputContains(cursorShow),
TimeSpan.FromSeconds(10));
- Assert.True(queued.OutputContains(cursorShow),
+ Assert.IsTrue(queued.OutputContains(cursorShow),
"Expected cursor-show sequence (\\x1b[?25h) from mouse cursor overlay");
}
@@ -459,10 +468,11 @@ await TestHelpers.WaitUntilAsync(
/// continuation bytes are processed as literal characters. This is inherent to
/// timeout-based escape detection (same tradeoff in xterm, vim, tmux, neovim).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task LateSplitSequence_KnownLimitation_DegradedBehavior()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
var events = new ConcurrentQueue();
@@ -512,10 +522,11 @@ await TestHelpers.WaitUntilAsync(
///
/// Verifies raw escape exits hex insert mode.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task RawEscape_ExitsHexInsertMode()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
DotsiderState? state = null;
@@ -530,7 +541,7 @@ public async Task RawEscape_ExitsHexInsertMode()
options.EnableMouse = true;
}, app => ctx =>
{
- state ??= new DotsiderState(app, samples.HelloWorldDll);
+ state ??= new DotsiderState(app, Samples.HelloWorldDll);
dotsiderApp ??= new DotsiderApp(state);
return dotsiderApp.Build(ctx);
})
@@ -550,8 +561,8 @@ public async Task RawEscape_ExitsHexInsertMode()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(state);
- Assert.Equal(HexEditMode.Insert, state!.HexMode);
+ Assert.IsNotNull(state);
+ Assert.AreEqual(HexEditMode.Insert, state!.HexMode);
// Send raw ESC through the adapter path
queued.EnqueueInput(0x1b);
@@ -561,17 +572,18 @@ await TestHelpers.WaitUntilAsync(
() => state.HexMode == HexEditMode.Normal,
TimeSpan.FromSeconds(10));
- Assert.Equal(HexEditMode.Normal, state.HexMode);
- Assert.True(state.HexEditorState.IsReadOnly);
+ Assert.AreEqual(HexEditMode.Normal, state.HexMode);
+ Assert.IsTrue(state.HexEditorState.IsReadOnly);
}
///
/// Verifies raw escape dismisses editing search.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task RawEscape_DismissesEditingSearch()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
DotsiderState? state = null;
@@ -586,7 +598,7 @@ public async Task RawEscape_DismissesEditingSearch()
options.EnableMouse = true;
}, app => ctx =>
{
- state ??= new DotsiderState(app, samples.HelloWorldDll);
+ state ??= new DotsiderState(app, Samples.HelloWorldDll);
dotsiderApp ??= new DotsiderApp(state);
return dotsiderApp.Build(ctx);
})
@@ -603,7 +615,7 @@ public async Task RawEscape_DismissesEditingSearch()
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(state);
+ Assert.IsNotNull(state);
await TestHelpers.WaitUntilAsync(
() => state!.Search[state.CurrentTab].IsActive && !state.Search[state.CurrentTab].IsConfirmed,
TimeSpan.FromSeconds(10));
@@ -616,16 +628,17 @@ await TestHelpers.WaitUntilAsync(
() => !state!.Search[state.CurrentTab].IsActive,
TimeSpan.FromSeconds(10));
- Assert.False(state.Search[state.CurrentTab].IsActive);
+ Assert.IsFalse(state.Search[state.CurrentTab].IsActive);
}
///
/// Verifies raw escape dismisses confirmed search.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task RawEscape_DismissesConfirmedSearch()
{
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var queued = new QueuedPresentationAdapter();
var escAdapter = new EscapeTimeoutPresentationAdapter(queued, TimeSpan.FromMilliseconds(50));
DotsiderState? state = null;
@@ -640,7 +653,7 @@ public async Task RawEscape_DismissesConfirmedSearch()
options.EnableMouse = true;
}, app => ctx =>
{
- state ??= new DotsiderState(app, samples.HelloWorldDll);
+ state ??= new DotsiderState(app, Samples.HelloWorldDll);
dotsiderApp ??= new DotsiderApp(state);
return dotsiderApp.Build(ctx);
})
@@ -671,7 +684,7 @@ await TestHelpers.WaitUntilAsync(
.Build()
.ApplyAsync(terminal, ct);
- Assert.NotNull(state);
+ Assert.IsNotNull(state);
await TestHelpers.WaitUntilAsync(
() => state!.Search[state.CurrentTab].IsConfirmed,
TimeSpan.FromSeconds(10));
@@ -684,6 +697,6 @@ await TestHelpers.WaitUntilAsync(
() => !state!.Search[state.CurrentTab].IsActive,
TimeSpan.FromSeconds(10));
- Assert.False(state!.Search[state.CurrentTab].IsActive);
+ Assert.IsFalse(state!.Search[state.CurrentTab].IsActive);
}
}
diff --git a/tests/Dotsider.Tests/FrozenObjectReaderTests.cs b/tests/Dotsider.Tests/FrozenObjectReaderTests.cs
index c4a3a658..ced3996e 100644
--- a/tests/Dotsider.Tests/FrozenObjectReaderTests.cs
+++ b/tests/Dotsider.Tests/FrozenObjectReaderTests.cs
@@ -7,20 +7,23 @@ namespace Dotsider.Tests;
/// Tests for frozen string recovery from the Native AOT frozen object region, exercised
/// through .
///
-[Collection("SampleAssemblies")]
-public class FrozenObjectReaderTests(SampleAssemblyFixture samples)
+[TestClass]
+public class FrozenObjectReaderTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Verifies frozen strings are recovered from the sample and include the literal it
/// prints. On Windows and macOS the region is file-backed; on Linux it is rebuilt from
/// the dehydrated data — either way the literals are recovered.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FrozenStrings_NativeAotExe_RecoversLiterals()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
- using var analyzer = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ using var analyzer = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
var frozen = analyzer.FrozenStrings;
@@ -31,34 +34,36 @@ public void FrozenStrings_NativeAotExe_RecoversLiterals()
Assert.Fail($"frozen={frozen.Count}; sections=[{sections}]");
}
- Assert.All(frozen, s => Assert.Equal(StringSource.FrozenObject, s.Source));
+ TestAssert.All(frozen, s => Assert.AreEqual(StringSource.FrozenObject, s.Source));
}
///
/// Verifies recovered frozen strings carry offsets inside the file.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FrozenStrings_NativeAotExe_HaveValidOffsets()
{
- Assert.SkipWhen(samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
+ TestSkip.When(Samples.NativeAotConsoleExe is null, "NativeAOT sample was not built");
- var size = new FileInfo(samples.NativeAotConsoleExe!).Length;
- using var analyzer = new AssemblyAnalyzer(samples.NativeAotConsoleExe!);
+ var size = new FileInfo(Samples.NativeAotConsoleExe!).Length;
+ using var analyzer = new AssemblyAnalyzer(Samples.NativeAotConsoleExe!);
var frozen = analyzer.FrozenStrings;
- Assert.NotEmpty(frozen);
- Assert.All(frozen, s => Assert.InRange(s.Offset, 0, (int)size - 1));
+ Assert.IsNotEmpty(frozen);
+ TestAssert.All(frozen, s => Assert.IsInRange(0, (int)size - 1, s.Offset));
}
///
/// Verifies a managed assembly has no frozen strings.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FrozenStrings_ManagedDll_Empty()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
- Assert.Empty(analyzer.FrozenStrings);
+ Assert.IsEmpty(analyzer.FrozenStrings);
}
}
diff --git a/tests/Dotsider.Tests/HexRowDocumentTests.cs b/tests/Dotsider.Tests/HexRowDocumentTests.cs
index df4ef2bc..c68c1aa0 100644
--- a/tests/Dotsider.Tests/HexRowDocumentTests.cs
+++ b/tests/Dotsider.Tests/HexRowDocumentTests.cs
@@ -5,12 +5,14 @@ namespace Dotsider.Tests;
///
/// Tests for Hex Row Document.
///
+[TestClass]
public class HexRowDocumentTests
{
///
/// Verifies get line text binary content does not throw.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineText_BinaryContent_DoesNotThrow()
{
var bytes = new byte[256];
@@ -25,7 +27,8 @@ public void GetLineText_BinaryContent_DoesNotThrow()
///
/// Verifies get line length binary content does not throw.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineLength_BinaryContent_DoesNotThrow()
{
var bytes = new byte[256];
@@ -40,7 +43,8 @@ public void GetLineLength_BinaryContent_DoesNotThrow()
///
/// Verifies get line text last row binary content.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineText_LastRow_BinaryContent()
{
var bytes = new byte[256];
@@ -51,13 +55,14 @@ public void GetLineText_LastRow_BinaryContent()
// Last row is the worst case for byte-to-char misalignment
var lastRow = hexDoc.LineCount;
var text = hexDoc.GetLineText(lastRow);
- Assert.NotNull(text);
+ Assert.IsNotNull(text);
}
///
/// Verifies get line length consistent with get line text.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineLength_ConsistentWithGetLineText()
{
var bytes = new byte[256];
@@ -69,79 +74,84 @@ public void GetLineLength_ConsistentWithGetLineText()
{
var text = hexDoc.GetLineText(line);
var length = hexDoc.GetLineLength(line);
- Assert.Equal(text.Length, length);
+ Assert.AreEqual(text.Length, length);
}
}
///
/// Verifies get line text empty document returns empty.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineText_EmptyDocument_ReturnsEmpty()
{
var doc = new Hex1bDocument([]);
var hexDoc = new HexRowDocument(doc) { BytesPerRow = 16 };
- Assert.Equal(1, hexDoc.LineCount);
- Assert.Equal("", hexDoc.GetLineText(1));
+ Assert.AreEqual(1, hexDoc.LineCount);
+ Assert.AreEqual("", hexDoc.GetLineText(1));
}
///
/// Verifies get line text single byte.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineText_SingleByte()
{
var doc = new Hex1bDocument([0x42]);
var hexDoc = new HexRowDocument(doc) { BytesPerRow = 16 };
- Assert.Equal(1, hexDoc.LineCount);
+ Assert.AreEqual(1, hexDoc.LineCount);
var text = hexDoc.GetLineText(1);
- Assert.NotEmpty(text);
+ Assert.IsNotEmpty(text);
}
///
/// Verifies line count matches ceil division.
///
- [Theory(Timeout = 30_000)]
- [InlineData(1, 1)]
- [InlineData(15, 1)]
- [InlineData(16, 1)]
- [InlineData(17, 2)]
- [InlineData(255, 16)]
- [InlineData(256, 16)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow(1, 1)]
+ [DataRow(15, 1)]
+ [DataRow(16, 1)]
+ [DataRow(17, 2)]
+ [DataRow(255, 16)]
+ [DataRow(256, 16)]
public void LineCount_MatchesCeilDivision(int byteCount, int expectedLines)
{
var bytes = new byte[byteCount];
var doc = new Hex1bDocument(bytes);
var hexDoc = new HexRowDocument(doc) { BytesPerRow = 16 };
- Assert.Equal(expectedLines, hexDoc.LineCount);
+ Assert.AreEqual(expectedLines, hexDoc.LineCount);
}
///
/// Verifies get line text out of range throws.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineText_OutOfRange_Throws()
{
var doc = new Hex1bDocument(new byte[32]);
var hexDoc = new HexRowDocument(doc) { BytesPerRow = 16 };
- Assert.Throws(() => hexDoc.GetLineText(0));
- Assert.Throws(() => hexDoc.GetLineText(hexDoc.LineCount + 1));
+ Assert.ThrowsExactly(() => hexDoc.GetLineText(0));
+ Assert.ThrowsExactly(() => hexDoc.GetLineText(hexDoc.LineCount + 1));
}
///
/// Verifies get line length out of range throws.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetLineLength_OutOfRange_Throws()
{
var doc = new Hex1bDocument(new byte[32]);
var hexDoc = new HexRowDocument(doc) { BytesPerRow = 16 };
- Assert.Throws(() => hexDoc.GetLineLength(0));
- Assert.Throws(() => hexDoc.GetLineLength(hexDoc.LineCount + 1));
+ Assert.ThrowsExactly(() => hexDoc.GetLineLength(0));
+ Assert.ThrowsExactly(() => hexDoc.GetLineLength(hexDoc.LineCount + 1));
}
}
diff --git a/tests/Dotsider.Tests/HexSaveStressTests.cs b/tests/Dotsider.Tests/HexSaveStressTests.cs
index 70867b99..5e9d0123 100644
--- a/tests/Dotsider.Tests/HexSaveStressTests.cs
+++ b/tests/Dotsider.Tests/HexSaveStressTests.cs
@@ -10,9 +10,11 @@ namespace Dotsider.Tests;
///
/// Tests for Hex Save Stress.
///
-[Collection("SampleAssemblies")]
-public class HexSaveStressTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class HexSaveStressTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -61,19 +63,20 @@ private static Hex1bTerminalInputSequenceBuilder EditSafeByte(Hex1bTerminalInput
///
/// Verifies locked file falls back to tmp path.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task LockedFile_FallsBackToTmpPath()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
var tempDll = Path.Combine(tempDir, "HelloWorld.dll");
- File.Copy(samples.HelloWorldDll, tempDll);
+ File.Copy(Samples.HelloWorldDll, tempDll);
FileStream? fileLock = null;
try
{
var (terminal, app) = CreateDotsiderApp(tempDll);
- var ct = TestContext.Current.CancellationToken;
+ var ct = CancellationToken.None;
var runTask = app.RunAsync(ct);
await Task.Delay(100, ct);
@@ -110,9 +113,9 @@ await EditSafeByte(new Hex1bTerminalInputSequenceBuilder())
.Build()
.ApplyAsync(terminal, ct);
- Assert.Contains("could not overwrite original", _state!.HexNotification);
- Assert.True(File.Exists(tempDll + ".tmp"), ".tmp file should remain after fallback");
- Assert.False(_state.HexIsDirty);
+ Assert.Contains("could not overwrite original", _state!.HexNotification!);
+ Assert.IsTrue(File.Exists(tempDll + ".tmp"), ".tmp file should remain after fallback");
+ Assert.IsFalse(_state.HexIsDirty);
await runTask.ContinueWith(_ => { }, ct);
}
@@ -135,18 +138,19 @@ await EditSafeByte(new Hex1bTerminalInputSequenceBuilder())
///
/// Verifies invalid edit rejects corrupted pe.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task InvalidEdit_RejectsCorruptedPe()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
var tempDll = Path.Combine(tempDir, "HelloWorld.dll");
- File.Copy(samples.HelloWorldDll, tempDll);
+ File.Copy(Samples.HelloWorldDll, tempDll);
try
{
var (terminal, app) = CreateDotsiderApp(tempDll);
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var runTask = app.RunAsync(cts.Token);
await Task.Delay(100, cts.Token);
@@ -169,11 +173,11 @@ public async Task InvalidEdit_RejectsCorruptedPe()
.ApplyAsync(terminal, cts.Token);
await Task.Delay(100, cts.Token);
- Assert.Contains("invalid image", _state!.HexNotification);
- Assert.False(File.Exists(tempDll + ".tmp"), ".tmp should be cleaned up after validation failure");
- Assert.True(_state.HexIsDirty, "Document should still be dirty after failed save");
+ Assert.Contains("invalid image", _state!.HexNotification!);
+ Assert.IsFalse(File.Exists(tempDll + ".tmp"), ".tmp should be cleaned up after validation failure");
+ Assert.IsTrue(_state.HexIsDirty, "Document should still be dirty after failed save");
// Analyzer should still be functional (never disposed during Phase 1 failure)
- Assert.Equal(tempDll, _state.Analyzer.FilePath);
+ Assert.AreEqual(tempDll, _state.Analyzer.FilePath);
cts.Cancel();
await runTask;
@@ -187,18 +191,19 @@ public async Task InvalidEdit_RejectsCorruptedPe()
///
/// Verifies double save no dirty state after first.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DoubleSave_NoDirtyStateAfterFirst()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
var tempDll = Path.Combine(tempDir, "HelloWorld.dll");
- File.Copy(samples.HelloWorldDll, tempDll);
+ File.Copy(Samples.HelloWorldDll, tempDll);
try
{
var (terminal, app) = CreateDotsiderApp(tempDll);
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var runTask = app.RunAsync(cts.Token);
await Task.Delay(100, cts.Token);
@@ -213,9 +218,9 @@ await EditSafeByte(new Hex1bTerminalInputSequenceBuilder())
.ApplyAsync(terminal, cts.Token);
await Task.Delay(100, cts.Token);
- Assert.False(_state!.HexIsDirty, "Should not be dirty after save");
- Assert.Equal(HexEditMode.Normal, _state.HexMode);
- Assert.Contains("written", _state.HexNotification);
+ Assert.IsFalse(_state!.HexIsDirty, "Should not be dirty after save");
+ Assert.AreEqual(HexEditMode.Normal, _state.HexMode);
+ Assert.Contains("written", _state.HexNotification!);
// Send another Ctrl+S — the binding guard (HexIsDirty) prevents it.
// Use E (toggle endianness) as a sentinel: if the app processes E,
@@ -231,8 +236,8 @@ await EditSafeByte(new Hex1bTerminalInputSequenceBuilder())
.ApplyAsync(terminal, cts.Token);
await Task.Delay(100, cts.Token);
- Assert.Null(_state.HexNotification);
- Assert.False(File.Exists(tempDll + ".tmp"), "No .tmp should be created on non-dirty save");
+ Assert.IsNull(_state.HexNotification);
+ Assert.IsFalse(File.Exists(tempDll + ".tmp"), "No .tmp should be created on non-dirty save");
cts.Cancel();
await runTask;
@@ -246,7 +251,8 @@ await EditSafeByte(new Hex1bTerminalInputSequenceBuilder())
///
/// Verifies native binary hex save succeeds.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task NativeBinary_HexSave_Succeeds()
{
// Copy ONLY the apphost (no companion DLL) so there's no apphost
@@ -254,12 +260,12 @@ public async Task NativeBinary_HexSave_Succeeds()
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
var tempFile = Path.Combine(tempDir, "HelloWorld");
- File.Copy(samples.HelloWorldExe, tempFile);
+ File.Copy(Samples.HelloWorldExe, tempFile);
try
{
var (terminal, app) = CreateDotsiderApp(tempFile);
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var runTask = app.RunAsync(cts.Token);
await Task.Delay(100, cts.Token);
@@ -281,8 +287,8 @@ public async Task NativeBinary_HexSave_Succeeds()
.Build()
.ApplyAsync(terminal, cts.Token);
- Assert.Contains("written", _state!.HexNotification);
- Assert.False(_state.HexIsDirty);
+ Assert.Contains("written", _state!.HexNotification!);
+ Assert.IsFalse(_state.HexIsDirty);
cts.Cancel();
await runTask;
@@ -296,57 +302,58 @@ public async Task NativeBinary_HexSave_Succeeds()
///
/// Verifies in memory analyzer functions without disk.
///
- [Fact]
+ [TestMethod]
public void InMemoryAnalyzer_FunctionsWithoutDisk()
{
// Unit test for the in-memory fallback path that SaveHexChanges uses
// when all disk candidates are exhausted
- var originalBytes = File.ReadAllBytes(samples.HelloWorldDll);
+ var originalBytes = File.ReadAllBytes(Samples.HelloWorldDll);
var fakePath = "/nonexistent/path/HelloWorld.dll";
using var analyzer = new AssemblyAnalyzer(originalBytes, fakePath);
- Assert.Equal(fakePath, analyzer.FilePath);
- Assert.Equal("HelloWorld.dll", analyzer.FileName);
- Assert.Equal(originalBytes.Length, analyzer.FileSize);
- Assert.True(analyzer.HasMetadata);
- Assert.NotNull(analyzer.AssemblyName);
- Assert.True(analyzer.RawBytes.Length > 0);
+ Assert.AreEqual(fakePath, analyzer.FilePath);
+ Assert.AreEqual("HelloWorld.dll", analyzer.FileName);
+ Assert.AreEqual(originalBytes.Length, analyzer.FileSize);
+ Assert.IsTrue(analyzer.HasMetadata);
+ Assert.IsNotNull(analyzer.AssemblyName);
+ Assert.IsGreaterThan(0, analyzer.RawBytes.Length);
}
///
/// Verifies in memory analyzer native binary save recovery path.
///
- [Fact]
+ [TestMethod]
public void InMemoryAnalyzer_NativeBinary_SaveRecoveryPath()
{
// Simulates the byte-array fallback in SaveHexChanges: after a hex
// edit on a native binary (apphost/NativeAOT), all disk candidates
// are exhausted and the analyzer is reconstructed from memory.
- var originalBytes = File.ReadAllBytes(samples.HelloWorldExe);
+ var originalBytes = File.ReadAllBytes(Samples.HelloWorldExe);
// Simulate a hex edit: modify a byte in the native binary
var editedBytes = originalBytes.ToArray();
editedBytes[4] = 0xFF;
- using var analyzer = new AssemblyAnalyzer(editedBytes, samples.HelloWorldExe);
+ using var analyzer = new AssemblyAnalyzer(editedBytes, Samples.HelloWorldExe);
- Assert.Equal(samples.HelloWorldExe, analyzer.FilePath);
- Assert.Equal(editedBytes.Length, analyzer.FileSize);
- Assert.False(analyzer.HasMetadata);
- Assert.False(analyzer.RawBytes.IsEmpty);
- Assert.Equal(0xFF, analyzer.RawBytes.Span[4]);
+ Assert.AreEqual(Samples.HelloWorldExe, analyzer.FilePath);
+ Assert.AreEqual(editedBytes.Length, analyzer.FileSize);
+ Assert.IsFalse(analyzer.HasMetadata);
+ Assert.IsFalse(analyzer.RawBytes.IsEmpty);
+ Assert.AreEqual(0xFF, analyzer.RawBytes.Span[4]);
}
///
/// Verifies reopen or fallback all candidates fail returns in memory analyzer.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ReopenOrFallback_AllCandidatesFail_ReturnsInMemoryAnalyzer()
{
// Exercises the candidate loop + byte-array fallback with all
// non-existent paths so the in-memory branch triggers.
- var originalBytes = File.ReadAllBytes(samples.HelloWorldExe);
+ var originalBytes = File.ReadAllBytes(Samples.HelloWorldExe);
var editedBytes = originalBytes.ToArray();
editedBytes[4] = 0xFF;
@@ -358,22 +365,23 @@ public void ReopenOrFallback_AllCandidatesFail_ReturnsInMemoryAnalyzer()
];
var (analyzer, resolvedPath) = DotsiderApp.ReopenOrFallback(
- bogusPath, editedBytes, samples.HelloWorldExe);
+ bogusPath, editedBytes, Samples.HelloWorldExe);
using (analyzer)
{
- Assert.Null(resolvedPath);
- Assert.Equal(samples.HelloWorldExe, analyzer.FilePath);
- Assert.False(analyzer.HasMetadata);
- Assert.Equal(editedBytes.Length, analyzer.FileSize);
- Assert.Equal(0xFF, analyzer.RawBytes.Span[4]);
+ Assert.IsNull(resolvedPath);
+ Assert.AreEqual(Samples.HelloWorldExe, analyzer.FilePath);
+ Assert.IsFalse(analyzer.HasMetadata);
+ Assert.AreEqual(editedBytes.Length, analyzer.FileSize);
+ Assert.AreEqual(0xFF, analyzer.RawBytes.Span[4]);
}
}
///
/// Verifies save hex changes native binary memory fallback sets notification.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SaveHexChanges_NativeBinary_MemoryFallbackSetsNotification()
{
// Drives SaveHexChanges through the resolvedPath == null branch by
@@ -382,7 +390,7 @@ public void SaveHexChanges_NativeBinary_MemoryFallbackSetsNotification()
var tempDir = Path.Combine(Path.GetTempPath(), $"dotsider-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
var tempFile = Path.Combine(tempDir, "HelloWorld");
- File.Copy(samples.HelloWorldExe, tempFile);
+ File.Copy(Samples.HelloWorldExe, tempFile);
try
{
@@ -402,16 +410,16 @@ public void SaveHexChanges_NativeBinary_MemoryFallbackSetsNotification()
_state.HexEditorState.Document.ApplyBytes(
new ByteReplaceOperation(4, 1, [0xFF]));
_state.HexEditorState.IsReadOnly = true;
- Assert.True(_state.HexIsDirty);
+ Assert.IsTrue(_state.HexIsDirty);
// Inject a reopener that simulates all disk candidates failing
DotsiderApp.SaveHexChanges(_state,
reopener: (_, bytes, path) => (new AssemblyAnalyzer(bytes, path), null));
- Assert.Equal("Saved (working from memory — file may be locked)", _state.HexNotification);
- Assert.False(_state.HexIsDirty);
- Assert.False(_state.Analyzer.HasMetadata);
- Assert.Equal(0xFF, _state.Analyzer.RawBytes.Span[4]);
+ Assert.AreEqual("Saved (working from memory — file may be locked)", _state.HexNotification);
+ Assert.IsFalse(_state.HexIsDirty);
+ Assert.IsFalse(_state.Analyzer.HasMetadata);
+ Assert.AreEqual(0xFF, _state.Analyzer.RawBytes.Span[4]);
}
finally
{
@@ -424,10 +432,10 @@ public void SaveHexChanges_NativeBinary_MemoryFallbackSetsNotification()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/HexSearchTests.cs b/tests/Dotsider.Tests/HexSearchTests.cs
index 645e47e0..2e9cc2c7 100644
--- a/tests/Dotsider.Tests/HexSearchTests.cs
+++ b/tests/Dotsider.Tests/HexSearchTests.cs
@@ -6,156 +6,169 @@ namespace Dotsider.Tests;
/// Tests for hex dump search: ASCII byte search, hex pattern parsing,
/// multiple match offsets, and edge cases.
///
+[TestClass]
public class HexSearchTests
{
///
/// Verifies parse hex pattern valid bytes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ParseHexPattern_ValidBytes()
{
var (bytes, error) = HexDumpView.ParseHexPattern("FF D8 FF E0");
- Assert.Null(error);
- Assert.NotNull(bytes);
- Assert.Equal(4, bytes!.Length);
- Assert.Equal(0xFF, bytes[0]);
- Assert.Equal(0xD8, bytes[1]);
- Assert.Equal(0xFF, bytes[2]);
- Assert.Equal(0xE0, bytes[3]);
+ Assert.IsNull(error);
+ Assert.IsNotNull(bytes);
+ Assert.HasCount(4, bytes!);
+ Assert.AreEqual(0xFF, bytes[0]);
+ Assert.AreEqual(0xD8, bytes[1]);
+ Assert.AreEqual(0xFF, bytes[2]);
+ Assert.AreEqual(0xE0, bytes[3]);
}
///
/// Verifies parse hex pattern no spaces.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ParseHexPattern_NoSpaces()
{
var (bytes, error) = HexDumpView.ParseHexPattern("FFD8FFE0");
- Assert.Null(error);
- Assert.NotNull(bytes);
- Assert.Equal(4, bytes!.Length);
+ Assert.IsNull(error);
+ Assert.IsNotNull(bytes);
+ Assert.HasCount(4, bytes!);
}
///
/// Verifies parse hex pattern odd digits error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ParseHexPattern_OddDigits_Error()
{
var (bytes, error) = HexDumpView.ParseHexPattern("FD8");
- Assert.Null(bytes);
- Assert.Equal("Invalid hex: odd number of digits", error);
+ Assert.IsNull(bytes);
+ Assert.AreEqual("Invalid hex: odd number of digits", error);
}
///
/// Verifies parse hex pattern invalid chars error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ParseHexPattern_InvalidChars_Error()
{
var (bytes, error) = HexDumpView.ParseHexPattern("GGZZ");
- Assert.Null(bytes);
- Assert.Equal("Invalid hex pattern", error);
+ Assert.IsNull(bytes);
+ Assert.AreEqual("Invalid hex pattern", error);
}
///
/// Verifies parse hex pattern empty error.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ParseHexPattern_Empty_Error()
{
var (bytes, error) = HexDumpView.ParseHexPattern("");
- Assert.Null(bytes);
- Assert.Equal("Invalid hex: empty pattern", error);
+ Assert.IsNull(bytes);
+ Assert.AreEqual("Invalid hex: empty pattern", error);
}
///
/// Verifies find byte pattern single match.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_SingleMatch()
{
byte[] data = [0x01, 0x02, 0x03, 0x04, 0x05];
byte[] pattern = [0x03, 0x04];
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Single(offsets);
- Assert.Equal(2, offsets[0]);
+ Assert.ContainsSingle(offsets);
+ Assert.AreEqual(2, offsets[0]);
}
///
/// Verifies find byte pattern multiple matches.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_MultipleMatches()
{
byte[] data = [0xAA, 0xBB, 0xAA, 0xBB, 0xAA, 0xBB];
byte[] pattern = [0xAA, 0xBB];
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Equal(3, offsets.Count);
- Assert.Equal(0, offsets[0]);
- Assert.Equal(2, offsets[1]);
- Assert.Equal(4, offsets[2]);
+ Assert.HasCount(3, offsets);
+ Assert.AreEqual(0, offsets[0]);
+ Assert.AreEqual(2, offsets[1]);
+ Assert.AreEqual(4, offsets[2]);
}
///
/// Verifies find byte pattern no match.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_NoMatch()
{
byte[] data = [0x01, 0x02, 0x03];
byte[] pattern = [0xFF, 0xFE];
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Empty(offsets);
+ Assert.IsEmpty(offsets);
}
///
/// Verifies find byte pattern empty data.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_EmptyData()
{
byte[] data = [];
byte[] pattern = [0xFF];
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Empty(offsets);
+ Assert.IsEmpty(offsets);
}
///
/// Verifies find byte pattern empty pattern.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_EmptyPattern()
{
byte[] data = [0x01, 0x02];
byte[] pattern = [];
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Empty(offsets);
+ Assert.IsEmpty(offsets);
}
///
/// Verifies find byte pattern pattern longer than data.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_PatternLongerThanData()
{
byte[] data = [0x01];
byte[] pattern = [0x01, 0x02, 0x03];
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Empty(offsets);
+ Assert.IsEmpty(offsets);
}
///
/// Verifies find byte pattern ascii text search.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FindBytePattern_AsciiTextSearch()
{
byte[] data = System.Text.Encoding.ASCII.GetBytes("Hello World Hello");
byte[] pattern = System.Text.Encoding.ASCII.GetBytes("Hello");
var offsets = HexDumpView.FindBytePattern(data, pattern);
- Assert.Equal(2, offsets.Count);
- Assert.Equal(0, offsets[0]);
- Assert.Equal(12, offsets[1]);
+ Assert.HasCount(2, offsets);
+ Assert.AreEqual(0, offsets[0]);
+ Assert.AreEqual(12, offsets[1]);
}
}
diff --git a/tests/Dotsider.Tests/IlColorizerTests.cs b/tests/Dotsider.Tests/IlColorizerTests.cs
index aad18824..cbfb2aea 100644
--- a/tests/Dotsider.Tests/IlColorizerTests.cs
+++ b/tests/Dotsider.Tests/IlColorizerTests.cs
@@ -6,6 +6,7 @@ namespace Dotsider.Tests;
///
/// Tests for IL disassembly syntax coloring.
///
+[TestClass]
public class IlColorizerTests
{
private const string Reset = "\x1b[0m";
@@ -22,30 +23,33 @@ public class IlColorizerTests
///
/// Verifies empty line returns unchanged.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EmptyLine_ReturnsUnchanged()
{
- Assert.Equal("", IlColorizer.ColorizeLine(""));
+ Assert.AreEqual("", IlColorizer.ColorizeLine(""));
}
///
/// Verifies whitespace line returns unchanged.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void WhitespaceLine_ReturnsUnchanged()
{
var line = " ";
- Assert.Equal(line, IlColorizer.ColorizeLine(line));
+ Assert.AreEqual(line, IlColorizer.ColorizeLine(line));
}
///
/// Verifies unrecognized line passes through.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void UnrecognizedLine_PassesThrough()
{
var line = "some other text";
- Assert.Equal(line, IlColorizer.ColorizeLine(line));
+ Assert.AreEqual(line, IlColorizer.ColorizeLine(line));
}
// --- Comment coloring ---
@@ -53,33 +57,36 @@ public void UnrecognizedLine_PassesThrough()
///
/// Verifies comment wrapped in comment color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Comment_WrappedInCommentColor()
{
var line = "// Method: Foo::Bar";
var result = IlColorizer.ColorizeLine(line);
- Assert.Equal($"{CommentFg}{line}{Reset}", result);
+ Assert.AreEqual($"{CommentFg}{line}{Reset}", result);
}
///
/// Verifies indented comment wrapped in comment color.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void IndentedComment_WrappedInCommentColor()
{
var line = " // RVA: 0x00002050";
var result = IlColorizer.ColorizeLine(line);
- Assert.Equal($"{CommentFg}{line}{Reset}", result);
+ Assert.AreEqual($"{CommentFg}{line}{Reset}", result);
}
///
/// Verifies locals init directive is colored without coloring the rest of the line.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void LocalsInit_ColorsDirectiveOnly()
{
var result = IlColorizer.ColorizeLine(" .locals init (");
- Assert.Equal($" {DirectiveFg}.locals init{Reset} (", result);
+ Assert.AreEqual($" {DirectiveFg}.locals init{Reset} (", result);
}
// --- Instruction coloring ---
@@ -87,17 +94,19 @@ public void LocalsInit_ColorsDirectiveOnly()
///
/// Verifies opcode only colors address and opcode.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void OpcodeOnly_ColorsAddressAndOpcode()
{
var result = IlColorizer.ColorizeLine("IL_0005: ret");
- Assert.Equal($"{AddressFg}IL_0005:{Reset} {OpcodeFg}ret{Reset}", result);
+ Assert.AreEqual($"{AddressFg}IL_0005:{Reset} {OpcodeFg}ret{Reset}", result);
}
///
/// Verifies opcode with non string operand preserves operand uncolored.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void OpcodeWithNonStringOperand_PreservesOperandUncolored()
{
var result = IlColorizer.ColorizeLine("IL_000B: call System.Console::WriteLine(string)");
@@ -110,7 +119,8 @@ public void OpcodeWithNonStringOperand_PreservesOperandUncolored()
///
/// Verifies branch target preserves target label.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void BranchTarget_PreservesTargetLabel()
{
var result = IlColorizer.ColorizeLine("IL_000A: br.s IL_0010");
@@ -121,7 +131,8 @@ public void BranchTarget_PreservesTargetLabel()
///
/// Verifies numeric operand preserves uncolored.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NumericOperand_PreservesUncolored()
{
var result = IlColorizer.ColorizeLine("IL_0002: ldc.i4.s 42");
@@ -135,7 +146,8 @@ public void NumericOperand_PreservesUncolored()
///
/// Verifies string operand colored green.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void StringOperand_ColoredGreen()
{
var result = IlColorizer.ColorizeLine("IL_0006: ldstr \"hello world\"");
@@ -147,19 +159,21 @@ public void StringOperand_ColoredGreen()
///
/// Verifies escaped quotes stay single segment.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscapedQuotes_StaySingleSegment()
{
var result = IlColorizer.ColorizeLine("IL_0006: ldstr \"say \\\"hi\\\"\"");
Assert.Contains(StringFg, result);
// Should open StringFg exactly once for the whole quoted segment
- Assert.Equal(1, CountOccurrences(result, StringFg));
+ Assert.AreEqual(1, CountOccurrences(result, StringFg));
}
///
/// Verifies unmatched quote ends with reset.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void UnmatchedQuote_EndsWithReset()
{
var result = IlColorizer.ColorizeLine("IL_0006: ldstr \"unclosed");
@@ -172,49 +186,53 @@ public void UnmatchedQuote_EndsWithReset()
///
/// Verifies malformed il line no separator passes through.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void MalformedIlLine_NoSeparator_PassesThrough()
{
var line = "IL_NOSEPARATOR";
var result = IlColorizer.ColorizeLine(line);
- Assert.Equal(line, result);
+ Assert.AreEqual(line, result);
}
///
/// Verifies empty body after separator colors address only.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EmptyBodyAfterSeparator_ColorsAddressOnly()
{
var result = IlColorizer.ColorizeLine("IL_0000:");
// No ": " separator (needs colon+space), so treated as malformed
- Assert.Equal("IL_0000:", result);
+ Assert.AreEqual("IL_0000:", result);
}
///
/// Verifies all resets paired instruction line.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllResetsPaired_InstructionLine()
{
var result = IlColorizer.ColorizeLine("IL_0001: call System.Object::.ctor()");
// Each colored span (address, opcode) should have a matching reset
var ansiOpens = CountOccurrences(result, "\x1b[38;");
var resets = CountOccurrences(result, Reset);
- Assert.Equal(ansiOpens, resets);
+ Assert.AreEqual(ansiOpens, resets);
}
///
/// Verifies all resets paired string operand.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllResetsPaired_StringOperand()
{
var result = IlColorizer.ColorizeLine("IL_0006: ldstr \"test\"");
// address + opcode + string = 3 colored spans, 3 resets
var ansiOpens = CountOccurrences(result, "\x1b[38;");
var resets = CountOccurrences(result, Reset);
- Assert.Equal(ansiOpens, resets);
+ Assert.AreEqual(ansiOpens, resets);
}
private static int CountOccurrences(string text, string pattern)
diff --git a/tests/Dotsider.Tests/IlDisassemblerTests.cs b/tests/Dotsider.Tests/IlDisassemblerTests.cs
index 99dcdc2d..aa484a68 100644
--- a/tests/Dotsider.Tests/IlDisassemblerTests.cs
+++ b/tests/Dotsider.Tests/IlDisassemblerTests.cs
@@ -6,46 +6,51 @@ namespace Dotsider.Tests;
///
/// Tests for Il Disassembler.
///
-[Collection("SampleAssemblies")]
-public class IlDisassemblerTests(SampleAssemblyFixture samples)
+[TestClass]
+public class IlDisassemblerTests
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
///
/// Verifies hello world main method contains call and ret.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void HelloWorld_MainMethod_ContainsCallAndRet()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var disasm = new IlDisassembler(a);
var mainMethod = a.MethodDefs.FirstOrDefault(m => m.Name == "$" || m.Name == "Main");
- Assert.NotNull(mainMethod);
+ Assert.IsNotNull(mainMethod);
var instructions = disasm.Disassemble(mainMethod);
- Assert.NotEmpty(instructions);
- Assert.Contains(instructions, i => i.OpCode.Contains("ret"));
+ Assert.IsNotEmpty(instructions);
+ Assert.Contains(i => i.OpCode.Contains("ret"), instructions);
}
///
/// Verifies rich library user service add disassembles successfully.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_UserServiceAdd_DisassemblesSuccessfully()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.FirstOrDefault(m =>
m.DeclaringType == "RichLibrary.Services.UserService" && m.Name == "Add");
- Assert.NotNull(method);
+ Assert.IsNotNull(method);
var instructions = disasm.Disassemble(method);
- Assert.NotEmpty(instructions);
+ Assert.IsNotEmpty(instructions);
}
///
/// Verifies formatted IL includes portable PDB annotations.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_UserServiceAdd_FormatIncludesPortablePdbAnnotations()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
var method = FindMethod(a, "RichLibrary.Services.UserService", "Add");
@@ -64,39 +69,40 @@ public void RichLibrary_UserServiceAdd_FormatIncludesPortablePdbAnnotations()
///
/// Verifies decoded IL instructions carry sequence point and local variable metadata.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_UserServiceAdd_InstructionsIncludeDebugMetadata()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
var method = FindMethod(a, "RichLibrary.Services.UserService", "Add");
var result = disasm.DisassembleWithText(method);
- Assert.NotNull(result);
- Assert.Contains(result.Value.Instructions,
- instruction => instruction.SequenceDocument?.EndsWith("UserService.cs",
+ Assert.IsNotNull(result);
+ Assert.Contains(instruction => instruction.SequenceDocument?.EndsWith("UserService.cs",
StringComparison.OrdinalIgnoreCase) == true
- && instruction.SourceLinkUrl is not null);
- Assert.Contains(result.Value.Instructions, instruction => instruction.LocalName == "id");
- Assert.Contains(result.Value.Instructions, instruction => instruction.LocalName == "user");
- Assert.All(result.Value.Instructions,
- instruction => Assert.True(instruction.DisplayLine is null or > 0));
+ && instruction.SourceLinkUrl is not null, result.Value.Instructions);
+ Assert.Contains(instruction => instruction.LocalName == "id", result.Value.Instructions);
+ Assert.Contains(instruction => instruction.LocalName == "user", result.Value.Instructions);
+ TestAssert.All(result.Value.Instructions,
+ instruction => Assert.IsTrue(instruction.DisplayLine is null or > 0));
}
///
/// Verifies formatted IL prints one Source Link marker per distinct URL.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_UserServiceAdd_FormatDeduplicatesSourceLinkMarkers()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
var method = FindMethod(a, "RichLibrary.Services.UserService", "Add");
var result = disasm.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
var markerCount = result.Value.Text
.Split('\n')
.Count(line => line.Contains("[source link]", StringComparison.Ordinal));
@@ -107,14 +113,15 @@ public void RichLibrary_UserServiceAdd_FormatDeduplicatesSourceLinkMarkers()
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
- Assert.True(distinctUrlCount > 0);
- Assert.Equal(distinctUrlCount, markerCount);
+ Assert.IsGreaterThan(0, distinctUrlCount);
+ Assert.AreEqual(distinctUrlCount, markerCount);
}
///
/// Verifies hidden sequence points do not consume the first visible Source Link marker.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Dotsider_IlInspectorViewMethod_HiddenPointDoesNotConsumeSourceLinkMarker()
{
using var a = new AssemblyAnalyzer(typeof(DotsiderApp).Assembly.Location);
@@ -123,175 +130,186 @@ public void Dotsider_IlInspectorViewMethod_HiddenPointDoesNotConsumeSourceLinkMa
var result = disasm.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
var lines = result.Value.Text.Split('\n');
- Assert.Contains(lines, line => line == "// (hidden)");
+ Assert.Contains(line => line == "// (hidden)", lines);
var firstVisibleSourceLine = lines.First(line =>
line.Contains("IlInspectorView.cs(", StringComparison.Ordinal));
Assert.Contains("[source link]", firstVisibleSourceLine);
- Assert.DoesNotContain("[source link]", lines.First(line => line == "// (hidden)"));
+ Assert.DoesNotContain(lines.First(line => line == "// (hidden)"), "[source link]");
}
///
/// Verifies native lib unsafe method has distinct opcodes.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeLib_UnsafeMethod_HasDistinctOpcodes()
{
- using var a = new AssemblyAnalyzer(samples.NativeLibDll);
+ using var a = new AssemblyAnalyzer(Samples.NativeLibDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.FirstOrDefault(m => m.Name == "SumWithPointers");
- Assert.NotNull(method);
+ Assert.IsNotNull(method);
var instructions = disasm.Disassemble(method);
- Assert.NotEmpty(instructions);
+ Assert.IsNotEmpty(instructions);
}
///
/// Verifies format disassembly returns readable text.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FormatDisassembly_ReturnsReadableText()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.First(m => m.Rva != 0);
var text = disasm.FormatDisassembly(method);
- Assert.NotNull(text);
- Assert.NotEmpty(text);
+ Assert.IsNotNull(text);
+ Assert.IsNotEmpty(text);
Assert.Contains("IL_", text);
}
///
/// Verifies all rich library methods disassemble without throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void AllRichLibraryMethods_DisassembleWithoutThrowing()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
foreach (var method in a.MethodDefs.Where(m => m.Rva != 0))
{
var instructions = disasm.Disassemble(method);
- Assert.NotNull(instructions);
+ Assert.IsNotNull(instructions);
}
}
///
/// Verifies method with no body returns empty list.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void MethodWithNoBody_ReturnsEmptyList()
{
- using var a = new AssemblyAnalyzer(samples.NativeLibDll);
+ using var a = new AssemblyAnalyzer(Samples.NativeLibDll);
var disasm = new IlDisassembler(a);
// P/Invoke methods have Rva == 0, no IL body
var externMethod = a.MethodDefs.FirstOrDefault(m => m.Rva == 0);
if (externMethod is not null)
{
var instructions = disasm.Disassemble(externMethod);
- Assert.Empty(instructions);
+ Assert.IsEmpty(instructions);
}
}
///
/// Verifies empty lib can construct no methods to disassemble.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EmptyLib_CanConstruct_NoMethodsToDisassemble()
{
- using var a = new AssemblyAnalyzer(samples.EmptyLibDll);
+ using var a = new AssemblyAnalyzer(Samples.EmptyLibDll);
var disasm = new IlDisassembler(a);
var methodsWithIl = a.MethodDefs.Where(m => m.Rva != 0).ToList();
// Either no methods or only compiler-generated
- Assert.True(methodsWithIl.Count <= 2);
+ Assert.IsLessThanOrEqualTo(2, methodsWithIl.Count);
}
///
/// Verifies complex app async method disassembles successfully.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ComplexApp_AsyncMethod_DisassemblesSuccessfully()
{
- using var a = new AssemblyAnalyzer(samples.ComplexAppDll);
+ using var a = new AssemblyAnalyzer(Samples.ComplexAppDll);
var disasm = new IlDisassembler(a);
var asyncMethods = a.MethodDefs.Where(m => m.Name.Contains("MoveNext")).ToList();
if (asyncMethods.Count > 0)
{
var instructions = disasm.Disassemble(asyncMethods[0]);
- Assert.NotEmpty(instructions);
+ Assert.IsNotEmpty(instructions);
}
}
///
/// Verifies minimal api methods disassemble without throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void MinimalApi_Methods_DisassembleWithoutThrowing()
{
- using var a = new AssemblyAnalyzer(samples.MinimalApiDll);
+ using var a = new AssemblyAnalyzer(Samples.MinimalApiDll);
var disasm = new IlDisassembler(a);
foreach (var method in a.MethodDefs.Where(m => m.Rva != 0).Take(20))
{
var instructions = disasm.Disassemble(method);
- Assert.NotNull(instructions);
+ Assert.IsNotNull(instructions);
}
}
///
/// Verifies disassemble instruction has offset.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Disassemble_InstructionHasOffset()
{
- using var a = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var a = new AssemblyAnalyzer(Samples.HelloWorldDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.First(m => m.Rva != 0);
var instructions = disasm.Disassemble(method);
- Assert.NotEmpty(instructions);
+ Assert.IsNotEmpty(instructions);
// First instruction should be at offset 0
- Assert.Equal(0, instructions[0].Offset);
+ Assert.AreEqual(0, instructions[0].Offset);
}
///
/// Verifies native lib stack alloc sum has instructions.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NativeLib_StackAllocSum_HasInstructions()
{
- using var a = new AssemblyAnalyzer(samples.NativeLibDll);
+ using var a = new AssemblyAnalyzer(Samples.NativeLibDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.FirstOrDefault(m => m.Name == "StackAllocSum");
- Assert.NotNull(method);
+ Assert.IsNotNull(method);
var instructions = disasm.Disassemble(method);
- Assert.NotEmpty(instructions);
+ Assert.IsNotEmpty(instructions);
}
///
/// Verifies instruction has op code and operand.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Instruction_HasOpCodeAndOperand()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.First(m => m.Rva != 0 && m.Name != ".ctor");
var instructions = disasm.Disassemble(method);
- Assert.NotEmpty(instructions);
+ Assert.IsNotEmpty(instructions);
foreach (var inst in instructions)
{
- Assert.NotNull(inst.OpCode);
- Assert.NotNull(inst.Operand); // can be empty string, not null
+ Assert.IsNotNull(inst.OpCode);
+ Assert.IsNotNull(inst.Operand); // can be empty string, not null
}
}
///
/// Verifies format disassembly contains hex offsets.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void FormatDisassembly_ContainsHexOffsets()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
var method = a.MethodDefs.First(m => m.Rva != 0);
var text = disasm.FormatDisassembly(method);
@@ -301,25 +319,27 @@ public void FormatDisassembly_ContainsHexOffsets()
///
/// Verifies complex app all methods disassemble without throwing.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ComplexApp_AllMethods_DisassembleWithoutThrowing()
{
- using var a = new AssemblyAnalyzer(samples.ComplexAppDll);
+ using var a = new AssemblyAnalyzer(Samples.ComplexAppDll);
var disasm = new IlDisassembler(a);
foreach (var method in a.MethodDefs.Where(m => m.Rva != 0))
{
var instructions = disasm.Disassemble(method);
- Assert.NotNull(instructions);
+ Assert.IsNotNull(instructions);
}
}
///
/// Verifies rich library method with string operand contains ldstr.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_MethodWithStringOperand_ContainsLdstr()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
// Find any method that loads a string literal
foreach (var method in a.MethodDefs.Where(m => m.Rva != 0))
@@ -328,7 +348,7 @@ public void RichLibrary_MethodWithStringOperand_ContainsLdstr()
var ldstr = instructions.FirstOrDefault(i => i.OpCode.Contains("ldstr"));
if (ldstr is not null)
{
- Assert.NotEmpty(ldstr.Operand);
+ Assert.IsNotEmpty(ldstr.Operand);
return;
}
}
@@ -337,10 +357,11 @@ public void RichLibrary_MethodWithStringOperand_ContainsLdstr()
///
/// Verifies rich library method with branches contains br.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_MethodWithBranches_ContainsBr()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
foreach (var method in a.MethodDefs.Where(m => m.Rva != 0))
{
@@ -358,10 +379,11 @@ public void RichLibrary_MethodWithBranches_ContainsBr()
///
/// Verifies rich library method call instructions resolve tokens.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RichLibrary_MethodCallInstructions_ResolveTokens()
{
- using var a = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var a = new AssemblyAnalyzer(Samples.RichLibraryDll);
var disasm = new IlDisassembler(a);
foreach (var method in a.MethodDefs.Where(m => m.Rva != 0))
{
@@ -370,7 +392,7 @@ public void RichLibrary_MethodCallInstructions_ResolveTokens()
i.OpCode == "call" || i.OpCode == "callvirt" || i.OpCode == "newobj");
if (call is not null)
{
- Assert.NotEmpty(call.Operand);
+ Assert.IsNotEmpty(call.Operand);
// Resolved token should not be just hex
Assert.DoesNotContain("0x", call.Operand);
return;
@@ -384,7 +406,7 @@ private static MethodDefInfo FindMethod(AssemblyAnalyzer analyzer, string typeNa
m.DeclaringType == typeName
&& m.Name == methodName);
- Assert.NotNull(method);
+ Assert.IsNotNull(method);
return method;
}
}
diff --git a/tests/Dotsider.Tests/IlEditorDoubleClickIntegrationTests.cs b/tests/Dotsider.Tests/IlEditorDoubleClickIntegrationTests.cs
index bf32e94b..7309c144 100644
--- a/tests/Dotsider.Tests/IlEditorDoubleClickIntegrationTests.cs
+++ b/tests/Dotsider.Tests/IlEditorDoubleClickIntegrationTests.cs
@@ -11,18 +11,21 @@ namespace Dotsider.Tests;
/// in the IL Inspector, exercising the full mouse → EditorNode → one-shot
/// cursor adjustment → yank pipeline.
///
-[Collection("SampleAssemblies")]
-public class IlEditorDoubleClickIntegrationTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class IlEditorDoubleClickIntegrationTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
private DotsiderState? _state;
private CancellationTokenSource? _cts;
+ private Task? _runTask;
private (Hex1bTerminal terminal, Hex1bApp app, CancellationToken ct) CreateDotsiderApp(string dllPath)
{
- _cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
_workload = new Hex1bAppWorkloadAdapter();
_terminal = Hex1bTerminal.CreateBuilder()
.WithWorkload(_workload)
@@ -48,6 +51,20 @@ public class IlEditorDoubleClickIntegrationTests(SampleAssemblyFixture samples)
return (_terminal, _hex1bApp, _cts.Token);
}
+ private Task RunAppAsync(Hex1bApp app, CancellationToken ct)
+ {
+ _runTask = app.RunAsync(ct);
+ return _runTask;
+ }
+
+ private bool TryWaitForAppExit()
+ {
+ if (_runTask is null) return true;
+ try { return _runTask.Wait(TimeSpan.FromSeconds(5)); }
+ catch (AggregateException ex) when (ex.InnerExceptions.All(static e => e is OperationCanceledException)) { return true; }
+ catch (OperationCanceledException) { return true; }
+ }
+
///
/// Expands the tree path for a method and selects it in the IL Inspector.
///
@@ -61,6 +78,7 @@ private void SelectMethodInTree(
_state.IlSelectedMethod = method;
_state.IlFocusedTreeKey = $"method:{method.Token}";
_state.App.Invalidate();
+ _state.RequestExtraFrame();
}
///
@@ -68,11 +86,12 @@ private void SelectMethodInTree(
/// through the full render pipeline. Verifies the one-shot cursor adjustment fires
/// correctly and that yank produces the right text and cursor position.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task SelectWordAt_ThroughRenderPipeline_AdjustsCursorAndYankWorks()
{
- var (terminal, app, ct) = CreateDotsiderApp(samples.RichLibraryDll);
- var runTask = app.RunAsync(ct);
+ var (terminal, app, ct) = CreateDotsiderApp(Samples.RichLibraryDll);
+ var runTask = RunAppAsync(app, ct);
await Task.Delay(50, ct);
// Navigate to IL Inspector tab
@@ -92,22 +111,22 @@ public async Task SelectWordAt_ThroughRenderPipeline_AdjustsCursorAndYankWorks()
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("IL_0000"), TimeSpan.FromSeconds(10))
+ .WaitUntil(_ => _state!.IlEditorMethod?.Token == method.Token
+ && _state.IlEditorState?.Document.GetText().Contains("System.", StringComparison.Ordinal) == true,
+ TimeSpan.FromSeconds(10))
.Build()
.ApplyAsync(terminal, ct);
- // Find first "System." in the disassembly
- var disassembly = _state.IlDisassembler!.FormatDisassembly(method);
- var systemIdx = disassembly.IndexOf("System.", StringComparison.Ordinal);
- Assert.True(systemIdx >= 0, "Expected 'System.' in disassembly");
-
- var doc = _state.IlEditorState!.Document;
-
- // Let a render cycle seed the one-shot tracking state
- await Task.Delay(50, ct);
+ var editorState = _state.IlEditorState!;
+ var doc = editorState.Document;
+ var fullText = doc.GetText();
+ var systemIdx = fullText.IndexOf("System.", StringComparison.Ordinal);
+ Assert.IsGreaterThanOrEqualTo(0, systemIdx, "Expected 'System.' in rendered IL editor document");
// Simulate double-click: SelectWordAt is what EditorNode.cs:281 calls
- _state.IlEditorState!.SelectWordAt(new DocumentOffset(systemIdx));
+ editorState.SelectWordAt(new DocumentOffset(systemIdx));
_state.App.Invalidate();
+ _state.RequestExtraFrame();
// Wait for the render cycle to process AdjustWordSelectionCursorOneShot —
// the one-shot pulls the cursor from the trailing '.' back onto the last
@@ -115,7 +134,8 @@ public async Task SelectWordAt_ThroughRenderPipeline_AdjustsCursorAndYankWorks()
await TestHelpers.WaitUntilAsync(
() =>
{
- var es = _state.IlEditorState!;
+ if (!ReferenceEquals(_state.IlEditorState, editorState)) return false;
+ var es = editorState;
if (!es.Cursor.HasSelection) return false;
var pos = es.Cursor.Position.Value;
var text = es.Document.GetText();
@@ -123,26 +143,25 @@ await TestHelpers.WaitUntilAsync(
},
TimeSpan.FromSeconds(5));
- var fullText = doc.GetText();
- var cursorOffset = _state.IlEditorState!.Cursor.Position.Value;
+ fullText = doc.GetText();
+ var cursorOffset = editorState.Cursor.Position.Value;
// Cursor must be on last word char ('m' of "System"), not on '.'
- Assert.True(cursorOffset < fullText.Length,
- "Cursor should be within document bounds");
- Assert.Equal('m', fullText[cursorOffset]);
+ Assert.IsLessThan(fullText.Length, cursorOffset, "Cursor should be within document bounds");
+ Assert.AreEqual('m', fullText[cursorOffset]);
// Yank must copy the full word "System"
- var range = _state.IlEditorState!.Cursor.SelectionRange;
+ var range = editorState.Cursor.SelectionRange;
var yankEnd = new DocumentOffset(Math.Min(
- Math.Max(range.End.Value, _state.IlEditorState!.Cursor.Position.Value + 1),
+ Math.Max(range.End.Value, editorState.Cursor.Position.Value + 1),
doc.Length));
var yankRange = new DocumentRange(range.Start, yankEnd);
var yankText = doc.GetText(yankRange);
- Assert.Equal("System", yankText);
+ Assert.AreEqual("System", yankText);
// Post-yank cursor must land on 'm'
var postYankCursor = new DocumentOffset(Math.Max(0, yankEnd.Value - 1));
- Assert.Equal('m', fullText[postYankCursor.Value]);
+ Assert.AreEqual('m', fullText[postYankCursor.Value]);
_cts!.Cancel();
await runTask;
@@ -153,11 +172,12 @@ await TestHelpers.WaitUntilAsync(
/// SGR mouse events through the terminal. Verifies EditorNode processes the
/// double-click, calls SelectWordAt, and the one-shot cursor adjustment fires.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task DoubleClickAt_SgrMouse_SelectsWordAndAdjustsCursor()
{
- var (terminal, app, ct) = CreateDotsiderApp(samples.RichLibraryDll);
- var runTask = app.RunAsync(ct);
+ var (terminal, app, ct) = CreateDotsiderApp(Samples.RichLibraryDll);
+ var runTask = RunAppAsync(app, ct);
await Task.Delay(50, ct);
// Navigate to IL Inspector tab
@@ -193,25 +213,29 @@ public async Task DoubleClickAt_SgrMouse_SelectsWordAndAdjustsCursor()
}, TimeSpan.FromSeconds(10))
.Build()
.ApplyAsync(terminal, ct);
- Assert.True(allMatches.Count > 0, "Expected 'System.' visible on screen");
+ Assert.IsGreaterThan(0, allMatches.Count, "Expected 'System.' visible on screen");
// Use the first match — coordinates are 0-based
var (targetRow, targetCol) = allMatches[0];
- // Single click first to give the editor focus (tree panel has focus by default).
- // Wait for the click to be processed — the editor cursor position changes
- // when the editor receives focus and handles the mouse event.
+ // Focus the editor without consuming a mouse click, then queue two real
+ // clicks so Hex1bApp's click-count state machine observes a double-click.
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(5));
- await auto.ClickAtAsync(targetCol, targetRow, ct: ct);
- await new Hex1bTerminalInputSequenceBuilder()
- .WaitUntil(_ => _state.IlEditorState?.Cursor.Position.Value > 0, TimeSpan.FromSeconds(5))
- .Build()
- .ApplyAsync(terminal, ct);
+ _state!.App.RequestFocus(node =>
+ node is EditorNode { State: var es } && es == _state.IlEditorState);
+ _state.App.Invalidate();
+ await auto.WaitUntilAsync(_ =>
+ _state.App.FocusedNode is EditorNode { State: var es } && es == _state.IlEditorState,
+ description: "IL editor focused");
// Double-click to select the word. Wait for HasSelection AND for the
// AdjustWordSelectionCursorOneShot to fire (runs on the next Build after
// selection, pulling the cursor back from punctuation to a word character).
- await auto.DoubleClickAtAsync(targetCol, targetRow, ct: ct);
+ await new Hex1bTerminalInputSequenceBuilder()
+ .ClickAt(targetCol, targetRow)
+ .ClickAt(targetCol, targetRow)
+ .Build()
+ .ApplyAsync(terminal, ct);
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(_ =>
{
@@ -225,24 +249,23 @@ public async Task DoubleClickAt_SgrMouse_SelectsWordAndAdjustsCursor()
.ApplyAsync(terminal, ct);
// Verify selection happened
- Assert.True(_state.IlEditorState!.Cursor.HasSelection,
+ Assert.IsTrue(_state.IlEditorState!.Cursor.HasSelection,
"Double-click should create a selection");
// Cursor must be on a word character, not punctuation
var doc = _state.IlEditorState!.Document;
var fullText = doc.GetText();
var cursorVal = _state.IlEditorState!.Cursor.Position.Value;
- Assert.True(cursorVal < fullText.Length,
- "Cursor should be within document bounds");
- Assert.True(char.IsLetterOrDigit(fullText[cursorVal]),
+ Assert.IsLessThan(fullText.Length, cursorVal, "Cursor should be within document bounds");
+ Assert.IsTrue(char.IsLetterOrDigit(fullText[cursorVal]),
$"Cursor should be on a word character after double-click, not '{fullText[cursorVal]}' at offset {cursorVal}");
// The selected text (via SelectionRange) should be pure word chars.
// After one-shot adjustment, SelectionRange is one char short (cursor on last char),
// which is correct — the yank logic adds +1 to compensate.
var selected = doc.GetText(_state.IlEditorState!.Cursor.SelectionRange);
- Assert.True(selected.Length > 0, "Selection must not be empty");
- Assert.True(selected.All(char.IsLetterOrDigit),
+ Assert.IsGreaterThan(0, selected.Length, "Selection must not be empty");
+ Assert.IsTrue(selected.All(char.IsLetterOrDigit),
$"Selected text should be a pure word, got '{selected}'");
// Verify the yank range (cursor.Position + 1) recovers the full word
@@ -263,12 +286,18 @@ public async Task DoubleClickAt_SgrMouse_SelectsWordAndAdjustsCursor()
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_cts?.Cancel();
+ if (!TryWaitForAppExit())
+ {
+ _hex1bApp?.Dispose();
+ _terminal?.Dispose();
+ _ = TryWaitForAppExit();
+ }
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
_cts?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/IlEditorLifecycleTests.cs b/tests/Dotsider.Tests/IlEditorLifecycleTests.cs
index 67eb6779..240528bf 100644
--- a/tests/Dotsider.Tests/IlEditorLifecycleTests.cs
+++ b/tests/Dotsider.Tests/IlEditorLifecycleTests.cs
@@ -13,9 +13,11 @@ namespace Dotsider.Tests;
/// Tests for IL editor lifecycle: StatePanelWidget-based editor caching,
/// tree list per-render sync, and field pane staleness.
///
-[Collection("SampleAssemblies")]
-public class IlEditorLifecycleTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public class IlEditorLifecycleTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -40,7 +42,7 @@ private Hex1bApp CreateMinimalApp()
private (Hex1bTerminal terminal, Hex1bApp app, CancellationToken ct) CreateDotsiderApp(string dllPath)
{
- _cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ _cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
_workload = new Hex1bAppWorkloadAdapter();
_terminal = Hex1bTerminal.CreateBuilder()
.WithWorkload(_workload)
@@ -124,55 +126,59 @@ private void RequestEditorFocusForRender()
///
/// Verifies get or create editor key same analyzer and token returns same reference.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetOrCreateEditorKey_SameAnalyzerAndToken_ReturnsSameReference()
{
var app = CreateMinimalApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
var key1 = state.GetOrCreateEditorKey(state.Analyzer, 0x06000001);
var key2 = state.GetOrCreateEditorKey(state.Analyzer, 0x06000001);
- Assert.Same(key1, key2);
+ Assert.AreSame(key1, key2);
}
///
/// Verifies get or create editor key different tokens returns different references.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void GetOrCreateEditorKey_DifferentTokens_ReturnsDifferentReferences()
{
var app = CreateMinimalApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
var key1 = state.GetOrCreateEditorKey(state.Analyzer, 0x06000001);
var key2 = state.GetOrCreateEditorKey(state.Analyzer, 0x06000002);
- Assert.NotSame(key1, key2);
+ Assert.AreNotSame(key1, key2);
}
///
/// Verifies set il focused tree key updates focused tree key.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SetIlFocusedTreeKey_UpdatesFocusedTreeKey()
{
var app = CreateMinimalApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.SetIlFocusedTreeKey("method:0x06000001");
- Assert.Equal("method:0x06000001", state.IlFocusedTreeKey);
+ Assert.AreEqual("method:0x06000001", state.IlFocusedTreeKey);
}
///
/// Verifies restore from il back entry reseeds editor key cache.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RestoreFromIlBackEntry_ReseedsEditorKeyCache()
{
var app = CreateMinimalApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
var method = state.Analyzer.MethodDefs[0];
var editorKey = state.GetOrCreateEditorKey(state.Analyzer, method.Token);
@@ -194,17 +200,18 @@ public void RestoreFromIlBackEntry_ReseedsEditorKeyCache()
// GetOrCreateEditorKey must return the reseeded key
var restoredKey = state.GetOrCreateEditorKey(state.Analyzer, method.Token);
- Assert.Same(editorKey, restoredKey);
+ Assert.AreSame(editorKey, restoredKey);
}
///
/// Verifies restore from il back entry saves outgoing editor.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void RestoreFromIlBackEntry_SavesOutgoingEditor()
{
var app = CreateMinimalApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
if (state.Analyzer.MethodDefs.Count < 2) return;
var methodB = state.Analyzer.MethodDefs[1];
@@ -226,18 +233,19 @@ public void RestoreFromIlBackEntry_SavesOutgoingEditor()
state.RestoreFromIlBackEntry(entry);
- Assert.True(state.IlCachedEditors.ContainsKey(keyB));
- Assert.Same(editorStateB, state.IlCachedEditors[keyB]);
+ Assert.IsTrue(state.IlCachedEditors.ContainsKey(keyB));
+ Assert.AreSame(editorStateB, state.IlCachedEditors[keyB]);
}
///
/// Verifies back entry preserves editor key.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void BackEntry_PreservesEditorKey()
{
var app = CreateMinimalApp();
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
if (state.Analyzer.MethodDefs.Count < 2) return;
var methodA = state.Analyzer.MethodDefs[0];
@@ -251,9 +259,9 @@ public void BackEntry_PreservesEditorKey()
var methodB = state.Analyzer.MethodDefs[1];
state.NavigateToIlDefinition(methodB.Token);
- Assert.True(state.IlBackStack.Count > 0);
+ Assert.IsGreaterThan(0, state.IlBackStack.Count);
var entry = state.IlBackStack.Peek();
- Assert.Same(keyA, entry.EditorKey);
+ Assert.AreSame(keyA, entry.EditorKey);
}
// ── Integration tests (real IL Inspector) ─────────────────
@@ -262,10 +270,11 @@ public void BackEntry_PreservesEditorKey()
/// After switching methods via the tree and switching back, the EditorNode's scroll
/// offset should be preserved (node kept alive by StatePanelWidget cache).
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EditorScroll_PreservedOnTreeRevisit()
{
- var (terminal, app, ct) = CreateDotsiderApp(samples.RichLibraryDll);
+ var (terminal, app, ct) = CreateDotsiderApp(Samples.RichLibraryDll);
var runTask = app.RunAsync(ct);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -283,10 +292,10 @@ public async Task EditorScroll_PreservedOnTreeRevisit()
// Capture the EditorNode's scroll offset after moving down
var editorNodeA = app.FocusedNode as EditorNode;
- Assert.NotNull(editorNodeA);
+ Assert.IsNotNull(editorNodeA);
var scrollAfterMove = editorNodeA!.ScrollOffset;
// Scroll should have moved from the default (1) after 10 down-arrows
- Assert.True(scrollAfterMove >= 1, $"Expected scroll > 0, got {scrollAfterMove}");
+ Assert.IsGreaterThanOrEqualTo(1, scrollAfterMove, $"Expected scroll > 0, got {scrollAfterMove}");
// Switch to a different method via tree
var methodB = _state.Analyzer.MethodDefs.First(m => m.Token != methodA.Token && m.Rva > 0);
@@ -304,9 +313,9 @@ await auto.WaitUntilAsync(_ => _state.IlEditorMethod?.Token == methodA.Token,
// The visible EditorNode should be the same cached node with preserved scroll
var restoredEditorNode = app.Focusables.OfType().FirstOrDefault();
- Assert.NotNull(restoredEditorNode);
- Assert.Same(editorNodeA, restoredEditorNode);
- Assert.Equal(scrollAfterMove, restoredEditorNode!.ScrollOffset);
+ Assert.IsNotNull(restoredEditorNode);
+ Assert.AreSame(editorNodeA, restoredEditorNode);
+ Assert.AreEqual(scrollAfterMove, restoredEditorNode!.ScrollOffset);
_cts!.Cancel();
await runTask;
@@ -316,10 +325,11 @@ await auto.WaitUntilAsync(_ => _state.IlEditorMethod?.Token == methodA.Token,
/// When search n/N switches to a different method, the editor must show the new
/// method's IL and focus must be on the visible editor, not on a hidden cached one.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task SearchNavigateToMatch_FocusesVisibleEditor()
{
- var (terminal, app, ct) = CreateDotsiderApp(samples.RichLibraryDll);
+ var (terminal, app, ct) = CreateDotsiderApp(Samples.RichLibraryDll);
var runTask = app.RunAsync(ct);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -344,7 +354,7 @@ public async Task SearchNavigateToMatch_FocusesVisibleEditor()
// Focus must be on an EditorNode that's in the focus ring (visible, not hidden)
var focused = app.FocusedNode;
- Assert.IsType(focused);
+ Assert.IsExactInstanceOfType(focused);
// The focused EditorNode must be in Focusables (not hidden in ResponsiveWidget)
Assert.Contains(focused, app.Focusables);
@@ -356,10 +366,11 @@ public async Task SearchNavigateToMatch_FocusesVisibleEditor()
///
/// The field pane must stabilize after the first render — not recreate EditorState every frame.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task FieldPane_StableAcrossFrames()
{
- var (terminal, app, ct) = CreateDotsiderApp(samples.RichLibraryDll);
+ var (terminal, app, ct) = CreateDotsiderApp(Samples.RichLibraryDll);
var runTask = app.RunAsync(ct);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -377,14 +388,14 @@ public async Task FieldPane_StableAcrossFrames()
// Capture the editor key after first render
var keyAfterFirstRender = _state.IlEditorKey;
- Assert.NotNull(keyAfterFirstRender);
+ Assert.IsNotNull(keyAfterFirstRender);
// Force another render
_state.App.Invalidate();
await auto.WaitAsync(TimeSpan.FromMilliseconds(200), ct: ct);
// Editor key should be the same (staleness guard prevents recreation)
- Assert.Same(keyAfterFirstRender, _state.IlEditorKey);
+ Assert.AreSame(keyAfterFirstRender, _state.IlEditorKey);
_cts!.Cancel();
await runTask;
@@ -399,10 +410,11 @@ public async Task FieldPane_StableAcrossFrames()
/// in between, so the render loop cannot tick and re-capture the panel before the
/// check sees the cleared field.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task ResetViewState_ClearsAllLifecycleProperties()
{
- var (terminal, app, ct) = CreateDotsiderApp(samples.RichLibraryDll);
+ var (terminal, app, ct) = CreateDotsiderApp(Samples.RichLibraryDll);
var runTask = app.RunAsync(ct);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -422,9 +434,9 @@ await auto.WaitUntilAsync(_ => _state.IlEditorMethod?.Token == methodB.Token,
description: "method B loaded");
// Verify state is populated before reset
- Assert.NotNull(_state.IlEditorKey);
- Assert.True(_state.IlEditorKeyCache.Count > 0);
- Assert.True(_state.IlCachedEditors.Count > 0);
+ Assert.IsNotNull(_state.IlEditorKey);
+ Assert.IsGreaterThan(0, _state.IlEditorKeyCache.Count);
+ Assert.IsGreaterThan(0, _state.IlCachedEditors.Count);
// Stop the render loop before mutating state so no concurrent render can
// re-populate IlScrollPanelNode (or any other field re-captured per render)
@@ -436,17 +448,17 @@ await auto.WaitUntilAsync(_ => _state.IlEditorMethod?.Token == methodB.Token,
_state.IlTreeScrollOffset = 7;
// PushAssemblyDirect calls ResetViewState (same path as CommitAnalyzer)
- using var otherAnalyzer = new AssemblyAnalyzer(samples.HelloWorldDll);
+ using var otherAnalyzer = new AssemblyAnalyzer(Samples.HelloWorldDll);
_state.PushAssemblyDirect(otherAnalyzer);
// All lifecycle properties must be cleared
- Assert.Null(_state.IlEditorKey);
- Assert.Null(_state.IlEditorField);
- Assert.Null(_state.IlScrollPanelNode);
- Assert.False(_state.IlScrollSelectionIntoViewPending);
- Assert.Equal(0, _state.IlTreeScrollOffset);
- Assert.Empty(_state.IlEditorKeyCache);
- Assert.Empty(_state.IlCachedEditors);
+ Assert.IsNull(_state.IlEditorKey);
+ Assert.IsNull(_state.IlEditorField);
+ Assert.IsNull(_state.IlScrollPanelNode);
+ Assert.IsFalse(_state.IlScrollSelectionIntoViewPending);
+ Assert.AreEqual(0, _state.IlTreeScrollOffset);
+ Assert.IsEmpty(_state.IlEditorKeyCache);
+ Assert.IsEmpty(_state.IlCachedEditors);
}
///
@@ -454,12 +466,12 @@ await auto.WaitUntilAsync(_ => _state.IlEditorMethod?.Token == methodB.Token,
///
public void Dispose()
{
+ GC.SuppressFinalize(this);
_cts?.Cancel();
_state?.Dispose();
_hex1bApp?.Dispose();
_terminal?.Dispose();
_workload?.Dispose();
_cts?.Dispose();
- GC.SuppressFinalize(this);
}
}
diff --git a/tests/Dotsider.Tests/IlEditorWordSelectionTests.cs b/tests/Dotsider.Tests/IlEditorWordSelectionTests.cs
index 67ddbb33..a46641c0 100644
--- a/tests/Dotsider.Tests/IlEditorWordSelectionTests.cs
+++ b/tests/Dotsider.Tests/IlEditorWordSelectionTests.cs
@@ -10,6 +10,7 @@ namespace Dotsider.Tests;
/// (periods and punctuation are NOT included in word selection) and
/// that Shift+Arrow keyboard selection is not blocked at word boundaries.
///
+[TestClass]
public class IlEditorWordSelectionTests
{
///
@@ -36,22 +37,24 @@ private static EditorState SelectWord(string content, int offset)
///
/// Verifies select word at dotted name cursor on last word char.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_DottedName_CursorOnLastWordChar()
{
// "System.Runtime" — double-click on "System" should place cursor on 'm' (offset 5),
// not on '.' (offset 6).
var state = SelectWord("System.Runtime", 0);
- Assert.Equal(5, state.Cursor.Position.Value); // 'm' in "System"
+ Assert.AreEqual(5, state.Cursor.Position.Value); // 'm' in "System"
var text = state.Document.GetText();
- Assert.Equal('m', text[state.Cursor.Position.Value]);
+ Assert.AreEqual('m', text[state.Cursor.Position.Value]);
}
///
/// Verifies select word at dotted name yank copies full word.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_DottedName_YankCopiesFullWord()
{
// After double-click + one-shot, yank must copy the full word "System"
@@ -66,13 +69,14 @@ public void SelectWordAt_DottedName_YankCopiesFullWord()
var yankRange = new DocumentRange(range.Start, yankEnd);
var yanked = state.Document.GetText(yankRange);
- Assert.Equal("System", yanked);
+ Assert.AreEqual("System", yanked);
}
///
/// Verifies select word at dotted name yank cursor on last char.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_DottedName_YankCursorOnLastChar()
{
// After yanking "System", cursor should collapse to 'm' (offset 5), not 'e' (offset 4).
@@ -84,14 +88,15 @@ public void SelectWordAt_DottedName_YankCursorOnLastChar()
state.Document.Length));
var lastChar = new DocumentOffset(Math.Max(0, yankEnd.Value - 1));
- Assert.Equal(5, lastChar.Value); // 'm' in "System"
- Assert.Equal('m', state.Document.GetText()[lastChar.Value]);
+ Assert.AreEqual(5, lastChar.Value); // 'm' in "System"
+ Assert.AreEqual('m', state.Document.GetText()[lastChar.Value]);
}
///
/// Verifies select word at dotted name second segment.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_DottedName_SecondSegment()
{
var state = SelectWord("System.Runtime", 7);
@@ -103,20 +108,22 @@ public void SelectWordAt_DottedName_SecondSegment()
///
/// Verifies select word at middle of word.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_MiddleOfWord()
{
var state = SelectWord("System.Runtime", 3);
var cursorPos = state.Document.OffsetToPosition(state.Cursor.Position);
var lineText = state.Document.GetLineText(cursorPos.Line);
- Assert.NotEqual('.', lineText[cursorPos.Column - 1]);
+ Assert.AreNotEqual('.', lineText[cursorPos.Column - 1]);
}
///
/// Verifies select word at il instruction stops at colon.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_IlInstruction_StopsAtColon()
{
var state = SelectWord(
@@ -129,20 +136,22 @@ public void SelectWordAt_IlInstruction_StopsAtColon()
///
/// Verifies select word at on period cursor not on period.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_OnPeriod_CursorNotOnPeriod()
{
var state = SelectWord("System.Runtime", 6);
var cursorPos = state.Document.OffsetToPosition(state.Cursor.Position);
var lineText = state.Document.GetLineText(cursorPos.Line);
- Assert.NotEqual('.', lineText[cursorPos.Column - 1]);
+ Assert.AreNotEqual('.', lineText[cursorPos.Column - 1]);
}
///
/// Verifies select word at cursor never on period after dotted selection.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void SelectWordAt_CursorNeverOnPeriod_AfterDottedSelection()
{
var doc = new Hex1bDocument("System.Runtime.CompilerServices");
@@ -166,7 +175,7 @@ public void SelectWordAt_CursorNeverOnPeriod_AfterDottedSelection()
{
var lineText = doc.GetLineText(doc.OffsetToPosition(pos).Line);
var col = doc.OffsetToPosition(pos).Column - 1;
- Assert.NotEqual('.', lineText[col]);
+ Assert.AreNotEqual('.', lineText[col]);
}
}
}
@@ -174,7 +183,8 @@ public void SelectWordAt_CursorNeverOnPeriod_AfterDottedSelection()
///
/// Verifies shift arrow selection crosses word boundary.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ShiftArrow_SelectionCrossesWordBoundary()
{
// "System.Runtime" — Shift+Arrow from offset 0 should be able to select
@@ -201,18 +211,19 @@ public void ShiftArrow_SelectionCrossesWordBoundary()
IlInspectorView.AdjustWordSelectionCursorOneShot(state, ref prevAnchor, ref prevPosition);
// Cursor must NOT be pulled back — it should stay where we put it.
- Assert.Equal(i, state.Cursor.Position.Value);
+ Assert.AreEqual(i, state.Cursor.Position.Value);
}
// Final selection should span from 0 to 13, including the period.
var selected = doc.GetText(state.Cursor.SelectionRange);
- Assert.Equal("System.Runtim", selected);
+ Assert.AreEqual("System.Runtim", selected);
}
///
/// Verifies one shot does not re fire on subsequent frames.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void OneShot_DoesNotReFireOnSubsequentFrames()
{
// After the one-shot fires, repeated calls with the same state should not
@@ -231,13 +242,13 @@ public void OneShot_DoesNotReFireOnSubsequentFrames()
IlInspectorView.AdjustWordSelectionCursorOneShot(state, ref prevAnchor, ref prevPosition);
var afterFirst = state.Cursor.Position.Value;
- Assert.Equal(5, afterFirst); // 'm' in "System"
+ Assert.AreEqual(5, afterFirst); // 'm' in "System"
// Simulate several more render frames with no user input.
for (var i = 0; i < 5; i++)
{
IlInspectorView.AdjustWordSelectionCursorOneShot(state, ref prevAnchor, ref prevPosition);
- Assert.Equal(afterFirst, state.Cursor.Position.Value);
+ Assert.AreEqual(afterFirst, state.Cursor.Position.Value);
}
}
}
diff --git a/tests/Dotsider.Tests/IlGoToDefinitionTests.cs b/tests/Dotsider.Tests/IlGoToDefinitionTests.cs
index df2da56c..5ec8c7f4 100644
--- a/tests/Dotsider.Tests/IlGoToDefinitionTests.cs
+++ b/tests/Dotsider.Tests/IlGoToDefinitionTests.cs
@@ -14,9 +14,11 @@ namespace Dotsider.Tests;
///
/// Tests for Il Go To Definition.
///
-[Collection("SampleAssemblies")]
-public sealed class IlGoToDefinitionTests(SampleAssemblyFixture samples) : IDisposable
+[TestClass]
+public sealed class IlGoToDefinitionTests : IDisposable
{
+ private static SampleAssemblyFixture Samples => SampleAssemblyHost.Instance;
+
private Hex1bAppWorkloadAdapter? _workload;
private Hex1bTerminal? _terminal;
private Hex1bApp? _hex1bApp;
@@ -34,7 +36,7 @@ public sealed class IlGoToDefinitionTests(SampleAssemblyFixture samples) : IDisp
_hex1bApp = new Hex1bApp(
ctx =>
{
- _state ??= new DotsiderState(_hex1bApp!, samples.RichLibraryDll)
+ _state ??= new DotsiderState(_hex1bApp!, Samples.RichLibraryDll)
{
CurrentTab = TabId.IlInspector
};
@@ -80,7 +82,7 @@ private async Task NavigateToCallLocalMethodAndFocusCallLine(
const string callText = "call RichLibrary.IlNavigationFixture::LocalTarget";
var text = _state!.IlEditorState!.Document.GetText();
var callOffset = text.IndexOf(callText, StringComparison.Ordinal);
- Assert.True(callOffset >= 0, $"Expected rendered IL to contain '{callText}'.");
+ Assert.IsGreaterThanOrEqualTo(0, callOffset, $"Expected rendered IL to contain '{callText}'.");
await SetIlCursorAsync(auto, callOffset, "cursor moved to local call instruction");
@@ -115,13 +117,13 @@ private async Task SetIlCursorOnInstructionAsync(
var marker = $"IL_{instruction.Offset:X4}:";
var text = _state!.IlEditorState!.Document.GetText();
var offset = text.IndexOf(marker, StringComparison.Ordinal);
- Assert.True(offset >= 0, $"Expected rendered IL to contain '{marker}'.");
+ Assert.IsGreaterThanOrEqualTo(0, offset, $"Expected rendered IL to contain '{marker}'.");
await SetIlCursorAsync(auto, offset, $"cursor moved to {marker}");
var instAtCursor = IlNavigationHelper.GetInstructionAtCursor(
_state.IlEditorState!, _state.IlInstructions!, _state.IlHeaderLineCount);
- Assert.NotNull(instAtCursor);
- Assert.Equal(instruction.Offset, instAtCursor!.Offset);
+ Assert.IsNotNull(instAtCursor);
+ Assert.AreEqual(instruction.Offset, instAtCursor!.Offset);
}
///
@@ -138,49 +140,51 @@ public void Dispose()
///
/// Verifies resolve method def returns local method.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Resolve_MethodDef_ReturnsLocalMethod()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var dis = new IlDisassembler(analyzer);
var method = analyzer.MethodDefs.First(m =>
m.Name == "CallLocalMethod" && m.DeclaringType.Contains("IlNavigationFixture"));
var callInst = dis.Disassemble(method).First(i => i.OpCode == "call" && i.MetadataToken is not null);
var target = IlNavigationResolver.Resolve(analyzer, callInst.MetadataToken!.Value);
- var localMethod = Assert.IsType(target);
- Assert.Equal("LocalTarget", localMethod.Method.Name);
+ var localMethod = Assert.IsExactInstanceOfType(target);
+ Assert.AreEqual("LocalTarget", localMethod.Method.Name);
}
///
/// Verifies resolve field def returns local field.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Resolve_FieldDef_ReturnsLocalField()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var dis = new IlDisassembler(analyzer);
var method = analyzer.MethodDefs.First(m =>
m.Name == "ReadInstanceField" && m.DeclaringType.Contains("IlNavigationFixture"));
var fieldInst = dis.Disassemble(method).First(i => i.OpCode == "ldfld" && i.MetadataToken is not null);
- Assert.Equal("_counter", Assert.IsType(
+ Assert.AreEqual("_counter", Assert.IsExactInstanceOfType(
IlNavigationResolver.Resolve(analyzer, fieldInst.MetadataToken!.Value)).Field.Name);
}
///
/// Verifies disassemble with text header and instruction counts match.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void DisassembleWithText_HeaderAndInstructionCountsMatch()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var dis = new IlDisassembler(analyzer);
var method = analyzer.MethodDefs.First(m =>
m.Name == "CallLocalMethod" && m.DeclaringType.Contains("IlNavigationFixture"));
var result = dis.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
var sequenceCommentCount = result.Value.Instructions.Count(i => i.SequenceStartLine is not null);
- Assert.Equal(result.Value.HeaderLineCount + result.Value.Instructions.Count + sequenceCommentCount,
- result.Value.Text.Split('\n').Length);
+ Assert.HasCount(result.Value.HeaderLineCount + result.Value.Instructions.Count + sequenceCommentCount, result.Value.Text.Split('\n'));
}
// --- Full end-to-end UI tests ---
@@ -188,10 +192,11 @@ public void DisassembleWithText_HeaderAndInstructionCountsMatch()
///
/// Verifies go to def enter screen shows target method il.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task GoToDef_Enter_ScreenShowsTargetMethodIL()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -208,7 +213,7 @@ public async Task GoToDef_Enter_ScreenShowsTargetMethodIL()
await auto.WaitUntilNoTextAsync("// Method: RichLibrary.IlNavigationFixture::CallLocalMethod");
// STATE: IlBackStack should have one entry
- Assert.Single(_state!.IlBackStack);
+ Assert.ContainsSingle(_state!.IlBackStack);
// HINTS BAR: should show Esc: Back
await auto.WaitUntilTextAsync("Esc: Back");
@@ -220,10 +225,11 @@ public async Task GoToDef_Enter_ScreenShowsTargetMethodIL()
///
/// Verifies esc back screen restores original method il.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_ScreenRestoresOriginalMethodIL()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -247,10 +253,11 @@ public async Task EscBack_ScreenRestoresOriginalMethodIL()
await auto.WaitUntilNoTextAsync("// Method: RichLibrary.IlNavigationFixture::LocalTarget");
// STATE: IlBackStack should be empty
- Assert.Empty(_state!.IlBackStack);
+ Assert.IsEmpty(_state!.IlBackStack);
// STATE: selected method should be CallLocalMethod
- Assert.Equal("CallLocalMethod", _state.IlSelectedMethod?.Name);
+ Assert.IsNotNull(_state.IlSelectedMethod);
+ Assert.AreEqual("CallLocalMethod", _state.IlSelectedMethod.Name);
cts.Cancel();
await runTask;
@@ -259,16 +266,17 @@ public async Task EscBack_ScreenRestoresOriginalMethodIL()
///
/// Verifies esc back cursor position restored exactly.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_CursorPositionRestoredExactly()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
var cursorBeforeNav = await NavigateToCallLocalMethodAndFocusCallLine(auto, cts.Token);
- Assert.True(cursorBeforeNav > 0, "Cursor should be past the header lines");
+ Assert.IsGreaterThan(0, cursorBeforeNav, "Cursor should be past the header lines");
// Go to definition
await auto.EnterAsync(cts.Token);
@@ -281,7 +289,7 @@ public async Task EscBack_CursorPositionRestoredExactly()
// CURSOR: must be in the instruction area (past header lines), proving
// the cursor was restored to the IL bytecode region, not reset to line 1.
var cursorAfterBack = _state!.IlEditorState?.Cursor.Position.Value ?? -1;
- Assert.True(cursorAfterBack > 0, "Cursor should be restored past header lines");
+ Assert.IsGreaterThan(0, cursorAfterBack, "Cursor should be restored past header lines");
var text = _state.IlEditorState!.Document.GetText();
int LineOf(int offset)
@@ -293,8 +301,7 @@ int LineOf(int offset)
}
var cursorLine = LineOf(cursorAfterBack);
- Assert.True(cursorLine > _state.IlHeaderLineCount,
- $"Cursor should be in the instruction area (line {cursorLine}) " +
+ Assert.IsGreaterThan(_state.IlHeaderLineCount, cursorLine, $"Cursor should be in the instruction area (line {cursorLine}) " +
$"but header has {_state.IlHeaderLineCount} lines");
cts.Cancel();
@@ -304,10 +311,11 @@ int LineOf(int offset)
///
/// Verifies esc back scroll works after restore.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_ScrollWorksAfterRestore()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -345,7 +353,7 @@ await auto.WaitUntilAsync(
description: "Up moved editor cursor");
var cursorAfterUp = _state.IlEditorState?.Cursor.Position.Value ?? -1;
- Assert.NotEqual(cursorAfterScroll, cursorAfterUp);
+ Assert.AreNotEqual(cursorAfterScroll, cursorAfterUp);
// SCREEN: IL bytecode must still be visible after scrolling
await auto.WaitUntilTextAsync("call RichLibrary.IlNavigationFixture::LocalTarget");
@@ -357,10 +365,11 @@ await auto.WaitUntilAsync(
///
/// Verifies esc back tree state restored.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_TreeStateRestored()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -382,13 +391,13 @@ public async Task EscBack_TreeStateRestored()
// TREE STATE: expansion state must match what was saved
foreach (var (key, value) in treeBefore)
{
- Assert.True(_state.IlTreeExpansionState.TryGetValue(key, out var restored),
+ Assert.IsTrue(_state.IlTreeExpansionState.TryGetValue(key, out var restored),
$"Tree key '{key}' missing after restore");
- Assert.Equal(value, restored);
+ Assert.AreEqual(value, restored);
}
// TREE STATE: focused key must be restored
- Assert.Equal(focusedKeyBefore, _state.IlFocusedTreeKey);
+ Assert.AreEqual(focusedKeyBefore, _state.IlFocusedTreeKey);
cts.Cancel();
await runTask;
@@ -396,10 +405,11 @@ public async Task EscBack_TreeStateRestored()
///
/// Verifies diagnostic escape handler fires state changes.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task Diagnostic_EscapeHandlerFires_StateChanges()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -411,8 +421,9 @@ public async Task Diagnostic_EscapeHandlerFires_StateChanges()
await auto.WaitUntilTextAsync("// Method: RichLibrary.IlNavigationFixture::LocalTarget");
// Verify back stack has entry
- Assert.Single(_state!.IlBackStack);
- Assert.Equal("LocalTarget", _state.IlSelectedMethod?.Name);
+ Assert.ContainsSingle(_state!.IlBackStack);
+ Assert.IsNotNull(_state.IlSelectedMethod);
+ Assert.AreEqual("LocalTarget", _state.IlSelectedMethod.Name);
// Press Escape
await auto.EscapeAsync(cts.Token);
@@ -424,9 +435,9 @@ public async Task Diagnostic_EscapeHandlerFires_StateChanges()
var methodName = _state.IlSelectedMethod?.Name;
// Diagnostic output
- Assert.True(backStackEmpty, $"IlBackStack should be empty after Esc but has {_state.IlBackStack.Count} entries. " +
+ Assert.IsTrue(backStackEmpty, $"IlBackStack should be empty after Esc but has {_state.IlBackStack.Count} entries. " +
$"Method is '{methodName}'. This means the Escape handler did NOT fire.");
- Assert.Equal("CallLocalMethod", methodName);
+ Assert.AreEqual("CallLocalMethod", methodName);
cts.Cancel();
await runTask;
@@ -435,13 +446,14 @@ public async Task Diagnostic_EscapeHandlerFires_StateChanges()
///
/// Verifies cross assembly back stack survives reset view state.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void CrossAssembly_BackStackSurvivesResetViewState()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
// Select CallExternal and set up editor state
@@ -449,7 +461,7 @@ public void CrossAssembly_BackStackSurvivesResetViewState()
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
var doc = new Hex1b.Documents.Hex1bDocument(result.Value.Text);
state.IlEditorState = new EditorState(doc) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -458,21 +470,21 @@ public void CrossAssembly_BackStackSurvivesResetViewState()
// Find the call instruction token
var callInst = result.Value.Instructions.FirstOrDefault(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
- Assert.NotNull(callInst);
+ Assert.IsNotNull(callInst);
// Navigate to external method
var navigated = state.NavigateToIlDefinition(callInst.MetadataToken!.Value);
// Back stack must survive PushAssemblyDirect → ResetViewState
- Assert.True(state.IlBackStack.Count > 0,
- $"IlBackStack must survive cross-assembly navigation but has {state.IlBackStack.Count} entries");
+ Assert.IsGreaterThan(0, state.IlBackStack.Count, $"IlBackStack must survive cross-assembly navigation but has {state.IlBackStack.Count} entries");
// Restore from back entry
if (navigated)
{
var entry = state.IlBackStack.Pop();
state.RestoreFromIlBackEntry(entry);
- Assert.Equal("CallExternal", state.IlSelectedMethod?.Name);
+ Assert.IsNotNull(state.IlSelectedMethod);
+ Assert.AreEqual("CallExternal", state.IlSelectedMethod.Name);
Assert.Contains("RichLibrary", state.Analyzer.FileName);
}
}
@@ -480,20 +492,21 @@ public void CrossAssembly_BackStackSurvivesResetViewState()
///
/// Verifies local field go to def clears selected method.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void LocalField_GoToDef_ClearsSelectedMethod()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "ReadInstanceField" && m.DeclaringType.Contains("IlNavigationFixture"));
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -506,25 +519,28 @@ public void LocalField_GoToDef_ClearsSelectedMethod()
// Field navigation should clear IlSelectedMethod (no method to show)
// and focus the declaring type in the tree
- Assert.Null(state.IlSelectedMethod);
- Assert.Contains("type:", (string?)state.IlFocusedTreeKey);
+ Assert.IsNull(state.IlSelectedMethod);
+ Assert.IsNotNull(state.IlFocusedTreeKey);
+ Assert.Contains("type:", (string)state.IlFocusedTreeKey);
// Back should restore
var entry = state.IlBackStack.Pop();
state.RestoreFromIlBackEntry(entry);
- Assert.Equal("ReadInstanceField", state.IlSelectedMethod?.Name);
+ Assert.IsNotNull(state.IlSelectedMethod);
+ Assert.AreEqual("ReadInstanceField", state.IlSelectedMethod.Name);
}
///
/// Verifies normal push assembly clears il back stack.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NormalPushAssembly_ClearsIlBackStack()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
// Set up a local go-to-def so IlBackStack has an entry
@@ -532,7 +548,7 @@ public void NormalPushAssembly_ClearsIlBackStack()
m.Name == "CallLocalMethod" && m.DeclaringType.Contains("IlNavigationFixture"));
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -541,12 +557,12 @@ public void NormalPushAssembly_ClearsIlBackStack()
var callInst = result.Value.Instructions.First(i =>
i.OpCode == "call" && i.MetadataToken is not null);
state.NavigateToIlDefinition(callInst.MetadataToken!.Value);
- Assert.Single(state.IlBackStack);
+ Assert.ContainsSingle(state.IlBackStack);
// Normal assembly push (dependency navigation) must clear the back stack
// because entries reference the old analyzer's state
- state.PushAssembly(samples.HelloWorldDll);
- Assert.Empty(state.IlBackStack);
+ state.PushAssembly(Samples.HelloWorldDll);
+ Assert.IsEmpty(state.IlBackStack);
}
// --- Issue #159: Esc on IL Inspector loses Size Map back target after gd into external ---
@@ -555,24 +571,25 @@ public void NormalPushAssembly_ClearsIlBackStack()
/// Verifies the cross-view back target survives a cross-assembly method gd
/// round-trip (Size Map → IL Inspector → external method → Esc back).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromCrossAssemblyMethodGd_RestoresSizeMapCrossViewBackTarget()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -582,34 +599,35 @@ public void EscBack_FromCrossAssemblyMethodGd_RestoresSizeMapCrossViewBackTarget
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
var navigated = state.NavigateToIlDefinition(callInst.MetadataToken!.Value);
- Assert.True(navigated);
- Assert.Single(state.IlBackStack);
- Assert.True(state.NavigationStack.Count > 0);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.IsTrue(navigated);
+ Assert.ContainsSingle(state.IlBackStack);
+ Assert.IsGreaterThan(0, state.NavigationStack.Count);
+ Assert.IsNull(state.CrossViewBackTarget);
var entry = state.IlBackStack.Pop();
state.RestoreFromIlBackEntry(entry);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Empty(state.NavigationStack);
- Assert.Empty(state.IlBackStack);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.IsEmpty(state.IlBackStack);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
state.NavigateBack();
- Assert.Equal(TabId.SizeMap, state.CurrentTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.SizeMap, state.CurrentTab);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
/// Verifies the cross-view back target survives a cross-assembly field gd round-trip.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromCrossAssemblyFieldGd_RestoresSizeMapCrossViewBackTarget()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// GetStringEmpty has body `ldsfld string.Empty` — external FieldRef
var method = state.Analyzer.MethodDefs.First(m =>
@@ -617,11 +635,11 @@ public void EscBack_FromCrossAssemblyFieldGd_RestoresSizeMapCrossViewBackTarget(
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -631,34 +649,35 @@ public void EscBack_FromCrossAssemblyFieldGd_RestoresSizeMapCrossViewBackTarget(
i.OpCode == "ldsfld" && i.MetadataToken is not null);
var navigated = state.NavigateToIlDefinition(ldsInst.MetadataToken!.Value);
- Assert.True(navigated);
- Assert.Single(state.IlBackStack);
- Assert.True(state.NavigationStack.Count > 0);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.IsTrue(navigated);
+ Assert.ContainsSingle(state.IlBackStack);
+ Assert.IsGreaterThan(0, state.NavigationStack.Count);
+ Assert.IsNull(state.CrossViewBackTarget);
var entry = state.IlBackStack.Pop();
state.RestoreFromIlBackEntry(entry);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Empty(state.NavigationStack);
- Assert.Empty(state.IlBackStack);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.IsEmpty(state.IlBackStack);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
state.NavigateBack();
- Assert.Equal(TabId.SizeMap, state.CurrentTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.SizeMap, state.CurrentTab);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
/// Verifies the cross-view back target survives a cross-assembly type gd round-trip.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromCrossAssemblyTypeGd_RestoresSizeMapCrossViewBackTarget()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// CastToExternalStream has body `castclass System.IO.Stream` — external TypeRef
var method = state.Analyzer.MethodDefs.First(m =>
@@ -666,11 +685,11 @@ public void EscBack_FromCrossAssemblyTypeGd_RestoresSizeMapCrossViewBackTarget()
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -680,22 +699,22 @@ public void EscBack_FromCrossAssemblyTypeGd_RestoresSizeMapCrossViewBackTarget()
i.OpCode == "castclass" && i.MetadataToken is not null);
var navigated = state.NavigateToIlDefinition(castInst.MetadataToken!.Value);
- Assert.True(navigated);
- Assert.Single(state.IlBackStack);
- Assert.True(state.NavigationStack.Count > 0);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.IsTrue(navigated);
+ Assert.ContainsSingle(state.IlBackStack);
+ Assert.IsGreaterThan(0, state.NavigationStack.Count);
+ Assert.IsNull(state.CrossViewBackTarget);
var entry = state.IlBackStack.Pop();
state.RestoreFromIlBackEntry(entry);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Empty(state.NavigationStack);
- Assert.Empty(state.IlBackStack);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.IsEmpty(state.IlBackStack);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
state.NavigateBack();
- Assert.Equal(TabId.SizeMap, state.CurrentTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.SizeMap, state.CurrentTab);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
@@ -703,24 +722,25 @@ public void EscBack_FromCrossAssemblyTypeGd_RestoresSizeMapCrossViewBackTarget()
/// back target without going through the snapshot/restore round-trip,
/// because the local path never calls PushAssemblyDirect → ResetViewState.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromLocalGd_PreservesSizeMapCrossViewBackTarget()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "CallLocalMethod" && m.DeclaringType.Contains("IlNavigationFixture"));
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -730,16 +750,16 @@ public void EscBack_FromLocalGd_PreservesSizeMapCrossViewBackTarget()
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("LocalTarget"));
var navigated = state.NavigateToIlDefinition(callInst.MetadataToken!.Value);
- Assert.True(navigated);
- Assert.Single(state.IlBackStack);
- Assert.Empty(state.NavigationStack);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.IsTrue(navigated);
+ Assert.ContainsSingle(state.IlBackStack);
+ Assert.IsEmpty(state.NavigationStack);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
state.RestoreFromIlBackEntry(state.IlBackStack.Pop());
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
state.NavigateBack();
- Assert.Equal(TabId.SizeMap, state.CurrentTab);
+ Assert.AreEqual(TabId.SizeMap, state.CurrentTab);
}
///
@@ -747,10 +767,11 @@ public void EscBack_FromLocalGd_PreservesSizeMapCrossViewBackTarget()
/// and confirm two REAL Esc presses return to Size Map. Pre-fix the second
/// Esc binding de-registers and the user is stuck on IL Inspector.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_FromSizeMapToExternalCall_TwoRealEscapesReturnToSizeMap()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -765,7 +786,7 @@ public async Task EscBack_FromSizeMapToExternalCall_TwoRealEscapesReturnToSizeMa
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
_state.CurrentTab = TabId.SizeMap;
_state.NavigateToIlMethod(callExternal);
- Assert.Equal((TabId.SizeMap, 0), _state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.SizeMap, 0), _state.CrossViewBackTarget);
await auto.WaitUntilTextAsync("// Method: RichLibrary.IlNavigationFixture::CallExternal");
await auto.WaitUntilTextAsync("call System.Console::WriteLine");
@@ -789,13 +810,13 @@ await auto.WaitUntilAsync(_ =>
var instructions = _state!.IlInstructions!.ToList();
var callIndex = instructions.FindIndex(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
- Assert.True(callIndex >= 0, "WriteLine call instruction must exist in CallExternal body");
+ Assert.IsGreaterThanOrEqualTo(0, callIndex, "WriteLine call instruction must exist in CallExternal body");
await SetIlCursorOnInstructionAsync(auto, instructions[callIndex]);
var instAtCursor = IlNavigationHelper.GetInstructionAtCursor(
_state.IlEditorState!, _state.IlInstructions!, _state.IlHeaderLineCount);
- Assert.NotNull(instAtCursor);
- Assert.Equal("call", instAtCursor!.OpCode);
+ Assert.IsNotNull(instAtCursor);
+ Assert.AreEqual("call", instAtCursor!.OpCode);
Assert.Contains("WriteLine", instAtCursor.Operand);
// Real gd via Enter — crosses into System.Console.dll.
@@ -804,9 +825,9 @@ await auto.WaitUntilAsync(_ =>
// The IlDisassembler emits "// Method: System.Console::WriteLine" only
// in the destination's IL header — unique landing marker.
await auto.WaitUntilTextAsync("// Method: System.Console::WriteLine");
- Assert.True(_state.NavigationStack.Count > 0);
- Assert.Single(_state.IlBackStack);
- Assert.Null(_state.CrossViewBackTarget);
+ Assert.IsGreaterThan(0, _state.NavigationStack.Count);
+ Assert.ContainsSingle(_state.IlBackStack);
+ Assert.IsNull(_state.CrossViewBackTarget);
// First REAL Esc — pops IL back entry, returns to CallExternal IL.
await auto.EscapeAsync(cts.Token);
@@ -816,16 +837,16 @@ await auto.WaitUntilAsync(_ =>
// binding is registered for the second press. Pre-fix all three back
// signals are zero/null and the binding de-registers.
await auto.WaitUntilTextAsync("Esc: Back");
- Assert.Equal((TabId.SizeMap, 0), _state.CrossViewBackTarget);
- Assert.Equal(TabId.IlInspector, _state.CurrentTab);
- Assert.Empty(_state.IlBackStack);
- Assert.Empty(_state.NavigationStack);
+ Assert.AreEqual((TabId.SizeMap, 0), _state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, _state.CurrentTab);
+ Assert.IsEmpty(_state.IlBackStack);
+ Assert.IsEmpty(_state.NavigationStack);
// Second REAL Esc — drives cross-view return to Size Map.
await auto.EscapeAsync(cts.Token);
await auto.WaitUntilTextAsync("Total:");
- Assert.Equal(TabId.SizeMap, _state.CurrentTab);
- Assert.Null(_state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.SizeMap, _state.CurrentTab);
+ Assert.IsNull(_state.CrossViewBackTarget);
cts.Cancel();
await runTask;
@@ -846,8 +867,7 @@ private static (SizeNode Root, SizeNode Namespace, SizeNode Type, int MethodChil
var typeNode = nsNode.Children.First(c => c.FullPath == method.DeclaringType);
var methodIdx = typeNode.Children.ToList().FindIndex(c =>
c.FullPath == $"{method.DeclaringType}::{method.Name}@0x{method.Token:X8}");
- Assert.True(methodIdx >= 0,
- $"Method {method.DeclaringType}::{method.Name} not found in size tree");
+ Assert.IsGreaterThanOrEqualTo(0, methodIdx, $"Method {method.DeclaringType}::{method.Name} not found in size tree");
return (root, nsNode, typeNode, methodIdx);
}
@@ -856,17 +876,18 @@ private static (SizeNode Root, SizeNode Namespace, SizeNode Type, int MethodChil
/// selection, search) is restored after a cross-assembly gd round-trip. Parameterized
/// across the three external dispatch paths: method, field, type.
///
- [Theory(Timeout = 30_000)]
- [InlineData("CallExternal", "call", "WriteLine")]
- [InlineData("GetStringEmpty", "ldsfld", "")]
- [InlineData("CastToExternalStream", "castclass", "")]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow("CallExternal", "call", "WriteLine")]
+ [DataRow("GetStringEmpty", "ldsfld", "")]
+ [DataRow("CastToExternalStream", "castclass", "")]
public void EscBack_FromCrossAssemblyGd_RestoresFullSizeMapDrillState(
string methodName, string opCode, string operandSubstring)
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CachedSizeTree = SizeAnalyzer.BuildSizeTree(state.Analyzer);
var origTree = state.CachedSizeTree;
@@ -891,12 +912,12 @@ public void EscBack_FromCrossAssemblyGd_RestoresFullSizeMapDrillState(
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
- Assert.Same(type, state.TreemapCurrentLevel);
- Assert.Equal(2, state.TreemapBreadcrumb.Count);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreSame(type, state.TreemapCurrentLevel);
+ Assert.HasCount(2, state.TreemapBreadcrumb);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
var dis = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(dis);
+ Assert.IsNotNull(dis);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(dis.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -905,57 +926,58 @@ public void EscBack_FromCrossAssemblyGd_RestoresFullSizeMapDrillState(
var inst = dis.Value.Instructions.First(i =>
i.OpCode == opCode && i.MetadataToken is not null
&& (operandSubstring.Length == 0 || i.Operand.Contains(operandSubstring)));
- Assert.True(state.NavigateToIlDefinition(inst.MetadataToken!.Value));
+ Assert.IsTrue(state.NavigateToIlDefinition(inst.MetadataToken!.Value));
// Mid-flight: ResetViewState wiped everything.
- Assert.Null(state.TreemapCurrentLevel);
- Assert.Empty(state.TreemapBreadcrumb);
- Assert.Null(state.CachedSizeTree);
- Assert.False(state.Search[TabId.SizeMap].IsActive);
+ Assert.IsNull(state.TreemapCurrentLevel);
+ Assert.IsEmpty(state.TreemapBreadcrumb);
+ Assert.IsNull(state.CachedSizeTree);
+ Assert.IsFalse(state.Search[TabId.SizeMap].IsActive);
// First Esc — pop the cross-assembly entry and restore.
state.RestoreFromIlBackEntry(state.IlBackStack.Pop());
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
- Assert.Same(origTree, state.CachedSizeTree);
- Assert.Same(type, state.TreemapCurrentLevel);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreSame(origTree, state.CachedSizeTree);
+ Assert.AreSame(type, state.TreemapCurrentLevel);
var stack = state.TreemapBreadcrumb.ToArray();
- Assert.Equal(2, stack.Length);
- Assert.Same(ns, stack[0]);
- Assert.Same(root, stack[1]);
- Assert.Equal(methodIdx, state.TreemapSelectedIndex);
- Assert.Same(
+ Assert.HasCount(2, stack);
+ Assert.AreSame(ns, stack[0]);
+ Assert.AreSame(root, stack[1]);
+ Assert.AreEqual(methodIdx, state.TreemapSelectedIndex);
+ Assert.AreSame(
type.Children[methodIdx],
state.TreemapCurrentLevel!.Children[state.TreemapSelectedIndex]);
var s = state.Search[TabId.SizeMap];
- Assert.True(s.IsActive);
- Assert.True(s.IsConfirmed);
- Assert.Equal("Call", s.Query);
- Assert.Equal(2, s.MatchCount);
+ Assert.IsTrue(s.IsActive);
+ Assert.IsTrue(s.IsConfirmed);
+ Assert.AreEqual("Call", s.Query);
+ Assert.AreEqual(2, s.MatchCount);
// Second Esc — back to Size Map at the drilled level.
state.NavigateBack();
- Assert.Equal(TabId.SizeMap, state.CurrentTab);
- Assert.Same(type, state.TreemapCurrentLevel);
+ Assert.AreEqual(TabId.SizeMap, state.CurrentTab);
+ Assert.AreSame(type, state.TreemapCurrentLevel);
}
///
/// Regression guard: when the user never drilled (TreemapCurrentLevel == null,
/// breadcrumb empty), the snapshot/restore round-trip must not invent a breadcrumb.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_NoDrill_NullCurrentLevelStaysNull()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// No drill: TreemapCurrentLevel left null, breadcrumb empty, no cached tree.
- Assert.Null(state.TreemapCurrentLevel);
- Assert.Empty(state.TreemapBreadcrumb);
- Assert.Null(state.CachedSizeTree);
+ Assert.IsNull(state.TreemapCurrentLevel);
+ Assert.IsEmpty(state.TreemapBreadcrumb);
+ Assert.IsNull(state.CachedSizeTree);
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
@@ -969,14 +991,14 @@ public void EscBack_NoDrill_NullCurrentLevelStaysNull()
state.IlEditorAnalyzer = state.Analyzer;
var callInst = dis.Value.Instructions.First(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
- Assert.True(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
+ Assert.IsTrue(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
state.RestoreFromIlBackEntry(state.IlBackStack.Pop());
- Assert.Null(state.TreemapCurrentLevel);
- Assert.Empty(state.TreemapBreadcrumb);
- Assert.Equal(-1, state.TreemapSelectedIndex);
- Assert.False(state.Search[TabId.SizeMap].IsActive);
+ Assert.IsNull(state.TreemapCurrentLevel);
+ Assert.IsEmpty(state.TreemapBreadcrumb);
+ Assert.AreEqual(-1, state.TreemapSelectedIndex);
+ Assert.IsFalse(state.Search[TabId.SizeMap].IsActive);
}
///
@@ -985,18 +1007,19 @@ public void EscBack_NoDrill_NullCurrentLevelStaysNull()
/// Pre-fix this fails because ResetViewState zeros TreemapCurrentLevel and nothing puts
/// the root reference back.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_DrilledToRoot_RestoresRootIdentity()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CachedSizeTree = SizeAnalyzer.BuildSizeTree(state.Analyzer);
var origTree = state.CachedSizeTree;
state.TreemapCurrentLevel = origTree; // user drilled then popped back to root
- Assert.Empty(state.TreemapBreadcrumb);
+ Assert.IsEmpty(state.TreemapBreadcrumb);
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
@@ -1010,13 +1033,13 @@ public void EscBack_DrilledToRoot_RestoresRootIdentity()
state.IlEditorAnalyzer = state.Analyzer;
var callInst = dis.Value.Instructions.First(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
- Assert.True(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
+ Assert.IsTrue(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
state.RestoreFromIlBackEntry(state.IlBackStack.Pop());
- Assert.Same(origTree, state.CachedSizeTree);
- Assert.Same(origTree, state.TreemapCurrentLevel);
- Assert.Empty(state.TreemapBreadcrumb);
+ Assert.AreSame(origTree, state.CachedSizeTree);
+ Assert.AreSame(origTree, state.TreemapCurrentLevel);
+ Assert.IsEmpty(state.TreemapBreadcrumb);
}
///
@@ -1024,13 +1047,14 @@ public void EscBack_DrilledToRoot_RestoresRootIdentity()
/// the treemap state survives without snapshot/restore. After a local gd round-trip
/// the original drill state must still be present (object identity preserved).
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromLocalGd_PreservesBreadcrumb()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CachedSizeTree = SizeAnalyzer.BuildSizeTree(state.Analyzer);
var origTree = state.CachedSizeTree;
@@ -1055,19 +1079,19 @@ public void EscBack_FromLocalGd_PreservesBreadcrumb()
state.IlEditorAnalyzer = state.Analyzer;
var callInst = dis.Value.Instructions.First(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("LocalTarget"));
- Assert.True(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
+ Assert.IsTrue(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
// Local path: ResetViewState was NEVER called — drill state intact.
- Assert.Same(origTree, state.CachedSizeTree);
- Assert.Same(type, state.TreemapCurrentLevel);
- Assert.Equal(2, state.TreemapBreadcrumb.Count);
+ Assert.AreSame(origTree, state.CachedSizeTree);
+ Assert.AreSame(type, state.TreemapCurrentLevel);
+ Assert.HasCount(2, state.TreemapBreadcrumb);
state.RestoreFromIlBackEntry(state.IlBackStack.Pop());
- Assert.Same(origTree, state.CachedSizeTree);
- Assert.Same(type, state.TreemapCurrentLevel);
- Assert.Equal(2, state.TreemapBreadcrumb.Count);
- Assert.Equal(methodIdx, state.TreemapSelectedIndex);
+ Assert.AreSame(origTree, state.CachedSizeTree);
+ Assert.AreSame(type, state.TreemapCurrentLevel);
+ Assert.HasCount(2, state.TreemapBreadcrumb);
+ Assert.AreEqual(methodIdx, state.TreemapSelectedIndex);
}
///
@@ -1076,10 +1100,11 @@ public void EscBack_FromLocalGd_PreservesBreadcrumb()
/// land back on Size Map at the original drilled type level — proven by waiting
/// for the rendered breadcrumb to include the type name.
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_FromSizeMapDeepDrill_TwoRealEscapesShowBreadcrumb()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -1105,7 +1130,7 @@ public async Task EscBack_FromSizeMapDeepDrill_TwoRealEscapesShowBreadcrumb()
// on the wrong tab and the test wouldn't prove the reopened bug.
_state.CurrentTab = TabId.SizeMap;
_state.NavigateToIlMethod(callExternal);
- Assert.Equal((TabId.SizeMap, 0), _state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.SizeMap, 0), _state.CrossViewBackTarget);
await auto.WaitUntilTextAsync("// Method: RichLibrary.IlNavigationFixture::CallExternal");
@@ -1123,7 +1148,7 @@ await auto.WaitUntilAsync(_ =>
var instructions = _state!.IlInstructions!.ToList();
var callIndex = instructions.FindIndex(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
- Assert.True(callIndex >= 0);
+ Assert.IsGreaterThanOrEqualTo(0, callIndex);
await SetIlCursorOnInstructionAsync(auto, instructions[callIndex]);
// Real gd via Enter — cross-assembly into System.Console.dll.
@@ -1144,9 +1169,9 @@ await auto.WaitUntilAsync(_ =>
var expectedCrumb = $" {origTree.Name} > {ns.Name} > {type.Name} ";
await auto.WaitUntilTextAsync(expectedCrumb);
- Assert.Equal(TabId.SizeMap, _state.CurrentTab);
- Assert.Same(type, _state.TreemapCurrentLevel);
- Assert.Same(origTree, _state.CachedSizeTree);
+ Assert.AreEqual(TabId.SizeMap, _state.CurrentTab);
+ Assert.AreSame(type, _state.TreemapCurrentLevel);
+ Assert.AreSame(origTree, _state.CachedSizeTree);
cts.Cancel();
await runTask;
@@ -1159,13 +1184,14 @@ await auto.WaitUntilAsync(_ =>
/// to the original Size Map drill — chained cross-view jumps must each store
/// their own back frame instead of overwriting one another.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromSizeMapIlHexChain_TwoEscsReturnToSizeMap()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CachedSizeTree = SizeAnalyzer.BuildSizeTree(state.Analyzer);
var origTree = state.CachedSizeTree;
@@ -1181,62 +1207,63 @@ public void EscBack_FromSizeMapIlHexChain_TwoEscsReturnToSizeMap()
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
state.NavigateToHexOffset(method.Rva);
- Assert.Equal(TabId.HexDump, state.CurrentTab);
- Assert.Equal((TabId.IlInspector, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.HexDump, state.CurrentTab);
+ Assert.AreEqual((TabId.IlInspector, 0), state.CrossViewBackTarget);
// First Esc — Hex → IL Inspector. Pre-fix the back target is null after
// this because the Hex push clobbered the SizeMap frame.
state.NavigateBack();
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
// Second Esc — IL → Size Map.
state.NavigateBack();
- Assert.Equal(TabId.SizeMap, state.CurrentTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.SizeMap, state.CurrentTab);
+ Assert.IsNull(state.CrossViewBackTarget);
// Breadcrumb survives end-to-end (NavigateToHexOffset never calls ResetViewState).
- Assert.Same(origTree, state.CachedSizeTree);
- Assert.Same(type, state.TreemapCurrentLevel);
- Assert.Equal(2, state.TreemapBreadcrumb.Count);
+ Assert.AreSame(origTree, state.CachedSizeTree);
+ Assert.AreSame(type, state.TreemapCurrentLevel);
+ Assert.HasCount(2, state.TreemapBreadcrumb);
}
///
/// Verifies that the PE → IL → Hex chain unwinds to the originating PE
/// Metadata sub-tab, proving both Tab and SubTab are stored per frame.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void EscBack_FromPeIlHexChain_RestoresPeWithExactSubTab()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.PeMetadata;
state.PeSubTab = PeSubTabId.MethodDef;
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.NavigateToIlMethod(method);
- Assert.Equal((TabId.PeMetadata, PeSubTabId.MethodDef), state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.PeMetadata, PeSubTabId.MethodDef), state.CrossViewBackTarget);
state.NavigateToHexOffset(method.Rva);
// The Hex frame captures (CurrentTab, PeSubTab) — PeSubTab is unchanged
// since the user was on IL Inspector, but the SubTab value is unused
// when NavigateBack returns to a non-PE tab. Only assert the Tab here.
- Assert.Equal(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
+ Assert.AreEqual(TabId.IlInspector, state.CrossViewBackTarget!.Value.Tab);
state.NavigateBack();
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
- Assert.Equal((TabId.PeMetadata, PeSubTabId.MethodDef), state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual((TabId.PeMetadata, PeSubTabId.MethodDef), state.CrossViewBackTarget);
state.NavigateBack();
- Assert.Equal(TabId.PeMetadata, state.CurrentTab);
- Assert.Equal(PeSubTabId.MethodDef, state.PeSubTab);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.PeMetadata, state.CurrentTab);
+ Assert.AreEqual(PeSubTabId.MethodDef, state.PeSubTab);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
@@ -1244,10 +1271,11 @@ public void EscBack_FromPeIlHexChain_RestoresPeWithExactSubTab()
/// Hex Dump → real Esc → real Esc must land back on Size Map at the
/// originating breadcrumb (proves the chain works through real input).
///
- [Fact(Timeout = 60_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public async Task EscBack_FromSizeMapIlHexChain_RealKeysReturnToSizeMap()
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None);
var (terminal, app) = CreateDotsiderApp();
var runTask = app.RunAsync(cts.Token);
var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(10));
@@ -1269,14 +1297,14 @@ public async Task EscBack_FromSizeMapIlHexChain_RealKeysReturnToSizeMap()
_state.CurrentTab = TabId.SizeMap;
_state.NavigateToIlMethod(callExternal);
- Assert.Equal((TabId.SizeMap, 0), _state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.SizeMap, 0), _state.CrossViewBackTarget);
await auto.WaitUntilTextAsync("// Method: RichLibrary.IlNavigationFixture::CallExternal");
// Real x key → NavigateToHexOffset (DllInspectorBindings.cs:221).
await auto.KeyAsync(Hex1bKey.X, cts.Token);
await auto.WaitUntilAsync(_ => _state!.CurrentTab == TabId.HexDump);
- Assert.Equal((TabId.IlInspector, 0), _state.CrossViewBackTarget);
+ Assert.AreEqual((TabId.IlInspector, 0), _state.CrossViewBackTarget);
// First REAL Esc — Hex → IL Inspector.
await auto.EscapeAsync(cts.Token);
@@ -1290,9 +1318,9 @@ public async Task EscBack_FromSizeMapIlHexChain_RealKeysReturnToSizeMap()
var expectedCrumb = $" {origTree.Name} > {ns.Name} > {type.Name} ";
await auto.WaitUntilTextAsync(expectedCrumb);
- Assert.Equal(TabId.SizeMap, _state.CurrentTab);
- Assert.Same(type, _state.TreemapCurrentLevel);
- Assert.Null(_state.CrossViewBackTarget);
+ Assert.AreEqual(TabId.SizeMap, _state.CurrentTab);
+ Assert.AreSame(type, _state.TreemapCurrentLevel);
+ Assert.IsNull(_state.CrossViewBackTarget);
cts.Cancel();
await runTask;
@@ -1302,25 +1330,26 @@ public async Task EscBack_FromSizeMapIlHexChain_RealKeysReturnToSizeMap()
/// ResetViewState (triggered by PushAssembly's dependency drill) clears the
/// entire cross-view back stack, not just the top frame.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ResetViewState_ClearsEntireBackStack()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
var method = state.Analyzer.MethodDefs.First(m => m.Rva > 0);
state.CurrentTab = TabId.SizeMap;
state.NavigateToIlMethod(method);
state.NavigateToHexOffset(method.Rva);
- Assert.Equal(2, state.CrossViewBackStack.Count);
+ Assert.HasCount(2, state.CrossViewBackStack);
// PushAssembly → ResetViewState clears everything
- state.PushAssembly(samples.HelloWorldDll);
+ state.PushAssembly(Samples.HelloWorldDll);
- Assert.Empty(state.CrossViewBackStack);
- Assert.Null(state.CrossViewBackTarget);
+ Assert.IsEmpty(state.CrossViewBackStack);
+ Assert.IsNull(state.CrossViewBackTarget);
}
///
@@ -1328,13 +1357,14 @@ public void ResetViewState_ClearsEntireBackStack()
/// not just the top tuple. Seeds a 2-deep stack before the gd push so a
/// single-tuple snapshot implementation would fail the count assertion.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void CrossAssemblyGd_SnapshotsAndRestoresMultiFrameStack()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
// Seed a multi-frame stack — bottom: PE/TypeDef, top: SizeMap.
state.CrossViewBackStack.Push((TabId.PeMetadata, PeSubTabId.TypeDef));
@@ -1345,7 +1375,7 @@ public void CrossAssemblyGd_SnapshotsAndRestoresMultiFrameStack()
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
state.IlSelectedMethod = method;
var dis = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(dis);
+ Assert.IsNotNull(dis);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(dis.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -1353,49 +1383,51 @@ public void CrossAssemblyGd_SnapshotsAndRestoresMultiFrameStack()
var callInst = dis.Value.Instructions.First(i =>
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
- Assert.True(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
+ Assert.IsTrue(state.NavigateToIlDefinition(callInst.MetadataToken!.Value));
// Cross-assembly push cleared the stack mid-flight.
- Assert.Empty(state.CrossViewBackStack);
+ Assert.IsEmpty(state.CrossViewBackStack);
state.RestoreFromIlBackEntry(state.IlBackStack.Pop());
// Whole stack is restored in correct top-first order.
var arr = state.CrossViewBackStack.ToArray();
- Assert.Equal(2, arr.Length);
- Assert.Equal((TabId.SizeMap, 0), arr[0]);
- Assert.Equal((TabId.PeMetadata, PeSubTabId.TypeDef), arr[1]);
+ Assert.HasCount(2, arr);
+ Assert.AreEqual((TabId.SizeMap, 0), arr[0]);
+ Assert.AreEqual((TabId.PeMetadata, PeSubTabId.TypeDef), arr[1]);
}
///
/// NavigateToHexOffset bails out early on an invalid RVA without mutating
/// the back stack — the early return precedes the push.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToHexOffset_InvalidRva_DoesNotMutateBackStack()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CrossViewBackStack.Push((TabId.SizeMap, 0));
- Assert.Single(state.CrossViewBackStack);
+ Assert.ContainsSingle(state.CrossViewBackStack);
// RvaToFileOffset returns -1 for an out-of-range RVA — early return fires.
state.NavigateToHexOffset(int.MaxValue);
- Assert.Single(state.CrossViewBackStack);
- Assert.Equal((TabId.SizeMap, 0), state.CrossViewBackTarget);
+ Assert.ContainsSingle(state.CrossViewBackStack);
+ Assert.AreEqual((TabId.SizeMap, 0), state.CrossViewBackTarget);
}
///
/// Verifies external method filters by declaring type.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ExternalMethod_FiltersByDeclaringType()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var dis = new IlDisassembler(analyzer);
// Find a method that calls an external method with a common name
@@ -1407,7 +1439,7 @@ public void ExternalMethod_FiltersByDeclaringType()
i.OpCode == "call" && i.MetadataToken is not null && i.Operand.Contains("WriteLine"));
var target = IlNavigationResolver.Resolve(analyzer, callInst.MetadataToken!.Value);
- var extMethod = Assert.IsType(target);
+ var extMethod = Assert.IsExactInstanceOfType(target);
// The resolver must capture the declaring type, not just the method name
Assert.Contains("Console", extMethod.DeclaringType);
@@ -1416,15 +1448,16 @@ public void ExternalMethod_FiltersByDeclaringType()
///
/// Verifies net fx external method navigates to mscorlib method.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NetFx_ExternalMethod_NavigatesToMscorlibMethod()
{
- if (samples.NetFxConsoleExe is null) return; // Windows-only sample
+ if (Samples.NetFxConsoleExe is null) return; // Windows-only sample
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.NetFxConsoleExe);
+ using var state = new DotsiderState(app, Samples.NetFxConsoleExe);
state.CurrentTab = TabId.IlInspector;
// NetFxConsole.Program::Main calls Console.WriteLine which references mscorlib in net48
@@ -1432,7 +1465,7 @@ public void NetFx_ExternalMethod_NavigatesToMscorlibMethod()
m.Name == "Main" && m.DeclaringType.Contains("Program"));
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -1444,42 +1477,44 @@ public void NetFx_ExternalMethod_NavigatesToMscorlibMethod()
// Resolver must identify this as an external method in mscorlib
var target = IlNavigationResolver.Resolve(state.Analyzer, callInst.MetadataToken!.Value);
- var extMethod = Assert.IsType(target);
- Assert.Equal("mscorlib", extMethod.AssemblyName);
+ var extMethod = Assert.IsExactInstanceOfType(target);
+ Assert.AreEqual("mscorlib", extMethod.AssemblyName);
// Navigation to mscorlib methods must succeed — the resolver should find
// System.Console.dll via namespace probing, not land on Internal.Console in CoreLib
var navigated = state.NavigateToIlDefinition(callInst.MetadataToken!.Value);
- Assert.True(navigated, $"Navigation failed with notice: {state.TransientNotice}");
- Assert.Null(state.TransientNotice);
- Assert.NotNull(state.IlSelectedMethod);
- Assert.Equal("System.Console", state.IlSelectedMethod.DeclaringType);
+ Assert.IsTrue(navigated, $"Navigation failed with notice: {state.TransientNotice}");
+ Assert.IsNull(state.TransientNotice);
+ Assert.IsNotNull(state.IlSelectedMethod);
+ Assert.AreEqual("System.Console", state.IlSelectedMethod.DeclaringType);
}
///
/// Verifies mscorlib resolver type forwarders find correct assembly.
///
- [Theory(Timeout = 30_000)]
- [InlineData("System.Console", "System.Console")]
- [InlineData("System.Object", "System.Private.CoreLib")]
- [InlineData("System.Collections.Queue", "System.Collections.NonGeneric")]
- [InlineData("Microsoft.Win32.RegistryKey", "Microsoft.Win32.Registry")]
- [InlineData("System.Environment/SpecialFolder", "System.Private.CoreLib")]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
+ [DataRow("System.Console", "System.Console")]
+ [DataRow("System.Object", "System.Private.CoreLib")]
+ [DataRow("System.Collections.Queue", "System.Collections.NonGeneric")]
+ [DataRow("Microsoft.Win32.RegistryKey", "Microsoft.Win32.Registry")]
+ [DataRow("System.Environment/SpecialFolder", "System.Private.CoreLib")]
public void MscorlibResolver_TypeForwarders_FindCorrectAssembly(
string declaringType, string expectedAssembly)
{
ImplementationAssemblyResolver.ClearCache();
var resolved = ImplementationAssemblyResolver.Resolve(
- samples.RichLibraryDll, "mscorlib", declaringType);
- Assert.NotNull(resolved);
- var fromFile = Assert.IsType(resolved);
+ Samples.RichLibraryDll, "mscorlib", declaringType);
+ Assert.IsNotNull(resolved);
+ var fromFile = Assert.IsExactInstanceOfType(resolved);
Assert.Contains(expectedAssembly, Path.GetFileNameWithoutExtension(fromFile.Path));
}
///
/// Verifies mscorlib resolver nested type forwarder follows parent chain.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void MscorlibResolver_NestedTypeForwarder_FollowsParentChain()
{
// System.Environment forwards to System.Private.CoreLib on modern .NET.
@@ -1487,32 +1522,33 @@ public void MscorlibResolver_NestedTypeForwarder_FollowsParentChain()
// proving the Implementation chain is followed rather than falling back.
ImplementationAssemblyResolver.ClearCache();
var parent = ImplementationAssemblyResolver.Resolve(
- samples.RichLibraryDll, "mscorlib", "System.Environment");
+ Samples.RichLibraryDll, "mscorlib", "System.Environment");
ImplementationAssemblyResolver.ClearCache();
var nested = ImplementationAssemblyResolver.Resolve(
- samples.RichLibraryDll, "mscorlib", "System.Environment/SpecialFolder");
- Assert.NotNull(parent);
- Assert.NotNull(nested);
- Assert.Equal(parent, nested);
+ Samples.RichLibraryDll, "mscorlib", "System.Environment/SpecialFolder");
+ Assert.IsNotNull(parent);
+ Assert.IsNotNull(nested);
+ Assert.AreEqual(parent, nested);
}
///
/// Verifies external method navigates to correct declaring type.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ExternalMethod_NavigatesToCorrectDeclaringType()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "CallExternal" && m.DeclaringType.Contains("IlNavigationFixture"));
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -1525,11 +1561,11 @@ public void ExternalMethod_NavigatesToCorrectDeclaringType()
if (navigated)
{
// Must have navigated to the correct assembly
- Assert.True(state.NavigationStack.Count > 0, "Should have pushed assembly");
+ Assert.IsGreaterThan(0, state.NavigationStack.Count, "Should have pushed assembly");
// The selected method's declaring type must contain "Console"
// (not some other type that also has a WriteLine-like method)
- Assert.NotNull(state.IlSelectedMethod);
+ Assert.IsNotNull(state.IlSelectedMethod);
Assert.Contains("Console", state.IlSelectedMethod.DeclaringType);
}
}
@@ -1537,13 +1573,14 @@ public void ExternalMethod_NavigatesToCorrectDeclaringType()
///
/// Verifies external field navigation sets il selected field.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void ExternalField_NavigationSetsIlSelectedField()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
// GetStringEmpty calls string.Empty which is ldsfld → ExternalField
@@ -1552,7 +1589,7 @@ public void ExternalField_NavigationSetsIlSelectedField()
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -1563,23 +1600,24 @@ public void ExternalField_NavigationSetsIlSelectedField()
i.OpCode == "ldsfld" && i.MetadataToken is not null && i.Operand.Contains("Empty"));
var target = IlNavigationResolver.Resolve(state.Analyzer, fieldInst.MetadataToken!.Value);
- Assert.IsType(target);
+ Assert.IsExactInstanceOfType(target);
var navigated = state.NavigateToIlDefinition(fieldInst.MetadataToken!.Value);
- Assert.True(navigated, "External field navigation must succeed");
+ Assert.IsTrue(navigated, "External field navigation must succeed");
// External field navigation must set IlSelectedField for the right pane
- Assert.NotNull(state.IlSelectedField);
+ Assert.IsNotNull(state.IlSelectedField);
Assert.Contains("Empty", state.IlSelectedField.Name);
// Must have pushed assembly
- Assert.True(state.NavigationStack.Count > 0);
+ Assert.IsGreaterThan(0, state.NavigationStack.Count);
// Esc back must restore
var entry = state.IlBackStack.Pop();
state.RestoreFromIlBackEntry(entry);
- Assert.Equal("GetStringEmpty", state.IlSelectedMethod?.Name);
- Assert.Null(state.IlSelectedField);
+ Assert.IsNotNull(state.IlSelectedMethod);
+ Assert.AreEqual("GetStringEmpty", state.IlSelectedMethod.Name);
+ Assert.IsNull(state.IlSelectedField);
}
///
@@ -1587,10 +1625,11 @@ public void ExternalField_NavigationSetsIlSelectedField()
/// UserService.FindByRole) and expects the underlying ExternalMethod target —
/// not a GenericInstantiation fallback.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Resolve_MethodSpec_ReturnsUnderlyingExternalMethod()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var dis = new IlDisassembler(analyzer);
var method = analyzer.MethodDefs.First(m =>
m.Name == "FindByRole" && m.DeclaringType.Contains("UserService"));
@@ -1601,9 +1640,9 @@ public void Resolve_MethodSpec_ReturnsUnderlyingExternalMethod()
var target = IlNavigationResolver.Resolve(analyzer, callInst.MetadataToken!.Value);
- var ext = Assert.IsType(target);
- Assert.Equal("Where", ext.MemberName);
- Assert.Equal("System.Linq.Enumerable", ext.DeclaringType);
+ var ext = Assert.IsExactInstanceOfType(target);
+ Assert.AreEqual("Where", ext.MemberName);
+ Assert.AreEqual("System.Linq.Enumerable", ext.DeclaringType);
// Signature should encode the method generic parameter as !!0, proving
// we resolved the open-generic definition rather than coincidental match.
Assert.Contains("!!0", ext.Signature);
@@ -1612,20 +1651,21 @@ public void Resolve_MethodSpec_ReturnsUnderlyingExternalMethod()
///
/// Navigates from a MethodSpec call site to the open-generic definition in System.Linq.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void NavigateToIlDefinition_MethodSpec_LandsOnOpenGenericDefinition()
{
var app = new Hex1bApp(
_ => Task.FromResult(new TextBlockWidget("test")),
new Hex1bAppOptions { WorkloadAdapter = new Hex1bAppWorkloadAdapter() });
- using var state = new DotsiderState(app, samples.RichLibraryDll);
+ using var state = new DotsiderState(app, Samples.RichLibraryDll);
state.CurrentTab = TabId.IlInspector;
var method = state.Analyzer.MethodDefs.First(m =>
m.Name == "FindByRole" && m.DeclaringType.Contains("UserService"));
state.IlSelectedMethod = method;
var result = state.IlDisassembler!.DisassembleWithText(method);
- Assert.NotNull(result);
+ Assert.IsNotNull(result);
state.IlEditorState = new EditorState(
new Hex1b.Documents.Hex1bDocument(result.Value.Text)) { IsReadOnly = true };
state.IlEditorMethod = method;
@@ -1638,13 +1678,13 @@ public void NavigateToIlDefinition_MethodSpec_LandsOnOpenGenericDefinition()
var navigated = state.NavigateToIlDefinition(callInst.MetadataToken!.Value);
- Assert.True(navigated, "MethodSpec navigation must succeed for Enumerable.Where");
- Assert.True(state.NavigationStack.Count > 0, "Should have pushed System.Linq");
- Assert.NotNull(state.IlSelectedMethod);
- Assert.Equal("Where", state.IlSelectedMethod.Name);
- Assert.Equal("System.Linq.Enumerable", state.IlSelectedMethod.DeclaringType);
+ Assert.IsTrue(navigated, "MethodSpec navigation must succeed for Enumerable.Where");
+ Assert.IsGreaterThan(0, state.NavigationStack.Count, "Should have pushed System.Linq");
+ Assert.IsNotNull(state.IlSelectedMethod);
+ Assert.AreEqual("Where", state.IlSelectedMethod.Name);
+ Assert.AreEqual("System.Linq.Enumerable", state.IlSelectedMethod.DeclaringType);
Assert.Contains("!!0", state.IlSelectedMethod.Signature);
- Assert.Equal(TabId.IlInspector, state.CurrentTab);
+ Assert.AreEqual(TabId.IlInspector, state.CurrentTab);
}
///
@@ -1653,10 +1693,11 @@ public void NavigateToIlDefinition_MethodSpec_LandsOnOpenGenericDefinition()
/// the parent is a TypeSpec, not a TypeRef. The resolver must still identify it as
/// an ExternalMethod, not return Unsupported.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Resolve_MemberRefWithTypeSpecParent_ReturnsExternalMethod()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
var dis = new IlDisassembler(analyzer);
var method = analyzer.MethodDefs.First(m =>
@@ -1667,8 +1708,8 @@ public void Resolve_MemberRefWithTypeSpecParent_ReturnsExternalMethod()
var target = IlNavigationResolver.Resolve(analyzer, callInst.MetadataToken!.Value);
- var ext = Assert.IsType(target);
- Assert.Equal("set_Item", ext.MemberName);
+ var ext = Assert.IsExactInstanceOfType(target);
+ Assert.AreEqual("set_Item", ext.MemberName);
Assert.Contains("ConcurrentDictionary", ext.DeclaringType);
}
@@ -1676,27 +1717,28 @@ public void Resolve_MemberRefWithTypeSpecParent_ReturnsExternalMethod()
/// A malformed MethodSpec token must surface as GenericInstantiation with a
/// non-empty Reason, and the DotsiderState arm must report it via TransientNotice.
///
- [Fact(Timeout = 30_000)]
+ [TestMethod]
+ [Timeout(30_000, CooperativeCancellation = true)]
public void Resolve_InvalidMethodSpecToken_ReturnsGenericInstantiationWithReason()
{
- using var analyzer = new AssemblyAnalyzer(samples.RichLibraryDll);
+ using var analyzer = new AssemblyAnalyzer(Samples.RichLibraryDll);
// HandleKind.MethodSpecification = 0x2B. Row 0xFFFFFF is well past any real row,
// so reader.GetMethodSpecification will throw BadImageFormatException.
var invalidToken = unchecked((int)0x2BFFFFFF);
var target = IlNavigationResolver.Resolve(analyzer, invalidToken);
- var gi = Assert.IsType(target);
- Assert.Equal(invalidToken, gi.Token);
- Assert.False(string.IsNullOrEmpty(gi.Reason));
+ var gi = Assert.IsExactInstanceOfType(target);
+ Assert.AreEqual(invalidToken, gi.Token);
+ Assert.IsFalse(string.IsNullOrEmpty(gi.Reason));
var app = new Hex1bApp(
_ => Task.FromResult