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]);
}
}