diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c841af1..cc89e2f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -211,6 +211,9 @@ jobs: dotnet-version: ['8.x', '9.x', '10.x'] steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Download package uses: actions/download-artifact@v7 with: @@ -253,6 +256,17 @@ jobs: || { echo "✗ Self-validation failed"; exit 1; } echo "✓ Self-validation succeeded" + - name: Capture tool versions + shell: bash + run: | + echo "Capturing tool versions..." + # Create short job ID: int-win-8, int-win-9, int-ubuntu-8, etc. + OS_SHORT=$(echo "${{ matrix.os }}" | sed 's/windows-latest/win/;s/ubuntu-latest/ubuntu/') + DOTNET_SHORT=$(echo "${{ matrix.dotnet-version }}" | sed 's/\.x$//') + JOB_ID="int-${OS_SHORT}-${DOTNET_SHORT}" + versionmark --capture --job-id "${JOB_ID}" -- dotnet git + echo "✓ Tool versions captured" + - name: Upload validation test results if: always() uses: actions/upload-artifact@v6 @@ -260,6 +274,12 @@ jobs: name: validation-test-results-${{ matrix.os }}-dotnet${{ matrix.dotnet-version }} path: validation-${{ matrix.os }}-dotnet${{ matrix.dotnet-version }}.trx + - name: Upload version capture + uses: actions/upload-artifact@v6 + with: + name: version-capture-${{ matrix.os }}-dotnet${{ matrix.dotnet-version }} + path: versionmark-int-*.json + # Builds the supporting documentation including user guides, requirements, # trace matrices, code quality reports, and build notes. build-docs: @@ -297,6 +317,13 @@ jobs: name: codeql-sarif path: codeql-results + - name: Download all version captures + uses: actions/download-artifact@v7 + with: + path: version-captures + pattern: 'version-capture-*' + continue-on-error: true + - name: Setup dotnet uses: actions/setup-dotnet@v5 with: @@ -317,6 +344,14 @@ jobs: - name: Restore Tools run: dotnet tool restore + - name: Capture tool versions for build-docs + shell: bash + run: | + echo "Capturing tool versions..." + versionmark --capture --job-id "build-docs" -- \ + dotnet git node npm pandoc weasyprint sarifmark sonarmark reqstream buildmark + echo "✓ Tool versions captured" + - name: Generate Requirements Report, Justifications, and Trace Matrix run: > dotnet reqstream @@ -370,6 +405,20 @@ jobs: --report docs/buildnotes.md --report-depth 1 + - name: Publish Tool Versions + shell: bash + run: | + echo "Publishing tool versions..." + versionmark --publish --report docs/buildnotes/versions.md --report-depth 1 \ + -- "versionmark-*.json" "version-captures/**/versionmark-*.json" + echo "✓ Tool versions published" + + - name: Display Tool Versions Report + shell: bash + run: | + echo "=== Tool Versions Report ===" + cat docs/buildnotes/versions.md + - name: Generate Build Notes HTML with Pandoc shell: bash run: > diff --git a/docs/buildnotes/definition.yaml b/docs/buildnotes/definition.yaml index ce05539..62699f2 100644 --- a/docs/buildnotes/definition.yaml +++ b/docs/buildnotes/definition.yaml @@ -6,6 +6,7 @@ input-files: - docs/buildnotes/title.txt - docs/buildnotes/introduction.md - docs/buildnotes.md + - docs/buildnotes/versions.md template: template.html table-of-contents: true number-sections: true diff --git a/src/DemaConsulting.VersionMark/MarkdownFormatter.cs b/src/DemaConsulting.VersionMark/MarkdownFormatter.cs index 6094568..0422b88 100644 --- a/src/DemaConsulting.VersionMark/MarkdownFormatter.cs +++ b/src/DemaConsulting.VersionMark/MarkdownFormatter.cs @@ -98,49 +98,43 @@ private static string GenerateMarkdown( foreach (var tool in sortedTools) { var versions = toolVersions[tool]; - var versionEntry = FormatVersionEntry(versions); - markdown.AppendLine($"- **{tool}**: {versionEntry}"); + FormatVersionEntries(markdown, tool, versions); } return markdown.ToString(); } /// - /// Formats a version entry for a tool based on whether versions are uniform or different. + /// Formats version entries for a tool as multiple bullets when versions differ. /// + /// The StringBuilder to append to. + /// The tool name. /// List of job ID and version pairs for a tool. - /// Formatted version string with job IDs when appropriate. - private static string FormatVersionEntry(List<(string JobId, string Version)> versions) + private static void FormatVersionEntries(StringBuilder markdown, string tool, List<(string JobId, string Version)> versions) { // Check if all versions are the same var distinctVersions = versions.Select(v => v.Version).Distinct().ToList(); - // If all versions are the same, show "All jobs" + // If all versions are the same, show single entry without job IDs if (distinctVersions.Count == 1) { - return $"{distinctVersions[0]} (All jobs)"; + markdown.AppendLine($"- **{tool}**: {distinctVersions[0]}"); + return; } - // Otherwise, group by version and show job IDs - // When versions differ across jobs, we need to show which jobs have which versions + // Otherwise, create multiple bullets - one for each version group var versionGroups = versions .GroupBy(v => v.Version) .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase); - // Build formatted version strings with subscripted job IDs - // Each version gets its own entry showing which jobs use it - var formattedVersions = new List(); foreach (var group in versionGroups) { // For each unique version, collect and sort the job IDs that use it var jobIds = group.Select(v => v.JobId).OrderBy(j => j, StringComparer.OrdinalIgnoreCase); var jobIdList = string.Join(", ", jobIds); - // Format as "version (job1, job2)" for HTML subscript rendering - formattedVersions.Add($"{group.Key} ({jobIdList})"); + // Format as separate bullet with tool name and version, showing which jobs use it + markdown.AppendLine($"- **{tool}**: {group.Key} ({jobIdList})"); } - - // Join all version entries with commas to create the final output - return string.Join(", ", formattedVersions); } } diff --git a/src/DemaConsulting.VersionMark/Validation.cs b/src/DemaConsulting.VersionMark/Validation.cs index 01f3f70..63f54eb 100644 --- a/src/DemaConsulting.VersionMark/Validation.cs +++ b/src/DemaConsulting.VersionMark/Validation.cs @@ -156,15 +156,15 @@ private static void RunCaptureTest(Context context, DemaConsulting.TestResults.T test.ErrorMessage = $"Expected job-id 'test-job', got '{versionInfo.JobId}'"; context.WriteError($"✗ Captures Versions Test - FAILED: Wrong job-id"); } - // Verify dotnet version was captured - else if (!versionInfo.Versions.ContainsKey("dotnet")) + // Verify dotnet version was captured and is not empty + else if (!versionInfo.Versions.TryGetValue("dotnet", out var dotnetVersion)) { test.Outcome = DemaConsulting.TestResults.TestOutcome.Failed; test.ErrorMessage = "Output JSON missing 'dotnet' version"; context.WriteError($"✗ Captures Versions Test - FAILED: Missing dotnet version"); } // Verify dotnet version is not empty - else if (string.IsNullOrWhiteSpace(versionInfo.Versions["dotnet"])) + else if (string.IsNullOrWhiteSpace(dotnetVersion)) { test.Outcome = DemaConsulting.TestResults.TestOutcome.Failed; test.ErrorMessage = "Dotnet version is empty"; diff --git a/src/DemaConsulting.VersionMark/VersionMarkConfig.cs b/src/DemaConsulting.VersionMark/VersionMarkConfig.cs index 48c54c9..09a2c46 100644 --- a/src/DemaConsulting.VersionMark/VersionMarkConfig.cs +++ b/src/DemaConsulting.VersionMark/VersionMarkConfig.cs @@ -336,27 +336,22 @@ public VersionInfo FindVersions(IEnumerable toolNames, string jobId) /// private static string RunCommand(string command) { - // Split command into executable and arguments - var parts = command.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) - { - throw new InvalidOperationException("Command is empty"); - } - - var fileName = parts[0]; - var arguments = parts.Length > 1 ? parts[1] : string.Empty; + // To support .cmd/.bat files on Windows and shell features on all platforms, + // we run commands through the appropriate shell using ArgumentList to avoid escaping issues + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); try { var processStartInfo = new ProcessStartInfo { - FileName = fileName, - Arguments = arguments, + FileName = isWindows ? "cmd.exe" : "/bin/sh", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + processStartInfo.ArgumentList.Add(isWindows ? "/c" : "-c"); + processStartInfo.ArgumentList.Add(command); using var process = Process.Start(processStartInfo); if (process == null) @@ -371,6 +366,13 @@ private static string RunCommand(string command) var output = outputTask.Result; var error = errorTask.Result; + // Check exit code - if non-zero, command failed + if (process.ExitCode != 0) + { + var errorMessage = string.IsNullOrEmpty(error) ? output : error; + throw new InvalidOperationException($"Failed to run command '{command}': {errorMessage}"); + } + // Combine stdout and stderr with newline separator for better debuggability if (string.IsNullOrEmpty(error)) { diff --git a/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs b/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs index d7eb3e1..ebe0387 100644 --- a/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs @@ -229,81 +229,130 @@ public void IntegrationTest_UnknownArgument_ReturnsError() [TestMethod] public void IntegrationTest_CaptureCommand_CapturesToolVersions() { - // Arrange - Set up test output file and find repository root with config file - var outputFile = Path.GetTempFileName(); - var currentDir = Directory.GetCurrentDirectory(); + // Arrange - Set up unique temp directory with config file + using var testDir = new TestDirectory(copyConfig: true); + var outputFile = PathHelpers.SafePathCombine(testDir.Path, "output.json"); - // Find the repository root by looking for .versionmark.yaml - var repoRoot = FindRepositoryRoot(currentDir); - if (string.IsNullOrEmpty(repoRoot)) - { - Assert.Inconclusive("Could not find repository root with .versionmark.yaml"); - return; - } + // Act - Run the capture command with specific tools + var exitCode = Runner.Run( + out var output, + "dotnet", + _dllPath, + "--capture", + "--job-id", $"test-job-{Guid.NewGuid():N}", + "--output", outputFile, + "--", "dotnet", "git"); - try - { - // Change to repository root where .versionmark.yaml exists - Directory.SetCurrentDirectory(repoRoot); + // Assert - Verify the command succeeded and captured the expected tool versions + // Verify success + Assert.AreEqual(0, exitCode); - // Act - Run the capture command with specific tools - var exitCode = Runner.Run( - out var output, - "dotnet", - _dllPath, - "--capture", - "--job-id", "test-job", - "--output", outputFile, - "--", "dotnet", "git"); + // Verify output contains expected information + Assert.Contains("Capturing tool versions", output); + Assert.Contains("dotnet", output); + Assert.Contains("git", output); - // Assert - Verify the command succeeded and captured the expected tool versions - // Verify success - Assert.AreEqual(0, exitCode); + // Verify output file was created + Assert.IsTrue(File.Exists(outputFile), "Output file was not created"); - // Verify output contains expected information - Assert.Contains("Capturing tool versions", output); - Assert.Contains("test-job", output); - Assert.Contains("dotnet", output); - Assert.Contains("git", output); + // Verify output file contains expected data + var versionInfo = VersionInfo.LoadFromFile(outputFile); + Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet")); + Assert.IsTrue(versionInfo.Versions.ContainsKey("git")); + } - // Verify output file was created - Assert.IsTrue(File.Exists(outputFile), "Output file was not created"); + /// + /// Helper class for managing isolated test directories. + /// Creates a unique temp directory with optional .versionmark.yaml config. + /// Automatically cleans up on disposal. + /// + private sealed class TestDirectory : IDisposable + { + private readonly string _originalDirectory; + private bool _disposed; + + /// + /// Gets the path to the test directory. + /// + public string Path { get; } + + /// + /// Creates a new isolated test directory. + /// + /// Whether to copy .versionmark.yaml from repo root. + public TestDirectory(bool copyConfig = false) + { + _originalDirectory = Directory.GetCurrentDirectory(); + Path = PathHelpers.SafePathCombine(System.IO.Path.GetTempPath(), $"versionmark-test-{Guid.NewGuid():N}"); + + Directory.CreateDirectory(Path); + + if (copyConfig) + { + var repoRoot = FindRepositoryRoot(_originalDirectory); + if (!string.IsNullOrEmpty(repoRoot)) + { + var configSource = PathHelpers.SafePathCombine(repoRoot, ".versionmark.yaml"); + var configDest = PathHelpers.SafePathCombine(Path, ".versionmark.yaml"); + File.Copy(configSource, configDest); + } + } - // Verify output file contains expected data - var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.AreEqual("test-job", versionInfo.JobId); - Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet")); - Assert.IsTrue(versionInfo.Versions.ContainsKey("git")); + Directory.SetCurrentDirectory(Path); } - finally + + /// + /// Disposes the test directory and restores the original directory. + /// + public void Dispose() { - // Restore original directory - Directory.SetCurrentDirectory(currentDir); + if (_disposed) + { + return; + } + + try + { + Directory.SetCurrentDirectory(_originalDirectory); + } + catch + { + // Ignore errors restoring directory + } - if (File.Exists(outputFile)) + try { - File.Delete(outputFile); + if (Directory.Exists(Path)) + { + Directory.Delete(Path, true); + } } + catch + { + // Ignore cleanup errors + } + + _disposed = true; } - } - /// - /// Helper method to find the repository root. - /// - /// Starting directory. - /// Repository root path or empty string if not found. - private static string FindRepositoryRoot(string startPath) - { - var dir = new DirectoryInfo(startPath); - while (dir != null) + /// + /// Helper method to find the repository root. + /// + /// Starting directory. + /// Repository root path or empty string if not found. + private static string FindRepositoryRoot(string startPath) { - if (File.Exists(PathHelpers.SafePathCombine(dir.FullName, ".versionmark.yaml"))) + var dir = new DirectoryInfo(startPath); + while (dir != null) { - return dir.FullName; + if (File.Exists(PathHelpers.SafePathCombine(dir.FullName, ".versionmark.yaml"))) + { + return dir.FullName; + } + dir = dir.Parent; } - dir = dir.Parent; + return string.Empty; } - return string.Empty; } /// @@ -333,36 +382,19 @@ public void IntegrationTest_CaptureCommandWithoutJobId_ReturnsError() public void IntegrationTest_CaptureCommandWithMissingConfig_ReturnsError() { // Arrange - Create temp directory without .versionmark.yaml config file - var currentDir = Directory.GetCurrentDirectory(); - var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); - - try - { - // Create and change to temp directory without .versionmark.yaml - Directory.CreateDirectory(tempDir); - Directory.SetCurrentDirectory(tempDir); + using var testDir = new TestDirectory(copyConfig: false); - // Act - Run capture command in directory without config file - var exitCode = Runner.Run( - out var output, - "dotnet", - _dllPath, - "--capture", - "--job-id", "test-job"); + // Act - Run capture command in directory without config file + var exitCode = Runner.Run( + out var output, + "dotnet", + _dllPath, + "--capture", + "--job-id", "test-job"); - // Assert - Verify the command fails with error message about missing config - Assert.AreNotEqual(0, exitCode); - Assert.Contains("Error:", output); - } - finally - { - // Restore original directory and cleanup - Directory.SetCurrentDirectory(currentDir); - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } + // Assert - Verify the command fails with error message about missing config + Assert.AreNotEqual(0, exitCode); + Assert.Contains("Error:", output); } /// @@ -372,58 +404,31 @@ public void IntegrationTest_CaptureCommandWithMissingConfig_ReturnsError() public void IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename() { // Arrange - Set up to test default output filename generation - var outputFile = "versionmark-integration-test-job.json"; - var currentDir = Directory.GetCurrentDirectory(); - - // Find the repository root by looking for .versionmark.yaml - var repoRoot = FindRepositoryRoot(currentDir); - if (string.IsNullOrEmpty(repoRoot)) - { - Assert.Inconclusive("Could not find repository root with .versionmark.yaml"); - return; - } + // Use a unique job ID to avoid conflicts with parallel test execution + var jobId = $"integration-test-job-{Guid.NewGuid():N}"; + var outputFile = $"versionmark-{jobId}.json"; + + using var testDir = new TestDirectory(copyConfig: true); - try - { - // Change to repository root where .versionmark.yaml exists - Directory.SetCurrentDirectory(repoRoot); - - // Delete output file if it exists - if (File.Exists(outputFile)) - { - File.Delete(outputFile); - } - - // Act - Run capture command without specifying --output parameter - var exitCode = Runner.Run( - out var _, - "dotnet", - _dllPath, - "--capture", - "--job-id", "integration-test-job", - "--", "dotnet"); - - // Assert - Verify command succeeded and created file with default name pattern - // Verify success - Assert.AreEqual(0, exitCode); + // Act - Run capture command without specifying --output parameter + var exitCode = Runner.Run( + out var _, + "dotnet", + _dllPath, + "--capture", + "--job-id", jobId, + "--", "dotnet"); - // Verify output file was created with default name - Assert.IsTrue(File.Exists(outputFile), $"Output file '{outputFile}' was not created"); + // Assert - Verify command succeeded and created file with default name pattern + // Verify success + Assert.AreEqual(0, exitCode); - // Verify output file contains expected data - var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.AreEqual("integration-test-job", versionInfo.JobId); - } - finally - { - // Restore original directory - Directory.SetCurrentDirectory(currentDir); + // Verify output file was created with default name + Assert.IsTrue(File.Exists(outputFile), $"Output file '{outputFile}' was not created"); - if (File.Exists(outputFile)) - { - File.Delete(outputFile); - } - } + // Verify output file contains expected data + var versionInfo = VersionInfo.LoadFromFile(outputFile); + Assert.AreEqual(jobId, versionInfo.JobId); } /// @@ -435,77 +440,62 @@ public void IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename( public void VersionMark_PublishCommand_GeneratesMarkdownReport() { // Arrange - Set up unique temp directory with multiple JSON files - var currentDir = Directory.GetCurrentDirectory(); - var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); - var json1File = PathHelpers.SafePathCombine(tempDir, "versionmark-job1.json"); - var json2File = PathHelpers.SafePathCombine(tempDir, "versionmark-job2.json"); - var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); - - try - { - Directory.CreateDirectory(tempDir); - Directory.SetCurrentDirectory(tempDir); - - // Create test JSON files - var versionInfo1 = new VersionInfo( - "job-1", - new Dictionary - { - ["dotnet"] = "8.0.0", - ["node"] = "18.0.0", - ["python"] = "3.11.0" - }); - var versionInfo2 = new VersionInfo( - "job-2", - new Dictionary - { - ["dotnet"] = "8.0.0", - ["node"] = "20.0.0", - ["python"] = "3.11.0" - }); + using var testDir = new TestDirectory(copyConfig: false); + var json1File = PathHelpers.SafePathCombine(testDir.Path, "versionmark-job1.json"); + var json2File = PathHelpers.SafePathCombine(testDir.Path, "versionmark-job2.json"); + var reportFile = PathHelpers.SafePathCombine(testDir.Path, "report.md"); + + // Create test JSON files + var versionInfo1 = new VersionInfo( + "job-1", + new Dictionary + { + ["dotnet"] = "8.0.0", + ["node"] = "18.0.0", + ["python"] = "3.11.0" + }); + var versionInfo2 = new VersionInfo( + "job-2", + new Dictionary + { + ["dotnet"] = "8.0.0", + ["node"] = "20.0.0", + ["python"] = "3.11.0" + }); - versionInfo1.SaveToFile(json1File); - versionInfo2.SaveToFile(json2File); + versionInfo1.SaveToFile(json1File); + versionInfo2.SaveToFile(json2File); - // Act - Run publish command - var exitCode = Runner.Run( - out var output, - "dotnet", - _dllPath, - "--publish", - "--report", reportFile); + // Act - Run publish command + var exitCode = Runner.Run( + out var output, + "dotnet", + _dllPath, + "--publish", + "--report", reportFile); - // Assert - Verify command succeeded - // What is proved: Publish command successfully generates markdown from JSON files - Assert.AreEqual(0, exitCode, $"Command failed with output: {output}"); - Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); + // Assert - Verify command succeeded + // What is proved: Publish command successfully generates markdown from JSON files + Assert.AreEqual(0, exitCode, $"Command failed with output: {output}"); + Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); - var reportContent = File.ReadAllText(reportFile); + var reportContent = File.ReadAllText(reportFile); - // Verify markdown structure - Assert.Contains("## Tool Versions", reportContent); + // Verify markdown structure + Assert.Contains("## Tool Versions", reportContent); - // Verify tools are present and sorted alphabetically - Assert.Contains("dotnet", reportContent); - Assert.Contains("node", reportContent); - Assert.Contains("python", reportContent); + // Verify tools are present and sorted alphabetically + Assert.Contains("dotnet", reportContent); + Assert.Contains("node", reportContent); + Assert.Contains("python", reportContent); - // Verify version formatting (uniform versions show "All jobs") - Assert.Contains("8.0.0 (All jobs)", reportContent); // dotnet uniform - Assert.Contains("3.11.0 (All jobs)", reportContent); // python uniform + // Verify version formatting (uniform versions show just the version) + Assert.Contains("- **dotnet**: 8.0.0", reportContent); // dotnet uniform, no job IDs + Assert.Contains("- **python**: 3.11.0", reportContent); // python uniform, no job IDs - // Verify version formatting (different versions show job IDs) - Assert.Contains("18.0.0", reportContent); - Assert.Contains("20.0.0", reportContent); - } - finally - { - Directory.SetCurrentDirectory(currentDir); - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } - } + // Verify version formatting (different versions show job IDs in parentheses) + Assert.Contains("18.0.0 (job-", reportContent); + Assert.Contains("20.0.0 (job-", reportContent); } /// @@ -518,7 +508,7 @@ public void VersionMark_PublishCommand_WithReportDepth_AdjustsHeadingLevels() { // Arrange - Set up unique temp directory with JSON file var currentDir = Directory.GetCurrentDirectory(); - var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempDir = PathHelpers.SafePathCombine(System.IO.Path.GetTempPath(), $"versionmark-test-{Guid.NewGuid():N}"); var jsonFile = PathHelpers.SafePathCombine(tempDir, "versionmark-job1.json"); var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); @@ -591,7 +581,7 @@ public void VersionMark_PublishCommandWithNoMatchingFiles_ReturnsError() { // Arrange - Set up empty temp directory var currentDir = Directory.GetCurrentDirectory(); - var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempDir = PathHelpers.SafePathCombine(System.IO.Path.GetTempPath(), $"versionmark-test-{Guid.NewGuid():N}"); var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); try @@ -633,7 +623,7 @@ public void VersionMark_PublishCommandWithInvalidJson_ReturnsError() { // Arrange - Set up temp directory with invalid JSON file var currentDir = Directory.GetCurrentDirectory(); - var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempDir = PathHelpers.SafePathCombine(System.IO.Path.GetTempPath(), $"versionmark-test-{Guid.NewGuid():N}"); var invalidJsonFile = PathHelpers.SafePathCombine(tempDir, "versionmark-invalid.json"); var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); @@ -678,7 +668,7 @@ public void VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles() { // Arrange - Set up temp directory with multiple JSON files var currentDir = Directory.GetCurrentDirectory(); - var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var tempDir = PathHelpers.SafePathCombine(System.IO.Path.GetTempPath(), $"versionmark-test-{Guid.NewGuid():N}"); var includedFile = PathHelpers.SafePathCombine(tempDir, "included-job1.json"); var excludedFile = PathHelpers.SafePathCombine(tempDir, "versionmark-excluded.json"); var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); diff --git a/test/DemaConsulting.VersionMark.Tests/MarkdownFormatterTests.cs b/test/DemaConsulting.VersionMark.Tests/MarkdownFormatterTests.cs index dcd74ad..7869731 100644 --- a/test/DemaConsulting.VersionMark.Tests/MarkdownFormatterTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/MarkdownFormatterTests.cs @@ -64,9 +64,9 @@ public void MarkdownFormatter_FormatVersions_SortsToolsAlphabetically() } /// - /// Test that MarkdownFormatter shows "All jobs" when versions are uniform across all jobs. - /// What is tested: FMT-002 - Versions that are the same across all jobs are collapsed to "All jobs" - /// What the assertions prove: The output displays "(All jobs)" when all jobs have the same version + /// Test that MarkdownFormatter shows version without job IDs when versions are uniform. + /// What is tested: FMT-002 - Versions that are the same across all jobs show just the version + /// What the assertions prove: The output displays only the version when all jobs have the same version /// [TestMethod] public void MarkdownFormatter_FormatVersions_WithUniformVersions_ShowsAllJobs() @@ -100,16 +100,21 @@ public void MarkdownFormatter_FormatVersions_WithUniformVersions_ShowsAllJobs() // Act - Format the version information var result = MarkdownFormatter.Format(versionInfos); - // Assert - Verify "All jobs" appears for uniform versions - // What is proved: When all jobs have the same version, the output shows "(All jobs)" - Assert.Contains("8.0.0 (All jobs)", result); - Assert.Contains("18.0.0 (All jobs)", result); + // Assert - Verify uniform versions show without job IDs + // What is proved: When all jobs have the same version, only the version is shown + Assert.Contains("- **dotnet**: 8.0.0", result); + Assert.Contains("- **node**: 18.0.0", result); + + // Verify no job IDs appear since versions are uniform + Assert.DoesNotContain("job-1", result); + Assert.DoesNotContain("job-2", result); + Assert.DoesNotContain("job-3", result); } /// /// Test that MarkdownFormatter shows individual job IDs when versions differ across jobs. - /// What is tested: FMT-003, FMT-004 - Different versions show job IDs in subscript format - /// What the assertions prove: The output displays job IDs in subscript format when versions differ + /// What is tested: FMT-003, FMT-004 - Different versions show job IDs in parentheses + /// What the assertions prove: The output displays job IDs in parentheses when versions differ /// [TestMethod] public void MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividualJobs() @@ -143,19 +148,16 @@ public void MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividu // Act - Format the version information var result = MarkdownFormatter.Format(versionInfos); - // Assert - Verify individual job IDs are shown with subscript formatting - // What is proved: Different versions show job IDs in subscript format (job-id) + // Assert - Verify individual job IDs are shown in parentheses + // What is proved: Different versions show job IDs in parentheses // For dotnet: two different versions (7.0.0 and 8.0.0) - Assert.Contains("(job-3)", result); - Assert.Contains("(job-1, job-2)", result); + Assert.Contains("(job-3)", result); + Assert.Contains("(job-1, job-2)", result); // For node: two different versions (18.0.0 and 20.0.0) - Assert.Contains("(job-1, job-3)", result); - Assert.Contains("(job-2)", result); - - // Verify "All jobs" does NOT appear since versions differ - Assert.DoesNotContain("(All jobs)", result); + Assert.Contains("(job-1, job-3)", result); + Assert.Contains("(job-2)", result); } /// @@ -220,8 +222,8 @@ public void MarkdownFormatter_FormatVersions_EmptyList_ProducesHeaderOnly() /// /// Test that MarkdownFormatter handles a single job correctly. - /// What is tested: Edge case - single job shows "All jobs" notation - /// What the assertions prove: Single job is treated as uniform (shows "All jobs") + /// What is tested: Edge case - single job shows just the version + /// What the assertions prove: Single job is treated as uniform (shows version only) /// [TestMethod] public void MarkdownFormatter_FormatVersions_SingleJob_ShowsAllJobs() @@ -241,10 +243,11 @@ public void MarkdownFormatter_FormatVersions_SingleJob_ShowsAllJobs() // Act - Format the version information var result = MarkdownFormatter.Format(versionInfos); - // Assert - Verify "All jobs" appears for single job - // What is proved: Single job is treated as uniform and shows "(All jobs)" - Assert.Contains("8.0.0 (All jobs)", result); - Assert.Contains("18.0.0 (All jobs)", result); + // Assert - Verify version appears without job IDs for single job + // What is proved: Single job is treated as uniform and shows version only + Assert.Contains("- **dotnet**: 8.0.0", result); + Assert.Contains("- **node**: 18.0.0", result); + Assert.DoesNotContain("job-1", result); } /// @@ -279,17 +282,17 @@ public void MarkdownFormatter_FormatVersions_MixedVersions_HandlesCorrectly() // Act - Format the version information var result = MarkdownFormatter.Format(versionInfos); - // Assert - Verify uniform tools show "All jobs" and different tools show job IDs + // Assert - Verify uniform tools show version only and different tools show job IDs // What is proved: Formatter correctly distinguishes uniform from varying versions - Assert.Contains("8.0.0 (All jobs)", result); // dotnet is uniform - Assert.Contains("3.11.0 (All jobs)", result); // python is uniform - Assert.Contains("(job-1)", result); // node differs - Assert.Contains("(job-2)", result); // node differs + Assert.Contains("- **dotnet**: 8.0.0", result); // dotnet is uniform, no job IDs + Assert.Contains("- **python**: 3.11.0", result); // python is uniform, no job IDs + Assert.Contains("(job-1)", result); // node differs + Assert.Contains("(job-2)", result); // node differs } /// - /// Test that MarkdownFormatter sorts job IDs alphabetically within subscript. - /// What is tested: Job IDs are sorted within subscript when multiple jobs share a version + /// Test that MarkdownFormatter sorts job IDs alphabetically when multiple jobs share a version. + /// What is tested: Job IDs are sorted alphabetically when multiple jobs share a version /// What the assertions prove: Job IDs appear in alphabetical order /// [TestMethod] @@ -312,9 +315,12 @@ public void MarkdownFormatter_FormatVersions_SortsJobIdsAlphabetically() // Act - Format the version information var result = MarkdownFormatter.Format(versionInfos); - // Assert - Verify "All jobs" is shown (since all have same version) - // What is proved: When all jobs have the same version, "All jobs" is displayed - Assert.Contains("1.0.0 (All jobs)", result); + // Assert - Verify version is shown without job IDs (since all have same version) + // What is proved: When all jobs have the same version, just the version is displayed + Assert.Contains("- **tool**: 1.0.0", result); + Assert.DoesNotContain("job-zebra", result); + Assert.DoesNotContain("job-alpha", result); + Assert.DoesNotContain("job-beta", result); } /// @@ -343,9 +349,9 @@ public void MarkdownFormatter_FormatVersions_WithSpecialCharacters_PreservesVers // Assert - Verify special characters are preserved // What is proved: Version strings with special characters are correctly preserved - Assert.Contains("1.0.0-beta+build.123 (All jobs)", result); - Assert.Contains("2.0.0-rc.1 (All jobs)", result); - Assert.Contains("3.0.0+meta.data (All jobs)", result); + Assert.Contains("- **tool1**: 1.0.0-beta+build.123", result); + Assert.Contains("- **tool2**: 2.0.0-rc.1", result); + Assert.Contains("- **tool3**: 3.0.0+meta.data", result); } /// @@ -410,14 +416,17 @@ public void MarkdownFormatter_FormatVersions_SortsVersionsAlphabetically() // Act - Format the version information var result = MarkdownFormatter.Format(versionInfos); - // Assert - Verify versions appear in sorted order - // What is proved: Different versions are listed in alphabetical order - var toolLine = result.Split('\n').First(l => l.StartsWith("- **tool**:")); - var indexOf1 = toolLine.IndexOf("1.0.0", StringComparison.Ordinal); - var indexOf2 = toolLine.IndexOf("2.0.0", StringComparison.Ordinal); - var indexOf3 = toolLine.IndexOf("3.0.0", StringComparison.Ordinal); - - Assert.IsLessThan(indexOf2, indexOf1, "1.0.0 should appear before 2.0.0"); - Assert.IsLessThan(indexOf3, indexOf2, "2.0.0 should appear before 3.0.0"); + // Assert - Verify versions appear in sorted order as separate bullets + // What is proved: Different versions are listed in alphabetical order, each on its own line + var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var toolLines = lines.Where(l => l.StartsWith("- **tool**:")).ToArray(); + + // Should have 3 separate bullets for the 3 different versions + Assert.HasCount(3, toolLines); + + // Verify they appear in sorted order: 1.0.0, 2.0.0, 3.0.0 + Assert.Contains("1.0.0", toolLines[0]); + Assert.Contains("2.0.0", toolLines[1]); + Assert.Contains("3.0.0", toolLines[2]); } }