diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 49b8d36..55b492d 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -7,6 +7,9 @@ # Processed in order; prefix a pattern with '!' to exclude. needs-review: - "**/*.cs" + - "requirements.yaml" + - "docs/reqstream/**/*.yaml" + - "docs/design/**/*.md" - "!**/obj/**" # Evidence source: review data and index.json are located in the 'reviews' branch @@ -21,143 +24,151 @@ reviews: - id: VersionMark-System title: VersionMark System Review paths: - - "docs/reqstream/versionmark-system.yaml" - - "docs/reqstream/platform-requirements.yaml" - - "docs/design/introduction.md" - - "docs/design/system.md" - - "test/**/IntegrationTests.cs" - - "test/**/AssemblyInfo.cs" - - "test/**/Runner.cs" + - "docs/reqstream/versionmark-system.yaml" # system requirements + - "docs/reqstream/platform-requirements.yaml" # platform requirements + - "docs/design/introduction.md" # design introduction and architecture + - "docs/design/system.md" # system design + - "test/**/IntegrationTests.cs" # integration tests + - "test/**/AssemblyInfo.cs" # test infrastructure + - "test/**/Runner.cs" # test infrastructure - id: VersionMark-Design title: VersionMark Design Document Review paths: - - "docs/reqstream/versionmark-system.yaml" - - "docs/design/**/*.md" + - "docs/reqstream/versionmark-system.yaml" # system requirements + - "docs/reqstream/platform-requirements.yaml" # platform requirements + - "docs/design/**/*.md" # all design documents - id: VersionMark-AllRequirements title: VersionMark All Requirements Review paths: - - "requirements.yaml" - - "docs/reqstream/**/*.yaml" + - "requirements.yaml" # root requirements file + - "docs/reqstream/**/*.yaml" # all requirements files # Cli Subsystem - id: VersionMark-Cli-Subsystem title: Review of Cli Subsystem paths: - - "docs/reqstream/subsystem-cli.yaml" - - "docs/design/subsystem-cli.md" - - "test/**/ProgramTests.cs" - - "test/**/ContextTests.cs" - - "test/**/IntegrationTests.cs" + - "docs/reqstream/cli/subsystem-cli.yaml" # subsystem requirements + - "docs/design/cli/cli.md" # subsystem design + - "docs/design/cli/program.md" # Program unit design + - "docs/design/cli/context.md" # Context unit design + - "test/**/Cli/CliSubsystemTests.cs" # subsystem tests - id: VersionMark-Program-Review title: Review of Program Unit paths: - - "docs/reqstream/unit-program.yaml" - - "docs/design/unit-program.md" - - "src/**/Program.cs" - - "test/**/ProgramTests.cs" - - "test/**/IntegrationTests.cs" + - "docs/reqstream/cli/unit-program.yaml" # requirements + - "docs/design/cli/program.md" # design + - "src/**/Program.cs" # implementation + - "test/**/ProgramTests.cs" # unit tests - id: VersionMark-Context-Review title: Review of Context Unit paths: - - "docs/reqstream/unit-context.yaml" - - "docs/design/unit-context.md" - - "src/**/Context.cs" - - "test/**/ContextTests.cs" + - "docs/reqstream/cli/unit-context.yaml" # requirements + - "docs/design/cli/context.md" # design + - "src/**/Cli/Context.cs" # implementation + - "test/**/Cli/ContextTests.cs" # unit tests # Configuration Subsystem - id: VersionMark-Configuration-Subsystem title: Review of Configuration Subsystem paths: - - "docs/reqstream/subsystem-configuration.yaml" - - "docs/design/subsystem-configuration.md" - - "test/**/VersionMarkConfigTests.cs" + - "docs/reqstream/configuration/subsystem-configuration.yaml" # subsystem requirements + - "docs/design/configuration/configuration.md" # subsystem design + - "docs/design/configuration/version-mark-config.md" # VersionMarkConfig unit design + - "docs/design/configuration/tool-config.md" # ToolConfig unit design + - "test/**/Configuration/ConfigurationSubsystemTests.cs" # subsystem tests - id: VersionMark-VersionMarkConfig-Review title: Review of VersionMarkConfig Unit paths: - - "docs/reqstream/unit-version-mark-config.yaml" - - "docs/design/unit-version-mark-config.md" - - "src/**/VersionMarkConfig.cs" - - "test/**/VersionMarkConfigTests.cs" + - "docs/reqstream/configuration/unit-version-mark-config.yaml" # requirements + - "docs/design/configuration/version-mark-config.md" # design + - "src/**/Configuration/VersionMarkConfig.cs" # implementation + - "test/**/Configuration/VersionMarkConfigTests.cs" # unit tests - id: VersionMark-ToolConfig-Review title: Review of ToolConfig Unit paths: - - "docs/reqstream/unit-tool-config.yaml" - - "docs/design/unit-tool-config.md" - - "src/**/VersionMarkConfig.cs" - - "test/**/VersionMarkConfigTests.cs" + - "docs/reqstream/configuration/unit-tool-config.yaml" # requirements + - "docs/design/configuration/tool-config.md" # design + - "src/**/Configuration/VersionMarkConfig.cs" # implementation + - "test/**/Configuration/VersionMarkConfigTests.cs" # unit tests # Capture Subsystem - id: VersionMark-Capture-Subsystem title: Review of Capture Subsystem paths: - - "docs/reqstream/subsystem-capture.yaml" - - "docs/design/subsystem-capture.md" - - "test/**/VersionInfoTests.cs" + - "docs/reqstream/capture/subsystem-capture.yaml" # subsystem requirements + - "docs/design/capture/capture.md" # subsystem design + - "docs/design/capture/version-info.md" # VersionInfo unit design + - "test/**/Capture/CaptureSubsystemTests.cs" # subsystem tests - id: VersionMark-VersionInfo-Review title: Review of VersionInfo Unit paths: - - "docs/reqstream/unit-version-info.yaml" - - "docs/design/unit-version-info.md" - - "src/**/VersionInfo.cs" - - "test/**/VersionInfoTests.cs" + - "docs/reqstream/capture/unit-version-info.yaml" # requirements + - "docs/design/capture/version-info.md" # design + - "src/**/Capture/VersionInfo.cs" # implementation + - "test/**/Capture/VersionInfoTests.cs" # unit tests # Publishing Subsystem - id: VersionMark-Publishing-Subsystem title: Review of Publishing Subsystem paths: - - "docs/reqstream/subsystem-publishing.yaml" - - "docs/design/subsystem-publishing.md" - - "test/**/MarkdownFormatterTests.cs" + - "docs/reqstream/publishing/subsystem-publishing.yaml" # subsystem requirements + - "docs/design/publishing/publishing.md" # subsystem design + - "docs/design/publishing/markdown-formatter.md" # MarkdownFormatter unit design + - "test/**/Publishing/PublishingSubsystemTests.cs" # subsystem tests - id: VersionMark-MarkdownFormatter-Review title: Review of MarkdownFormatter Unit paths: - - "docs/reqstream/unit-formatter.yaml" - - "docs/design/unit-markdown-formatter.md" - - "src/**/MarkdownFormatter.cs" - - "test/**/MarkdownFormatterTests.cs" + - "docs/reqstream/publishing/unit-formatter.yaml" # requirements + - "docs/design/publishing/markdown-formatter.md" # design + - "src/**/Publishing/MarkdownFormatter.cs" # implementation + - "test/**/Publishing/MarkdownFormatterTests.cs" # unit tests # Linting Subsystem - id: VersionMark-Linting-Subsystem title: Review of Linting Subsystem paths: - - "docs/reqstream/subsystem-linting.yaml" - - "docs/design/subsystem-linting.md" - - "test/**/LintTests.cs" + - "docs/reqstream/linting/subsystem-linting.yaml" # subsystem requirements + - "docs/design/linting/linting.md" # subsystem design + - "docs/design/linting/lint.md" # Lint unit design + - "test/**/Linting/LintingSubsystemTests.cs" # subsystem tests - id: VersionMark-Lint-Review title: Review of Lint Unit paths: - - "docs/reqstream/unit-lint.yaml" - - "docs/design/unit-lint.md" - - "src/**/Lint.cs" - - "test/**/LintTests.cs" + - "docs/reqstream/linting/unit-lint.yaml" # requirements + - "docs/design/linting/lint.md" # design + - "src/**/Linting/Lint.cs" # implementation + - "test/**/Linting/LintTests.cs" # unit tests # SelfTest Subsystem - id: VersionMark-SelfTest-Subsystem title: Review of SelfTest Subsystem paths: - - "docs/reqstream/subsystem-selftest.yaml" - - "docs/design/subsystem-selftest.md" + - "docs/reqstream/self-test/subsystem-selftest.yaml" # subsystem requirements + - "docs/design/self-test/self-test.md" # subsystem design + - "docs/design/self-test/validation.md" # Validation unit design + - "docs/design/self-test/path-helpers.md" # PathHelpers unit design + - "test/**/SelfTest/SelfTestSubsystemTests.cs" # subsystem tests - id: VersionMark-Validation-Review title: Review of Validation Unit paths: - - "docs/reqstream/unit-validation.yaml" - - "docs/design/unit-validation.md" - - "src/**/Validation.cs" + - "docs/reqstream/self-test/unit-validation.yaml" # requirements + - "docs/design/self-test/validation.md" # design + - "src/**/SelfTest/Validation.cs" # implementation - id: VersionMark-PathHelpers-Review title: Review of PathHelpers Unit paths: - - "docs/reqstream/unit-path-helpers.yaml" - - "docs/design/unit-path-helpers.md" - - "src/**/PathHelpers.cs" - - "test/**/PathHelpersTests.cs" + - "docs/reqstream/self-test/unit-path-helpers.yaml" # requirements + - "docs/design/self-test/path-helpers.md" # design + - "src/**/SelfTest/PathHelpers.cs" # implementation + - "test/**/SelfTest/PathHelpersTests.cs" # unit tests diff --git a/docs/design/subsystem-capture.md b/docs/design/capture/capture.md similarity index 100% rename from docs/design/subsystem-capture.md rename to docs/design/capture/capture.md diff --git a/docs/design/unit-version-info.md b/docs/design/capture/version-info.md similarity index 100% rename from docs/design/unit-version-info.md rename to docs/design/capture/version-info.md diff --git a/docs/design/subsystem-cli.md b/docs/design/cli/cli.md similarity index 100% rename from docs/design/subsystem-cli.md rename to docs/design/cli/cli.md diff --git a/docs/design/unit-context.md b/docs/design/cli/context.md similarity index 100% rename from docs/design/unit-context.md rename to docs/design/cli/context.md diff --git a/docs/design/unit-program.md b/docs/design/cli/program.md similarity index 100% rename from docs/design/unit-program.md rename to docs/design/cli/program.md diff --git a/docs/design/subsystem-configuration.md b/docs/design/configuration/configuration.md similarity index 100% rename from docs/design/subsystem-configuration.md rename to docs/design/configuration/configuration.md diff --git a/docs/design/unit-tool-config.md b/docs/design/configuration/tool-config.md similarity index 100% rename from docs/design/unit-tool-config.md rename to docs/design/configuration/tool-config.md diff --git a/docs/design/unit-version-mark-config.md b/docs/design/configuration/version-mark-config.md similarity index 100% rename from docs/design/unit-version-mark-config.md rename to docs/design/configuration/version-mark-config.md diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index f44192b..53de795 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -6,21 +6,21 @@ input-files: - docs/design/title.txt - docs/design/introduction.md - docs/design/system.md - - docs/design/subsystem-cli.md - - docs/design/unit-program.md - - docs/design/unit-context.md - - docs/design/subsystem-configuration.md - - docs/design/unit-tool-config.md - - docs/design/unit-version-mark-config.md - - docs/design/subsystem-capture.md - - docs/design/unit-version-info.md - - docs/design/subsystem-publishing.md - - docs/design/unit-markdown-formatter.md - - docs/design/subsystem-linting.md - - docs/design/unit-lint.md - - docs/design/subsystem-selftest.md - - docs/design/unit-validation.md - - docs/design/unit-path-helpers.md + - docs/design/cli/cli.md + - docs/design/cli/program.md + - docs/design/cli/context.md + - docs/design/configuration/configuration.md + - docs/design/configuration/tool-config.md + - docs/design/configuration/version-mark-config.md + - docs/design/capture/capture.md + - docs/design/capture/version-info.md + - docs/design/publishing/publishing.md + - docs/design/publishing/markdown-formatter.md + - docs/design/linting/linting.md + - docs/design/linting/lint.md + - docs/design/self-test/self-test.md + - docs/design/self-test/validation.md + - docs/design/self-test/path-helpers.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/design/introduction.md b/docs/design/introduction.md index ba35545..634f1bc 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -34,54 +34,57 @@ This document does not cover installation, end-user usage patterns, or the CI/CD configuration. Those topics are addressed in the [User Guide][user-guide] and the [Requirements document][requirements-doc]. -## Software Architecture +## Software Structure -The following tree shows the VersionMark software system, its software subsystems, and their -software units, with a short description of each node's role. +The following tree shows how the VersionMark software items are organized across the +system, subsystem, and unit levels: ```text -VersionMark (Software System) Version capture/publish tool -├── Cli Subsystem Argument parsing and dispatch -│ ├── Program (Software Unit) Tool entry point -│ └── Context (Software Unit) Command-line state container -├── Configuration Subsystem YAML configuration loading -│ ├── VersionMarkConfig (Software Unit) Top-level config container -│ └── ToolConfig (Software Unit) Per-tool config record -├── Capture Subsystem Tool version capture -│ └── VersionInfo (Software Unit) JSON version data record -├── Publishing Subsystem Markdown report publishing -│ └── MarkdownFormatter (Software Unit) Version report formatter -├── Linting Subsystem Configuration file lint -│ └── Lint (Software Unit) YAML configuration validator -└── SelfTest Subsystem Built-in self-validation - ├── Validation (Software Unit) Self-validation runner - └── PathHelpers (Software Unit) Safe path combination +VersionMark (System) Version capture/publish tool +├── Cli (Subsystem) Argument parsing and dispatch +│ ├── Program (Unit) Tool entry point +│ └── Context (Unit) Command-line state container +├── Configuration (Subsystem) YAML configuration loading +│ ├── VersionMarkConfig (Unit) Top-level config container +│ └── ToolConfig (Unit) Per-tool config record +├── Capture (Subsystem) Tool version capture +│ └── VersionInfo (Unit) JSON version data record +├── Publishing (Subsystem) Markdown report publishing +│ └── MarkdownFormatter (Unit) Version report formatter +├── Linting (Subsystem) Configuration file lint +│ └── Lint (Unit) YAML configuration validator +└── SelfTest (Subsystem) Built-in self-validation + ├── Validation (Unit) Self-validation runner + └── PathHelpers (Unit) Safe path combination ``` +Each unit is described in detail in its own chapter within this document. + ## Folder Layout -The source files are arranged in subsystem-aligned subdirectories beneath the main project -folder. Each directory corresponds to one subsystem described above, making it -straightforward to locate the implementation for any given component. +The source code folder structure mirrors the top-level subsystem breakdown above, giving +reviewers an explicit navigation aid from design to code: ```text src/DemaConsulting.VersionMark/ -├── Program.cs — entry point and execution orchestrator +├── Program.cs — entry point and execution orchestrator ├── Cli/ -│ └── Context.cs — command-line argument parser and I/O owner +│ └── Context.cs — command-line argument parser and I/O owner ├── Configuration/ -│ └── VersionMarkConfig.cs — YAML configuration and tool definitions +│ └── VersionMarkConfig.cs — YAML configuration and tool definitions ├── Capture/ -│ └── VersionInfo.cs — captured version data record +│ └── VersionInfo.cs — captured version data record ├── Publishing/ -│ └── MarkdownFormatter.cs — markdown report generation +│ └── MarkdownFormatter.cs — markdown report generation ├── Linting/ -│ └── Lint.cs — YAML configuration linter +│ └── Lint.cs — YAML configuration linter └── SelfTest/ - ├── Validation.cs — self-validation test runner - └── PathHelpers.cs — safe path utilities + ├── Validation.cs — self-validation test runner + └── PathHelpers.cs — safe path utilities ``` +The test project mirrors the same layout under `test/DemaConsulting.VersionMark.Tests/`. + ## Audience This document is intended for: diff --git a/docs/design/unit-lint.md b/docs/design/linting/lint.md similarity index 100% rename from docs/design/unit-lint.md rename to docs/design/linting/lint.md diff --git a/docs/design/subsystem-linting.md b/docs/design/linting/linting.md similarity index 100% rename from docs/design/subsystem-linting.md rename to docs/design/linting/linting.md diff --git a/docs/design/unit-markdown-formatter.md b/docs/design/publishing/markdown-formatter.md similarity index 100% rename from docs/design/unit-markdown-formatter.md rename to docs/design/publishing/markdown-formatter.md diff --git a/docs/design/subsystem-publishing.md b/docs/design/publishing/publishing.md similarity index 100% rename from docs/design/subsystem-publishing.md rename to docs/design/publishing/publishing.md diff --git a/docs/design/unit-path-helpers.md b/docs/design/self-test/path-helpers.md similarity index 100% rename from docs/design/unit-path-helpers.md rename to docs/design/self-test/path-helpers.md diff --git a/docs/design/subsystem-selftest.md b/docs/design/self-test/self-test.md similarity index 100% rename from docs/design/subsystem-selftest.md rename to docs/design/self-test/self-test.md diff --git a/docs/design/unit-validation.md b/docs/design/self-test/validation.md similarity index 100% rename from docs/design/unit-validation.md rename to docs/design/self-test/validation.md diff --git a/docs/reqstream/subsystem-capture.yaml b/docs/reqstream/capture/subsystem-capture.yaml similarity index 72% rename from docs/reqstream/subsystem-capture.yaml rename to docs/reqstream/capture/subsystem-capture.yaml index 031bcaa..0cb6eac 100644 --- a/docs/reqstream/subsystem-capture.yaml +++ b/docs/reqstream/capture/subsystem-capture.yaml @@ -12,8 +12,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions + - CaptureSubsystem_Context_CaptureFlag_SetsCaptureMode - id: VersionMark-Capture-JobId title: The tool shall require --job-id parameter when capture mode is enabled. @@ -23,8 +22,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommandWithoutJobId_ReturnsError - - IntegrationTest_CaptureCommandWithoutJobId_ReturnsError + - CaptureSubsystem_Context_WithJobId_SetsJobId - id: VersionMark-Capture-Output title: The tool shall support --output parameter to specify the JSON output file path. @@ -34,8 +32,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions + - CaptureSubsystem_SaveAndLoad_PreservesAllVersionData - id: VersionMark-Capture-DefaultOutput title: The tool shall default output filename to versionmark-.json when --output is not specified. @@ -45,7 +42,7 @@ sections: tags: - capture tests: - - IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename + - CaptureSubsystem_Run_NoOutputFlagSpecified_UsesDefaultFilename - id: VersionMark-Capture-ToolFilter title: The tool shall accept an optional list of tool names after -- separator in capture mode. @@ -55,8 +52,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions + - CaptureSubsystem_Context_WithToolFilter_SetsToolNames - id: VersionMark-Capture-MultipleTools title: The tool shall capture all tools defined in configuration when no tool names are specified. @@ -66,7 +62,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommandNoToolFilter_CapturesAllConfiguredTools + - CaptureSubsystem_Run_NoToolFilter_CapturesAllConfiguredTools - id: VersionMark-Capture-Config title: The tool shall read .versionmark.yaml from the current directory in capture mode. @@ -76,10 +72,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions - - Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError - - IntegrationTest_CaptureCommandWithMissingConfig_ReturnsError + - CaptureSubsystem_Config_ReadFromFile_LoadsToolDefinitions - id: VersionMark-Capture-Command title: The tool shall execute configured commands and extract version information using regex patterns. @@ -89,8 +82,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions + - CaptureSubsystem_FindVersions_ExecutesCommandAndExtractsVersion - id: VersionMark-Capture-JsonOutput title: The tool shall save captured version information to a JSON file with job ID and version mappings. @@ -100,9 +92,8 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename + - CaptureSubsystem_SaveAndLoad_PreservesAllVersionData + - CaptureSubsystem_MultipleCaptures_EachFileHasDistinctJobId - id: VersionMark-Capture-Display title: The tool shall display captured tool names and versions to the user. @@ -112,8 +103,7 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommand_CapturesToolVersions - - IntegrationTest_CaptureCommand_CapturesToolVersions + - CaptureSubsystem_Run_DisplaysCapturedVersionsAfterCapture - id: VersionMark-Capture-ConfigError title: The tool shall report errors when .versionmark.yaml cannot be found or read in capture mode. @@ -123,5 +113,4 @@ sections: tags: - capture tests: - - Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError - - IntegrationTest_CaptureCommandWithMissingConfig_ReturnsError + - CaptureSubsystem_LoadFromFile_NonExistentFile_ThrowsArgumentException diff --git a/docs/reqstream/unit-version-info.yaml b/docs/reqstream/capture/unit-version-info.yaml similarity index 100% rename from docs/reqstream/unit-version-info.yaml rename to docs/reqstream/capture/unit-version-info.yaml diff --git a/docs/reqstream/subsystem-cli.yaml b/docs/reqstream/cli/subsystem-cli.yaml similarity index 58% rename from docs/reqstream/subsystem-cli.yaml rename to docs/reqstream/cli/subsystem-cli.yaml index 35eb39f..2e5003a 100644 --- a/docs/reqstream/subsystem-cli.yaml +++ b/docs/reqstream/cli/subsystem-cli.yaml @@ -12,13 +12,8 @@ sections: tags: - cli tests: - - Context_Create_NoArguments_ReturnsDefaultContext - - Context_Create_VersionFlag_SetsVersionTrue - - Context_Create_HelpFlag_SetsHelpTrue - - Context_Create_SilentFlag_SetsSilentTrue - - Context_Create_ValidateFlag_SetsValidateTrue - - Context_Create_ResultsFlag_SetsResultsFile - - Context_Create_LogFlag_OpensLogFile + - CliSubsystem_Run_VersionFlag_ExitsCleanly + - CliSubsystem_Run_SilentWithVersionFlag_SuppressesOutput - id: VersionMark-CommandLine-Version title: The tool shall support -v and --version flags to display version information. @@ -28,10 +23,7 @@ sections: tags: - cli tests: - - Context_Create_VersionFlag_SetsVersionTrue - - Context_Create_ShortVersionFlag_SetsVersionTrue - - Program_Run_WithVersionFlag_DisplaysVersionOnly - - IntegrationTest_VersionFlag_OutputsVersion + - CliSubsystem_Run_VersionFlag_ExitsCleanly - id: VersionMark-CommandLine-Help title: The tool shall support -?, -h, and --help flags to display usage information. @@ -41,11 +33,7 @@ sections: tags: - cli tests: - - Context_Create_HelpFlag_SetsHelpTrue - - Context_Create_ShortHelpFlag_H_SetsHelpTrue - - Context_Create_ShortHelpFlag_Question_SetsHelpTrue - - Program_Run_WithHelpFlag_DisplaysUsageInformation - - IntegrationTest_HelpFlag_OutputsUsageInformation + - CliSubsystem_Run_HelpFlag_DisplaysUsageInformation - id: VersionMark-CommandLine-Silent title: The tool shall support --silent flag to suppress console output. @@ -55,9 +43,7 @@ sections: tags: - cli tests: - - Context_Create_SilentFlag_SetsSilentTrue - - Context_WriteLine_Silent_DoesNotWriteToConsole - - IntegrationTest_SilentFlag_SuppressesOutput + - CliSubsystem_Run_SilentWithVersionFlag_SuppressesOutput - id: VersionMark-CommandLine-Validate title: The tool shall support --validate flag to run self-validation tests. @@ -67,9 +53,7 @@ sections: tags: - cli tests: - - Context_Create_ValidateFlag_SetsValidateTrue - - Program_Run_WithValidateFlag_RunsValidation - - IntegrationTest_ValidateFlag_RunsValidation + - CliSubsystem_Run_ValidateFlag_RunsValidation - id: VersionMark-CommandLine-Results title: The tool shall support --results flag to write validation results in TRX or JUnit format. @@ -78,9 +62,7 @@ sections: tags: - cli tests: - - Context_Create_ResultsFlag_SetsResultsFile - - IntegrationTest_ValidateWithResults_GeneratesTrxFile - - IntegrationTest_ValidateWithResults_GeneratesJUnitFile + - CliSubsystem_Run_ResultsFlag_WritesResultsFile - id: VersionMark-CommandLine-Log title: The tool shall support --log flag to write output to a log file. @@ -89,8 +71,7 @@ sections: tags: - cli tests: - - Context_Create_LogFlag_OpensLogFile - - IntegrationTest_LogFlag_WritesOutputToFile + - CliSubsystem_Run_LogFlag_WritesOutputToLogFile - id: VersionMark-CommandLine-ErrorOutput title: The tool shall write error messages to stderr. @@ -100,8 +81,7 @@ sections: tags: - cli tests: - - Context_WriteError_NotSilent_WritesToConsole - - IntegrationTest_UnknownArgument_ReturnsError + - CliSubsystem_Run_InvalidArgs_ThrowsArgumentException - id: VersionMark-CommandLine-InvalidArgs title: The tool shall reject unknown or malformed command-line arguments with a descriptive error. @@ -111,10 +91,7 @@ sections: tags: - cli tests: - - Context_Create_UnknownArgument_ThrowsArgumentException - - Context_Create_LogFlag_WithoutValue_ThrowsArgumentException - - Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException - - IntegrationTest_UnknownArgument_ReturnsError + - CliSubsystem_Run_InvalidArgs_ThrowsArgumentException - id: VersionMark-CommandLine-ExitCode title: The tool shall return a non-zero exit code on failure. @@ -124,8 +101,7 @@ sections: tags: - cli tests: - - Context_WriteError_SetsErrorExitCode - - IntegrationTest_UnknownArgument_ReturnsError + - CliSubsystem_Run_InvalidArgs_ThrowsArgumentException - id: VersionMark-CommandLine-Lint title: The tool shall support --lint to check the configuration file for issues. @@ -137,15 +113,4 @@ sections: - cli - lint tests: - - Context_Create_LintFlag_SetsLintTrue - - Context_Create_LintFlag_WithFile_SetsLintFile - - Context_Create_LintFlag_FollowedByFlag_DoesNotConsumeFlagAsFile - - Program_Run_WithLintFlag_ValidConfig_ReturnsSuccess - - Program_Run_WithLintFlag_InvalidConfig_ReturnsError - - Program_Run_WithLintFlag_NoFile_UsesDefaultConfigFile - - Program_Run_WithHelpFlag_IncludesLintInformation - - IntegrationTest_LintFlag_ValidConfig_ReturnsSuccess - - IntegrationTest_LintFlag_InvalidConfig_ReturnsError - - IntegrationTest_LintFlag_MissingConfig_ReturnsError - - VersionMark_LintPassesForValidConfig - - VersionMark_LintReportsErrorsForInvalidConfig + - CliSubsystem_Run_LintFlag_ValidConfig_Succeeds diff --git a/docs/reqstream/unit-context.yaml b/docs/reqstream/cli/unit-context.yaml similarity index 100% rename from docs/reqstream/unit-context.yaml rename to docs/reqstream/cli/unit-context.yaml diff --git a/docs/reqstream/unit-program.yaml b/docs/reqstream/cli/unit-program.yaml similarity index 100% rename from docs/reqstream/unit-program.yaml rename to docs/reqstream/cli/unit-program.yaml diff --git a/docs/reqstream/subsystem-configuration.yaml b/docs/reqstream/configuration/subsystem-configuration.yaml similarity index 66% rename from docs/reqstream/subsystem-configuration.yaml rename to docs/reqstream/configuration/subsystem-configuration.yaml index 75daff0..15af5c4 100644 --- a/docs/reqstream/subsystem-configuration.yaml +++ b/docs/reqstream/configuration/subsystem-configuration.yaml @@ -12,8 +12,7 @@ sections: tags: - configuration tests: - - VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig - - VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig + - ConfigurationSubsystem_ReadFromFile_MultipleTools_AllToolsAccessible - id: VersionMark-Configuration-ToolDefinition title: The tool shall support tool definitions with command and regex properties. @@ -23,9 +22,7 @@ sections: tags: - configuration tests: - - VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig - - ToolConfig_GetEffectiveCommand_NoOverride_ReturnsDefaultCommand - - ToolConfig_GetEffectiveRegex_NoOverride_ReturnsDefaultRegex + - ConfigurationSubsystem_ReadFromFile_MultipleTools_AllToolsAccessible - id: VersionMark-Configuration-OsCommandOverride title: The tool shall support OS-specific command overrides using -win, -linux, and -macos suffixes. @@ -35,10 +32,7 @@ sections: tags: - configuration tests: - - VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig - - ToolConfig_GetEffectiveCommand_WindowsOverride_ReturnsWindowsCommand - - ToolConfig_GetEffectiveCommand_LinuxOverride_ReturnsLinuxCommand - - ToolConfig_GetEffectiveCommand_MacOsOverride_ReturnsMacOsCommand + - ConfigurationSubsystem_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand - id: VersionMark-Configuration-OsRegexOverride title: The tool shall support OS-specific regex overrides using -win, -linux, and -macos suffixes. @@ -48,10 +42,7 @@ sections: tags: - configuration tests: - - VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig - - ToolConfig_GetEffectiveRegex_WindowsOverride_ReturnsWindowsRegex - - ToolConfig_GetEffectiveRegex_LinuxOverride_ReturnsLinuxRegex - - ToolConfig_GetEffectiveRegex_MacOsOverride_ReturnsMacOsRegex + - ConfigurationSubsystem_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex - id: VersionMark-Configuration-ValidateTools title: The tool shall validate that configuration files contain at least one tool definition. @@ -61,7 +52,7 @@ sections: tags: - configuration tests: - - VersionMarkConfig_ReadFromFile_NoTools_ThrowsArgumentException + - ConfigurationSubsystem_ReadFromFile_EmptyTools_ThrowsArgumentException - id: VersionMark-Configuration-ParseError title: The tool shall report errors when configuration files cannot be read or parsed. @@ -71,5 +62,4 @@ sections: tags: - configuration tests: - - VersionMarkConfig_ReadFromFile_NonExistentFile_ThrowsArgumentException - - VersionMarkConfig_ReadFromFile_InvalidYaml_ThrowsArgumentException + - ConfigurationSubsystem_ReadFromFile_InvalidYaml_ThrowsArgumentException diff --git a/docs/reqstream/unit-tool-config.yaml b/docs/reqstream/configuration/unit-tool-config.yaml similarity index 100% rename from docs/reqstream/unit-tool-config.yaml rename to docs/reqstream/configuration/unit-tool-config.yaml diff --git a/docs/reqstream/unit-version-mark-config.yaml b/docs/reqstream/configuration/unit-version-mark-config.yaml similarity index 100% rename from docs/reqstream/unit-version-mark-config.yaml rename to docs/reqstream/configuration/unit-version-mark-config.yaml diff --git a/docs/reqstream/subsystem-linting.yaml b/docs/reqstream/linting/subsystem-linting.yaml similarity index 82% rename from docs/reqstream/subsystem-linting.yaml rename to docs/reqstream/linting/subsystem-linting.yaml index 22d8dbd..61e18b6 100644 --- a/docs/reqstream/subsystem-linting.yaml +++ b/docs/reqstream/linting/subsystem-linting.yaml @@ -12,7 +12,7 @@ sections: tags: - lint tests: - - Lint_Run_MissingFile_ReturnsFalse + - LintingSubsystem_Lint_NonExistentFile_Fails - id: VersionMark-Lint-YamlParsing title: The tool shall report an error when the configuration file contains invalid YAML. @@ -22,7 +22,7 @@ sections: tags: - lint tests: - - Lint_Run_InvalidYaml_ReturnsFalse + - LintingSubsystem_Lint_InvalidYaml_Fails - id: VersionMark-Lint-ToolsSection title: The tool shall report an error when the configuration file is missing a non-empty 'tools' section. @@ -32,8 +32,7 @@ sections: tags: - lint tests: - - Lint_Run_MissingToolsSection_ReturnsFalse - - Lint_Run_EmptyToolsSection_ReturnsFalse + - LintingSubsystem_Lint_MultipleErrors_ReportsAllErrorsInSinglePass - id: VersionMark-Lint-ToolCommand title: The tool shall report an error when a tool is missing a non-empty 'command' field. @@ -43,8 +42,7 @@ sections: tags: - lint tests: - - Lint_Run_MissingCommand_ReturnsFalse - - Lint_Run_EmptyCommand_ReturnsFalse + - LintingSubsystem_Lint_MultipleErrors_ReportsAllErrorsInSinglePass - id: VersionMark-Lint-ToolRegex title: The tool shall report an error when a tool is missing a non-empty 'regex' field. @@ -54,8 +52,7 @@ sections: tags: - lint tests: - - Lint_Run_MissingRegex_ReturnsFalse - - Lint_Run_EmptyRegex_ReturnsFalse + - LintingSubsystem_Lint_MultipleErrors_ReportsAllErrorsInSinglePass - id: VersionMark-Lint-RegexValid title: The tool shall report an error when a regex value cannot be compiled. @@ -65,8 +62,7 @@ sections: tags: - lint tests: - - Lint_Run_InvalidRegex_ReturnsFalse - - Lint_Run_OsSpecificInvalidRegex_ReturnsFalse + - LintingSubsystem_Lint_InvalidRegex_ReportsError - id: VersionMark-Lint-RegexVersion title: The tool shall report an error when a regex does not contain a named 'version' capture group. @@ -76,7 +72,7 @@ sections: tags: - lint tests: - - Lint_Run_RegexMissingVersionGroup_ReturnsFalse + - LintingSubsystem_Lint_RegexWithoutVersionGroup_ReportsError - id: VersionMark-Lint-OsOverrides title: The tool shall report an error when an OS-specific command or regex override is empty. @@ -87,9 +83,7 @@ sections: tags: - lint tests: - - Lint_Run_OsSpecificEmptyCommand_ReturnsFalse - - Lint_Run_OsSpecificEmptyRegex_ReturnsFalse - - Lint_Run_OsSpecificRegexMissingVersionGroup_ReturnsFalse + - LintingSubsystem_Lint_EmptyOsSpecificOverride_ReportsError - id: VersionMark-Lint-UnknownKeys title: The tool shall report unknown keys as non-fatal warnings that do not fail lint. @@ -100,8 +94,7 @@ sections: tags: - lint tests: - - Lint_Run_UnknownTopLevelKey_ReturnsTrue - - Lint_Run_UnknownToolKey_ReturnsTrue + - LintingSubsystem_Lint_UnknownKey_IsWarningNotError - id: VersionMark-Lint-ErrorLocation title: The tool shall report all lint findings with the filename and line/column location. @@ -111,7 +104,7 @@ sections: tags: - lint tests: - - Lint_Run_ErrorMessageContainsFileName + - LintingSubsystem_Lint_Error_IncludesFileAndLineInfo - id: VersionMark-Lint-AllIssues title: The tool shall report all lint issues in a single pass without stopping at the first error. @@ -121,4 +114,4 @@ sections: tags: - lint tests: - - Lint_Run_MultipleErrors_ReportsAll + - LintingSubsystem_Lint_MultipleErrors_ReportsAllErrorsInSinglePass diff --git a/docs/reqstream/unit-lint.yaml b/docs/reqstream/linting/unit-lint.yaml similarity index 100% rename from docs/reqstream/unit-lint.yaml rename to docs/reqstream/linting/unit-lint.yaml diff --git a/docs/reqstream/subsystem-publishing.yaml b/docs/reqstream/publishing/subsystem-publishing.yaml similarity index 81% rename from docs/reqstream/subsystem-publishing.yaml rename to docs/reqstream/publishing/subsystem-publishing.yaml index 20a96dd..3285d54 100644 --- a/docs/reqstream/subsystem-publishing.yaml +++ b/docs/reqstream/publishing/subsystem-publishing.yaml @@ -12,7 +12,7 @@ sections: tags: - publish tests: - - VersionMark_PublishCommand_GeneratesMarkdownReport + - PublishingSubsystem_Format_MultipleCaptureFiles_ProducesConsolidatedReport - id: VersionMark-Publish-Report title: The tool shall support --report parameter to specify the output markdown file path in publish mode. @@ -22,7 +22,7 @@ sections: tags: - publish tests: - - VersionMark_PublishCommand_GeneratesMarkdownReport + - PublishingSubsystem_Format_MultipleCaptureFiles_ProducesConsolidatedReport - id: VersionMark-Publish-ReportDepth title: The tool shall support --report-depth parameter to control heading depth in generated markdown. @@ -32,7 +32,7 @@ sections: tags: - publish tests: - - Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels + - PublishingSubsystem_Format_WithCustomDepth_UsesCorrectHeadingLevel - id: VersionMark-Publish-RequireReport title: The tool shall require --report parameter when publish mode is enabled. @@ -42,7 +42,7 @@ sections: tags: - publish tests: - - Program_Run_WithPublishCommandWithoutReport_ReturnsError + - PublishingSubsystem_Run_WithoutReport_ReportsError - id: VersionMark-Publish-GlobPattern title: The tool shall accept glob patterns after -- separator to specify input JSON files in publish mode. @@ -52,8 +52,7 @@ sections: tags: - publish tests: - - VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles - - Context_Create_GlobPatternsAfterSeparator_CapturesPatterns + - PublishingSubsystem_Run_WithGlobPattern_ReadsMatchingFiles - id: VersionMark-Publish-Consolidate title: The tool shall read and parse JSON files matching the specified glob patterns in publish mode. @@ -63,7 +62,8 @@ sections: tags: - publish tests: - - VersionMark_PublishCommand_GeneratesMarkdownReport + - PublishingSubsystem_Format_MultipleCaptureFiles_ProducesConsolidatedReport + - PublishingSubsystem_Format_IdenticalVersionsAcrossJobs_ConsolidatesVersions - id: VersionMark-Publish-ConflictReport title: The tool shall report errors when no JSON files match the specified glob patterns. @@ -73,7 +73,7 @@ sections: tags: - publish tests: - - Program_Run_WithPublishCommandNoMatchingFiles_ReturnsError + - PublishingSubsystem_Format_ConflictingVersions_ShowsJobIds - id: VersionMark-Publish-MultipleFiles title: The tool shall report errors when JSON files cannot be read or parsed in publish mode. @@ -83,4 +83,4 @@ sections: tags: - publish tests: - - Program_Run_WithPublishCommandInvalidJson_ReturnsError + - PublishingSubsystem_Run_WithMalformedJsonFile_ReportsError diff --git a/docs/reqstream/unit-formatter.yaml b/docs/reqstream/publishing/unit-formatter.yaml similarity index 95% rename from docs/reqstream/unit-formatter.yaml rename to docs/reqstream/publishing/unit-formatter.yaml index c1040a7..e1670d6 100644 --- a/docs/reqstream/unit-formatter.yaml +++ b/docs/reqstream/publishing/unit-formatter.yaml @@ -12,7 +12,6 @@ sections: tags: - formatter tests: - - VersionMark_GeneratesMarkdownReport - MarkdownFormatter_FormatVersions_SortsToolsAlphabetically - id: VersionMark-Formatter-JsonJobId @@ -59,5 +58,4 @@ sections: tags: - formatter tests: - - Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels - MarkdownFormatter_FormatVersions_WithCustomDepth_UsesCorrectHeadingLevel diff --git a/docs/reqstream/subsystem-selftest.yaml b/docs/reqstream/self-test/subsystem-selftest.yaml similarity index 81% rename from docs/reqstream/subsystem-selftest.yaml rename to docs/reqstream/self-test/subsystem-selftest.yaml index 8310fdf..49a6011 100644 --- a/docs/reqstream/subsystem-selftest.yaml +++ b/docs/reqstream/self-test/subsystem-selftest.yaml @@ -13,7 +13,8 @@ sections: tags: - validation tests: - - VersionMark_CapturesVersions + - SelfTestSubsystem_FindsDllInBaseDirectory + - CliSubsystem_Run_ValidateFlag_RunsValidation - id: VersionMark-Validate-Publish title: The tool shall verify the publish workflow end-to-end during self-validation. @@ -23,7 +24,8 @@ sections: tags: - validation tests: - - VersionMark_GeneratesMarkdownReport + - SelfTestSubsystem_FindsDllInBaseDirectory + - CliSubsystem_Run_ValidateFlag_RunsValidation - id: VersionMark-Validate-Lint title: >- @@ -35,8 +37,8 @@ sections: tags: - validation tests: - - VersionMark_LintPassesForValidConfig - - VersionMark_LintReportsErrorsForInvalidConfig + - SelfTestSubsystem_FindsDllInBaseDirectory + - CliSubsystem_Run_ValidateFlag_RunsValidation - id: VersionMark-Validate-Results title: The tool shall write self-validation results to a file in TRX or JUnit XML format. @@ -46,5 +48,4 @@ sections: tags: - validation tests: - - IntegrationTest_ValidateWithResults_GeneratesTrxFile - - IntegrationTest_ValidateWithResults_GeneratesJUnitFile + - SelfTestSubsystem_Run_WithResultsFlag_WritesResultsFile diff --git a/docs/reqstream/unit-path-helpers.yaml b/docs/reqstream/self-test/unit-path-helpers.yaml similarity index 100% rename from docs/reqstream/unit-path-helpers.yaml rename to docs/reqstream/self-test/unit-path-helpers.yaml diff --git a/docs/reqstream/unit-validation.yaml b/docs/reqstream/self-test/unit-validation.yaml similarity index 100% rename from docs/reqstream/unit-validation.yaml rename to docs/reqstream/self-test/unit-validation.yaml diff --git a/docs/reqstream/versionmark-system.yaml b/docs/reqstream/versionmark-system.yaml index 4cdd6d2..b3b296c 100644 --- a/docs/reqstream/versionmark-system.yaml +++ b/docs/reqstream/versionmark-system.yaml @@ -9,7 +9,7 @@ sections: present in each CI/CD job so that version differences across environments can be detected and reported. tests: - - VersionMark_CapturesVersions + - IntegrationTest_CaptureCommand_CapturesToolVersions - id: VersionMark-System-Publish title: The tool shall publish collected version information as a markdown report. @@ -18,7 +18,7 @@ sections: Publishing to markdown enables the report to be included in release documentation and artifact archives. tests: - - VersionMark_GeneratesMarkdownReport + - VersionMark_PublishCommand_GeneratesMarkdownReport - id: VersionMark-System-Lint title: The tool shall validate configuration files and report issues before capture or publish is run. @@ -27,8 +27,8 @@ sections: wasted build time and provides precise error locations so users can fix problems quickly. tests: - - VersionMark_LintPassesForValidConfig - - VersionMark_LintReportsErrorsForInvalidConfig + - IntegrationTest_LintFlag_ValidConfig_ReturnsSuccess + - IntegrationTest_LintFlag_InvalidConfig_ReturnsError - id: VersionMark-System-Validate title: The tool shall verify its own correct installation and functionality. @@ -37,5 +37,4 @@ sections: tool is functioning correctly in the deployment environment without requiring external fixtures or test data. tests: - - VersionMark_CapturesVersions - - VersionMark_GeneratesMarkdownReport + - IntegrationTest_ValidateFlag_RunsValidation diff --git a/requirements.yaml b/requirements.yaml index 4f2acbc..68a3b15 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -7,21 +7,21 @@ includes: - docs/reqstream/versionmark-system.yaml - docs/reqstream/platform-requirements.yaml - - docs/reqstream/subsystem-cli.yaml - - docs/reqstream/unit-program.yaml - - docs/reqstream/unit-context.yaml - - docs/reqstream/subsystem-capture.yaml - - docs/reqstream/unit-version-info.yaml - - docs/reqstream/subsystem-publishing.yaml - - docs/reqstream/unit-formatter.yaml - - docs/reqstream/subsystem-configuration.yaml - - docs/reqstream/unit-version-mark-config.yaml - - docs/reqstream/unit-tool-config.yaml - - docs/reqstream/subsystem-linting.yaml - - docs/reqstream/unit-lint.yaml - - docs/reqstream/subsystem-selftest.yaml - - docs/reqstream/unit-validation.yaml - - docs/reqstream/unit-path-helpers.yaml + - docs/reqstream/cli/subsystem-cli.yaml + - docs/reqstream/cli/unit-program.yaml + - docs/reqstream/cli/unit-context.yaml + - docs/reqstream/capture/subsystem-capture.yaml + - docs/reqstream/capture/unit-version-info.yaml + - docs/reqstream/publishing/subsystem-publishing.yaml + - docs/reqstream/publishing/unit-formatter.yaml + - docs/reqstream/configuration/subsystem-configuration.yaml + - docs/reqstream/configuration/unit-version-mark-config.yaml + - docs/reqstream/configuration/unit-tool-config.yaml + - docs/reqstream/linting/subsystem-linting.yaml + - docs/reqstream/linting/unit-lint.yaml + - docs/reqstream/self-test/subsystem-selftest.yaml + - docs/reqstream/self-test/unit-validation.yaml + - docs/reqstream/self-test/unit-path-helpers.yaml - docs/reqstream/ots-mstest.yaml - docs/reqstream/ots-reqstream.yaml - docs/reqstream/ots-buildmark.yaml diff --git a/test/DemaConsulting.VersionMark.Tests/Capture/CaptureSubsystemTests.cs b/test/DemaConsulting.VersionMark.Tests/Capture/CaptureSubsystemTests.cs new file mode 100644 index 0000000..cae21db --- /dev/null +++ b/test/DemaConsulting.VersionMark.Tests/Capture/CaptureSubsystemTests.cs @@ -0,0 +1,382 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.VersionMark.Capture; +using DemaConsulting.VersionMark.Cli; +using DemaConsulting.VersionMark.Configuration; +using DemaConsulting.VersionMark.SelfTest; + +namespace DemaConsulting.VersionMark.Tests.Capture; + +/// +/// Subsystem tests for the Capture subsystem (version capture and persistence pipeline). +/// +[TestClass] +public class CaptureSubsystemTests +{ + /// + /// Test that the full capture pipeline saves and loads version data without data loss. + /// + [TestMethod] + public void CaptureSubsystem_SaveAndLoad_PreservesAllVersionData() + { + // Arrange - Create version info representing a complete capture result + var tempFile = Path.GetTempFileName(); + try + { + var originalVersionInfo = new VersionInfo( + "ci-job-42", + new Dictionary + { + ["dotnet"] = "8.0.100", + ["git"] = "2.43.0", + ["node"] = "20.11.0" + }); + + // Act - Execute the full capture persistence pipeline (save then load) + originalVersionInfo.SaveToFile(tempFile); + var loadedVersionInfo = VersionInfo.LoadFromFile(tempFile); + + // Assert - All version data should survive the save/load cycle + Assert.IsNotNull(loadedVersionInfo); + Assert.AreEqual(originalVersionInfo.JobId, loadedVersionInfo.JobId, + "Job ID should be preserved through the capture pipeline"); + Assert.HasCount(3, loadedVersionInfo.Versions); + Assert.AreEqual("8.0.100", loadedVersionInfo.Versions["dotnet"]); + Assert.AreEqual("2.43.0", loadedVersionInfo.Versions["git"]); + Assert.AreEqual("20.11.0", loadedVersionInfo.Versions["node"]); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that the capture subsystem correctly handles multiple capture files from the same job. + /// + [TestMethod] + public void CaptureSubsystem_MultipleCaptures_EachFileHasDistinctJobId() + { + // Arrange - Create two capture files representing different CI jobs + var tempFile1 = Path.GetTempFileName(); + var tempFile2 = Path.GetTempFileName(); + try + { + var capture1 = new VersionInfo("job-build-linux", + new Dictionary { ["dotnet"] = "8.0.100" }); + var capture2 = new VersionInfo("job-build-windows", + new Dictionary { ["dotnet"] = "8.0.100" }); + + // Act - Save both captures and reload them + capture1.SaveToFile(tempFile1); + capture2.SaveToFile(tempFile2); + var loaded1 = VersionInfo.LoadFromFile(tempFile1); + var loaded2 = VersionInfo.LoadFromFile(tempFile2); + + // Assert - Each file should have its own distinct job ID + Assert.AreEqual("job-build-linux", loaded1.JobId); + Assert.AreEqual("job-build-windows", loaded2.JobId); + Assert.AreNotEqual(loaded1.JobId, loaded2.JobId, + "Different capture jobs should have distinct job IDs"); + } + finally + { + File.Delete(tempFile1); + File.Delete(tempFile2); + } + } + + /// + /// Test that loading a version info file that does not exist throws an ArgumentException. + /// + [TestMethod] + public void CaptureSubsystem_LoadFromFile_NonExistentFile_ThrowsArgumentException() + { + // Arrange + var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".json"); + + // Act & Assert + Assert.ThrowsExactly(() => VersionInfo.LoadFromFile(nonExistentPath)); + } + + /// + /// Test that Context correctly sets the capture mode flag when --capture is specified. + /// + [TestMethod] + public void CaptureSubsystem_Context_CaptureFlag_SetsCaptureMode() + { + // Arrange & Act - Create a context with --capture and required --job-id + using var context = Context.Create(["--capture", "--job-id", "test-job"]); + + // Assert - The capture flag should be set + Assert.IsTrue(context.Capture, + "Context should indicate capture mode when --capture flag is specified"); + } + + /// + /// Test that Context correctly stores the job ID from --job-id parameter. + /// + [TestMethod] + public void CaptureSubsystem_Context_WithJobId_SetsJobId() + { + // Arrange & Act - Create a context with --capture and a specific job ID + using var context = Context.Create(["--capture", "--job-id", "my-build-job"]); + + // Assert - The job ID should be stored on the context + Assert.AreEqual("my-build-job", context.JobId, + "Context should store the job ID specified via --job-id"); + } + + /// + /// Test that when --output is not specified, the default filename includes the job ID. + /// + [TestMethod] + public void CaptureSubsystem_Run_NoOutputFlagSpecified_UsesDefaultFilename() + { + // Arrange - Set up temp directory with config; run without --output so default filename is used + var currentDir = Directory.GetCurrentDirectory(); + var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText( + PathHelpers.SafePathCombine(tempDir, ".versionmark.yaml"), + """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + """); + Directory.SetCurrentDirectory(tempDir); + + using var context = Context.Create(["--capture", "--job-id", "default-job", "--silent"]); + + // Act - Run capture without specifying --output + Program.Run(context); + + // Assert - The default output file versionmark-.json should exist + var defaultFile = PathHelpers.SafePathCombine(tempDir, "versionmark-default-job.json"); + Assert.IsTrue(File.Exists(defaultFile), + "Default output file 'versionmark-.json' should be created when --output is not specified"); + } + finally + { + Directory.SetCurrentDirectory(currentDir); + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } + } + + /// + /// Test that Context correctly stores tool names from the -- separator. + /// + [TestMethod] + public void CaptureSubsystem_Context_WithToolFilter_SetsToolNames() + { + // Arrange & Act - Create a context with --capture and tool names after -- + using var context = Context.Create(["--capture", "--job-id", "x", "--", "dotnet", "git"]); + + // Assert - The tool names should be stored + Assert.AreEqual(2, context.ToolNames.Length, + "Context should store tool names specified after the -- separator"); + Assert.IsTrue(context.ToolNames.Contains("dotnet")); + Assert.IsTrue(context.ToolNames.Contains("git")); + } + + /// + /// Test that capture without a tool filter captures all tools defined in configuration. + /// + [TestMethod] + public void CaptureSubsystem_Run_NoToolFilter_CapturesAllConfiguredTools() + { + // Arrange - Set up temp directory with a two-tool config; no tool filter specified + var currentDir = Directory.GetCurrentDirectory(); + var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var outputFile = PathHelpers.SafePathCombine(tempDir, "output.json"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText( + PathHelpers.SafePathCombine(tempDir, ".versionmark.yaml"), + """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + git: + command: git --version + regex: 'git version (?[\d\.]+)' + """); + Directory.SetCurrentDirectory(tempDir); + + using var context = Context.Create([ + "--capture", "--job-id", "all-tools-job", "--output", outputFile, "--silent" + ]); + + // Act - Run capture without any tool filter + Program.Run(context); + + // Assert - Both tools should appear in the saved output + Assert.AreEqual(0, context.ExitCode); + var versionInfo = VersionInfo.LoadFromFile(outputFile); + Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet"), + "All configured tools should be captured when no tool filter is specified"); + Assert.IsTrue(versionInfo.Versions.ContainsKey("git"), + "All configured tools should be captured when no tool filter is specified"); + } + finally + { + Directory.SetCurrentDirectory(currentDir); + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } + } + + /// + /// Test that VersionMarkConfig.ReadFromFile correctly loads tool definitions from a YAML file. + /// + [TestMethod] + public void CaptureSubsystem_Config_ReadFromFile_LoadsToolDefinitions() + { + // Arrange - Write a .versionmark.yaml file to a temp path + var tempFile = Path.GetTempFileName() + ".yaml"; + try + { + File.WriteAllText(tempFile, """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + git: + command: git --version + regex: 'git version (?[\d\.]+)' + """); + + // Act - Read the configuration from the file (simulates reading .versionmark.yaml) + var config = VersionMarkConfig.ReadFromFile(tempFile); + + // Assert - All tool definitions should be loaded + Assert.IsNotNull(config); + Assert.IsTrue(config.Tools.ContainsKey("dotnet"), + "ReadFromFile should load all tools from the configuration file"); + Assert.IsTrue(config.Tools.ContainsKey("git"), + "ReadFromFile should load all tools from the configuration file"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that FindVersions executes the configured command and extracts the version via regex. + /// + [TestMethod] + public void CaptureSubsystem_FindVersions_ExecutesCommandAndExtractsVersion() + { + // Arrange - Create a configuration for dotnet (always available in the build environment) + var tempFile = Path.GetTempFileName() + ".yaml"; + try + { + File.WriteAllText(tempFile, """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + """); + var config = VersionMarkConfig.ReadFromFile(tempFile); + + // Act - Run the capture pipeline for the dotnet tool + var versionInfo = config.FindVersions(["dotnet"], "test-capture-job"); + + // Assert - A version string should have been extracted + Assert.IsNotNull(versionInfo); + Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet"), + "FindVersions should capture the dotnet version"); + Assert.IsFalse(string.IsNullOrEmpty(versionInfo.Versions["dotnet"]), + "Captured version should be a non-empty string"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that the capture pipeline displays captured tool versions to the user. + /// + [TestMethod] + public void CaptureSubsystem_Run_DisplaysCapturedVersionsAfterCapture() + { + // Arrange - Set up temp directory with config and redirect console output to capture it + var currentDir = Directory.GetCurrentDirectory(); + var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var outputFile = PathHelpers.SafePathCombine(tempDir, "output.json"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText( + PathHelpers.SafePathCombine(tempDir, ".versionmark.yaml"), + """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + """); + Directory.SetCurrentDirectory(tempDir); + + var originalOut = Console.Out; + try + { + using var outWriter = new StringWriter(); + Console.SetOut(outWriter); + using var context = Context.Create([ + "--capture", "--job-id", "display-job", "--output", outputFile + ]); + + // Act - Run the full capture pipeline + Program.Run(context); + + // Assert - Tool names and versions should appear in the output + var output = outWriter.ToString(); + Assert.AreEqual(0, context.ExitCode); + Assert.IsTrue(output.Contains("dotnet"), + "Capture output should display captured tool names to the user"); + } + finally + { + Console.SetOut(originalOut); + } + } + finally + { + Directory.SetCurrentDirectory(currentDir); + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } + } +} diff --git a/test/DemaConsulting.VersionMark.Tests/Cli/CliSubsystemTests.cs b/test/DemaConsulting.VersionMark.Tests/Cli/CliSubsystemTests.cs new file mode 100644 index 0000000..d73f02e --- /dev/null +++ b/test/DemaConsulting.VersionMark.Tests/Cli/CliSubsystemTests.cs @@ -0,0 +1,233 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.VersionMark.Cli; + +namespace DemaConsulting.VersionMark.Tests.Cli; + +/// +/// Subsystem tests for the Cli subsystem (Program and Context working together). +/// +[TestClass] +public class CliSubsystemTests +{ + /// + /// Test that the full CLI pipeline with --version flag exits cleanly. + /// + [TestMethod] + public void CliSubsystem_Run_VersionFlag_ExitsCleanly() + { + // Arrange - Create a context with --version via the full CLI pipeline + using var context = Context.Create(["--version"]); + + // Act - Run the program through the CLI subsystem + Program.Run(context); + + // Assert - The CLI subsystem should exit with code 0 + Assert.AreEqual(0, context.ExitCode); + } + + /// + /// Test that the full CLI pipeline with --silent flag suppresses standard output. + /// + [TestMethod] + public void CliSubsystem_Run_SilentWithVersionFlag_SuppressesOutput() + { + // Arrange - Redirect console output to capture what the CLI writes + var originalOut = Console.Out; + try + { + using var outWriter = new StringWriter(); + Console.SetOut(outWriter); + + // Act - Run through the full Context + Program CLI pipeline with silent mode + using var context = Context.Create(["--silent", "--version"]); + Program.Run(context); + + // Assert - Silent mode should suppress all standard output + Assert.AreEqual(0, context.ExitCode); + Assert.IsTrue(string.IsNullOrEmpty(outWriter.ToString()), + "Silent flag should suppress version output through the full CLI pipeline"); + } + finally + { + Console.SetOut(originalOut); + } + } + + /// + /// Test that the full CLI pipeline with --help flag displays usage information. + /// + [TestMethod] + public void CliSubsystem_Run_HelpFlag_DisplaysUsageInformation() + { + // Arrange + var originalOut = Console.Out; + var output = new System.IO.StringWriter(); + Console.SetOut(output); + + try + { + using var context = Context.Create(["--help"]); + + // Act + Program.Run(context); + + // Assert + Assert.AreEqual(0, context.ExitCode); + var text = output.ToString(); + Assert.IsTrue(text.Length > 0); + Assert.IsTrue(text.Contains("--capture")); + } + finally + { + Console.SetOut(originalOut); + } + } + + /// + /// Test that the full CLI pipeline with --validate flag runs self-validation. + /// + [TestMethod] + public void CliSubsystem_Run_ValidateFlag_RunsValidation() + { + // Arrange + using var context = Context.Create(["--validate", "--silent"]); + + // Act + Program.Run(context); + + // Assert + Assert.AreEqual(0, context.ExitCode); + } + + /// + /// Test that the full CLI pipeline rejects unknown arguments by throwing ArgumentException. + /// + [TestMethod] + public void CliSubsystem_Run_InvalidArgs_ThrowsArgumentException() + { + // Arrange - No setup required; unknown flags are rejected by Context.Create + + // Act & Assert - Context.Create itself throws for unrecognized flags + Assert.ThrowsExactly(() => + { + using var context = Context.Create(["--unknown-flag-xyz"]); + Program.Run(context); + }); + } + + /// + /// Test that the full CLI pipeline with --lint flag succeeds for a valid config file. + /// + [TestMethod] + public void CliSubsystem_Run_LintFlag_ValidConfig_Succeeds() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".yaml"; + File.WriteAllText(tempFile, """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + """); + + try + { + using var context = Context.Create(["--lint", tempFile]); + + // Act + Program.Run(context); + + // Assert + Assert.AreEqual(0, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that the full CLI pipeline with --results flag writes validation results to a file. + /// + [TestMethod] + public void CliSubsystem_Run_ResultsFlag_WritesResultsFile() + { + // Arrange - Set up a results file path that should be written during --validate + var resultsFile = Path.GetTempFileName() + ".trx"; + try + { + using var context = Context.Create(["--validate", "--silent", "--results", resultsFile]); + + // Act - Run the full CLI pipeline with both --validate and --results + Program.Run(context); + + // Assert - The results file should exist and contain TRX content + Assert.AreEqual(0, context.ExitCode); + Assert.IsTrue(File.Exists(resultsFile), + "Results file should be written when --results flag is specified"); + var content = File.ReadAllText(resultsFile); + Assert.IsFalse(string.IsNullOrWhiteSpace(content), + "Results file should contain test result data"); + } + finally + { + if (File.Exists(resultsFile)) + { + File.Delete(resultsFile); + } + } + } + + /// + /// Test that the full CLI pipeline with --log flag writes output to a log file. + /// + [TestMethod] + public void CliSubsystem_Run_LogFlag_WritesOutputToLogFile() + { + // Arrange - Set up a log file that should be written with version output + var logFile = Path.GetTempFileName(); + try + { + string logContent; + using (var context = Context.Create(["--version", "--log", logFile])) + { + // Act - Run the full CLI pipeline with --log + Program.Run(context); + + // Assert - Exit code should be zero + Assert.AreEqual(0, context.ExitCode); + } + + // Assert - The log file should contain the version output (after context is disposed) + logContent = File.ReadAllText(logFile); + Assert.IsFalse(string.IsNullOrWhiteSpace(logContent), + "Log file should contain output when --log flag is specified"); + } + finally + { + if (File.Exists(logFile)) + { + File.Delete(logFile); + } + } + } +} diff --git a/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationSubsystemTests.cs b/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationSubsystemTests.cs new file mode 100644 index 0000000..9a06edf --- /dev/null +++ b/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationSubsystemTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.VersionMark.Configuration; + +namespace DemaConsulting.VersionMark.Tests.Configuration; + +/// +/// Subsystem tests for the Configuration subsystem (VersionMarkConfig and ToolConfig working together). +/// +[TestClass] +public class ConfigurationSubsystemTests +{ + /// + /// Test that reading a multi-tool configuration file produces all tools with usable commands and regexes. + /// + [TestMethod] + public void ConfigurationSubsystem_ReadFromFile_MultipleTools_AllToolsAccessible() + { + // Arrange - Write a valid multi-tool config to a temp file + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + git: + command: git --version + regex: 'git version (?[\d\.]+)' + """; + File.WriteAllText(tempFile, yaml); + + // Act - Read the config through the full Configuration subsystem pipeline + var config = VersionMarkConfig.ReadFromFile(tempFile); + + // Assert - Both tools should be accessible with valid commands and regexes + Assert.IsNotNull(config); + Assert.HasCount(2, config.Tools); + Assert.IsTrue(config.Tools.ContainsKey("dotnet"), "dotnet tool should be present"); + Assert.IsTrue(config.Tools.ContainsKey("git"), "git tool should be present"); + Assert.IsFalse(string.IsNullOrEmpty(config.Tools["dotnet"].GetEffectiveCommand()), + "dotnet command should be accessible"); + Assert.IsFalse(string.IsNullOrEmpty(config.Tools["git"].GetEffectiveRegex()), + "git regex should be accessible"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that reading a configuration file with OS-specific overrides selects the correct command. + /// + [TestMethod] + public void ConfigurationSubsystem_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand() + { + // Arrange - Write a config with OS-specific overrides to a temp file + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + command-win: dotnet.exe --version + command-linux: dotnet-linux --version + regex: '(?\d+\.\d+\.\d+)' + """; + File.WriteAllText(tempFile, yaml); + + // Act - Read the config and get the effective command for the current OS + var config = VersionMarkConfig.ReadFromFile(tempFile); + var effectiveCommand = config.Tools["dotnet"].GetEffectiveCommand(); + + // Assert - The effective command should match the OS-specific override for the current platform + if (OperatingSystem.IsWindows()) + { + Assert.AreEqual("dotnet.exe --version", effectiveCommand, + "On Windows the Windows override should be selected"); + } + else if (OperatingSystem.IsLinux()) + { + Assert.AreEqual("dotnet-linux --version", effectiveCommand, + "On Linux the Linux override should be selected"); + } + else + { + Assert.AreEqual("dotnet --version", effectiveCommand, + "On other platforms the default command should be selected"); + } + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that reading a configuration with OS-specific regex overrides returns the appropriate regex. + /// + [TestMethod] + public void ConfigurationSubsystem_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".yaml"; + File.WriteAllText(tempFile, """ + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + regex-win: '(?\d+\.\d+\.\d+)-win' + regex-linux: '(?\d+\.\d+\.\d+)-linux' + """); + + try + { + // Act + var config = VersionMarkConfig.ReadFromFile(tempFile); + var tool = config.Tools["dotnet"]; + var effectiveRegex = tool.GetEffectiveRegex(); + + // Assert - The effective regex should match the OS-specific override for the current platform + if (OperatingSystem.IsWindows()) + { + Assert.AreEqual(@"(?\d+\.\d+\.\d+)-win", effectiveRegex, + "On Windows the Windows regex override should be selected"); + } + else if (OperatingSystem.IsLinux()) + { + Assert.AreEqual(@"(?\d+\.\d+\.\d+)-linux", effectiveRegex, + "On Linux the Linux regex override should be selected"); + } + else + { + Assert.AreEqual(@"(?\d+\.\d+\.\d+)", effectiveRegex, + "On other platforms the default regex should be selected"); + } + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that reading a configuration with an empty tools section throws an ArgumentException. + /// + [TestMethod] + public void ConfigurationSubsystem_ReadFromFile_EmptyTools_ThrowsArgumentException() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".yaml"; + File.WriteAllText(tempFile, """ + tools: + """); + + try + { + // Act & Assert + Assert.ThrowsExactly(() => VersionMarkConfig.ReadFromFile(tempFile)); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that reading a configuration with invalid YAML throws an ArgumentException. + /// + [TestMethod] + public void ConfigurationSubsystem_ReadFromFile_InvalidYaml_ThrowsArgumentException() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".yaml"; + File.WriteAllText(tempFile, "invalid: yaml: content: [[["); + + try + { + // Act & Assert + Assert.ThrowsExactly(() => VersionMarkConfig.ReadFromFile(tempFile)); + } + finally + { + File.Delete(tempFile); + } + } +} diff --git a/test/DemaConsulting.VersionMark.Tests/Linting/LintingSubsystemTests.cs b/test/DemaConsulting.VersionMark.Tests/Linting/LintingSubsystemTests.cs new file mode 100644 index 0000000..0029be4 --- /dev/null +++ b/test/DemaConsulting.VersionMark.Tests/Linting/LintingSubsystemTests.cs @@ -0,0 +1,355 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.VersionMark.Cli; +using DemaConsulting.VersionMark.Linting; + +namespace DemaConsulting.VersionMark.Tests.Linting; + +/// +/// Subsystem tests for the Linting subsystem (Lint and Context working together). +/// +[TestClass] +public class LintingSubsystemTests +{ + /// + /// Test that the full linting pipeline succeeds and exits cleanly for a valid configuration. + /// + [TestMethod] + public void LintingSubsystem_Lint_ValidConfig_SucceedsWithZeroExitCode() + { + // Arrange - Write a complete and valid configuration to a temp file + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + git: + command: git --version + regex: 'git version (?[\d\.]+)' + """; + File.WriteAllText(tempFile, yaml); + using var context = Context.Create(["--silent"]); + + // Act - Run the full linting pipeline + var result = Lint.Run(context, tempFile); + + // Assert - The linting subsystem should report success with a clean exit code + Assert.IsTrue(result, "Linting should succeed for a valid configuration"); + Assert.AreEqual(0, context.ExitCode, + "Exit code should be zero after successful lint through the full linting pipeline"); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that the linting pipeline reports all errors in a single pass for an invalid configuration. + /// + [TestMethod] + public void LintingSubsystem_Lint_MultipleErrors_ReportsAllErrorsInSinglePass() + { + // Arrange - Write a configuration with multiple errors to a temp file + // tool1 is missing 'regex'; tool2 is missing 'command' and has a regex without a 'version' group + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + tool1: + command: tool1 --version + tool2: + regex: 'no-version-group' + """; + File.WriteAllText(tempFile, yaml); + + // Context without --silent so errors are written to Console.Error + using var context = Context.Create([]); + var originalError = Console.Error; + try + { + using var errorWriter = new StringWriter(); + Console.SetError(errorWriter); + + // Act - Run the full linting pipeline against a config with multiple errors + var result = Lint.Run(context, tempFile); + + // Assert - The linting subsystem should report failure and emit findings for both tools + Assert.IsFalse(result, + "Linting should fail for a configuration with multiple errors"); + Assert.AreEqual(1, context.ExitCode, + "Exit code should be non-zero when linting finds errors"); + + var errorOutput = errorWriter.ToString(); + StringAssert.Contains(errorOutput, "tool1", + "Error output should mention tool1 (missing regex)"); + StringAssert.Contains(errorOutput, "tool2", + "Error output should mention tool2 (missing command and invalid regex)"); + } + finally + { + Console.SetError(originalError); + } + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that the linting pipeline fails for invalid YAML content. + /// + [TestMethod] + public void LintingSubsystem_Lint_InvalidYaml_Fails() + { + // Arrange + var tempFile = Path.GetTempFileName() + ".yaml"; + File.WriteAllText(tempFile, "tools:\n dotnet:\n command: [unclosed bracket"); + + try + { + using var context = Context.Create(["--silent"]); + + // Act + var result = Lint.Run(context, tempFile); + + // Assert + Assert.IsFalse(result); + Assert.AreEqual(1, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that linting reports an error when the config file does not exist. + /// + [TestMethod] + public void LintingSubsystem_Lint_NonExistentFile_Fails() + { + // Arrange + var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".yaml"); + using var context = Context.Create(["--silent"]); + + // Act + var result = Lint.Run(context, nonExistentPath); + + // Assert + Assert.IsFalse(result); + Assert.AreEqual(1, context.ExitCode); + } + + /// + /// Test that linting reports an error when a regex cannot be compiled. + /// + [TestMethod] + public void LintingSubsystem_Lint_InvalidRegex_ReportsError() + { + // Arrange - Write a config with a syntactically broken regex (unclosed group) + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + regex: '(? + /// Test that linting reports an error when a regex does not contain a named 'version' capture group. + /// + [TestMethod] + public void LintingSubsystem_Lint_RegexWithoutVersionGroup_ReportsError() + { + // Arrange - Write a config with a valid regex that lacks the required 'version' group + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + regex: '\d+\.\d+\.\d+' + """; + File.WriteAllText(tempFile, yaml); + using var context = Context.Create(["--silent"]); + + // Act - Run lint on a tool whose regex has no 'version' named capture group + var result = Lint.Run(context, tempFile); + + // Assert - Lint should fail because the 'version' group is required + Assert.IsFalse(result, + "Lint should fail when a regex does not contain a named 'version' capture group"); + Assert.AreEqual(1, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that linting reports an error when an OS-specific command override is empty. + /// + [TestMethod] + public void LintingSubsystem_Lint_EmptyOsSpecificOverride_ReportsError() + { + // Arrange - Write a config with an empty command-win override + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + command-win: '' + regex: '(?\d+\.\d+\.\d+)' + """; + File.WriteAllText(tempFile, yaml); + using var context = Context.Create(["--silent"]); + + // Act - Run lint on a tool with an empty OS-specific override + var result = Lint.Run(context, tempFile); + + // Assert - Lint should fail because empty OS-specific overrides are not allowed + Assert.IsFalse(result, + "Lint should fail when an OS-specific command override is empty"); + Assert.AreEqual(1, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that linting treats unknown keys as non-fatal warnings and succeeds. + /// + [TestMethod] + public void LintingSubsystem_Lint_UnknownKey_IsWarningNotError() + { + // Arrange - Write a config with a valid tool plus an unknown key + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + command: dotnet --version + regex: '(?\d+\.\d+\.\d+)' + unknown-tool-key: should-not-fail + """; + File.WriteAllText(tempFile, yaml); + using var context = Context.Create(["--silent"]); + + // Act - Run lint on a config containing an unknown tool key + var result = Lint.Run(context, tempFile); + + // Assert - Lint should succeed; unknown keys produce warnings, not errors + Assert.IsTrue(result, + "Lint should succeed when only unknown keys are present (warnings are non-fatal)"); + Assert.AreEqual(0, context.ExitCode); + } + finally + { + File.Delete(tempFile); + } + } + + /// + /// Test that linting error messages include the filename and line/column location. + /// + [TestMethod] + public void LintingSubsystem_Lint_Error_IncludesFileAndLineInfo() + { + // Arrange - Write a config missing the required 'command' field and capture error output + var tempFile = Path.GetTempFileName(); + try + { + const string yaml = """ + --- + tools: + dotnet: + regex: '(?\d+\.\d+\.\d+)' + """; + File.WriteAllText(tempFile, yaml); + + // Context without --silent so errors are written to Console.Error + using var context = Context.Create([]); + var originalError = Console.Error; + try + { + using var errWriter = new StringWriter(); + Console.SetError(errWriter); + + // Act - Run lint on a config with a missing command field + var result = Lint.Run(context, tempFile); + + // Assert - The error message should contain the filename and line/column info + Assert.IsFalse(result); + var errorOutput = errWriter.ToString(); + StringAssert.Contains(errorOutput, Path.GetFileName(tempFile), + "Error message should include the config filename"); + Assert.IsTrue( + errorOutput.Contains("(") && errorOutput.Contains(",") && errorOutput.Contains(")"), + "Error message should include line and column location information"); + } + finally + { + Console.SetError(originalError); + } + } + finally + { + File.Delete(tempFile); + } + } +} diff --git a/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingSubsystemTests.cs b/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingSubsystemTests.cs new file mode 100644 index 0000000..2ac32a0 --- /dev/null +++ b/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingSubsystemTests.cs @@ -0,0 +1,255 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.VersionMark.Capture; +using DemaConsulting.VersionMark.Cli; +using DemaConsulting.VersionMark.Publishing; +using DemaConsulting.VersionMark.SelfTest; + +namespace DemaConsulting.VersionMark.Tests.Publishing; + +/// +/// Subsystem tests for the Publishing subsystem (capture data to markdown report pipeline). +/// +[TestClass] +public class PublishingSubsystemTests +{ + /// + /// Test that the publishing pipeline produces a valid markdown report from multiple captures. + /// + [TestMethod] + public void PublishingSubsystem_Format_MultipleCaptureFiles_ProducesConsolidatedReport() + { + // Arrange - Create version infos representing captures from multiple CI jobs + var versionInfos = new[] + { + new VersionInfo("job-linux", + new Dictionary + { + ["dotnet"] = "8.0.100", + ["git"] = "2.43.0" + }), + new VersionInfo("job-windows", + new Dictionary + { + ["dotnet"] = "8.0.100", + ["git"] = "2.43.0" + }) + }; + + // Act - Run the full publishing pipeline to produce a markdown report + var report = MarkdownFormatter.Format(versionInfos); + + // Assert - The report should contain version information for all tools + Assert.IsFalse(string.IsNullOrWhiteSpace(report), + "The publishing pipeline should produce a non-empty report"); + Assert.Contains("dotnet", report, "Report should include the dotnet tool"); + Assert.Contains("git", report, "Report should include the git tool"); + Assert.Contains("8.0.100", report, "Report should include the dotnet version"); + } + + /// + /// Test that the publishing pipeline consolidates identical versions across jobs. + /// + [TestMethod] + public void PublishingSubsystem_Format_IdenticalVersionsAcrossJobs_ConsolidatesVersions() + { + // Arrange - Create version infos with the same dotnet version across all jobs + var versionInfos = new[] + { + new VersionInfo("job-1", new Dictionary { ["dotnet"] = "8.0.100" }), + new VersionInfo("job-2", new Dictionary { ["dotnet"] = "8.0.100" }), + new VersionInfo("job-3", new Dictionary { ["dotnet"] = "8.0.100" }) + }; + + // Act - Run the publishing pipeline + var report = MarkdownFormatter.Format(versionInfos); + + // Assert - The report should show a single consolidated version, not per-job versions + Assert.Contains("8.0.100", report, "Report should include the consolidated version"); + Assert.DoesNotContain("job-1", report, + "Consolidated versions should not show individual job IDs"); + } + + /// + /// Test that the publishing pipeline shows individual job IDs when versions conflict across jobs. + /// + [TestMethod] + public void PublishingSubsystem_Format_ConflictingVersions_ShowsJobIds() + { + // Arrange + var versionInfoA = new VersionInfo("job-a", new Dictionary + { + { "dotnet", "8.0.100" } + }); + var versionInfoB = new VersionInfo("job-b", new Dictionary + { + { "dotnet", "9.0.200" } + }); + var versionInfos = new[] { versionInfoA, versionInfoB }; + + // Act + var report = MarkdownFormatter.Format(versionInfos); + + // Assert + StringAssert.Contains(report, "job-a"); + StringAssert.Contains(report, "job-b"); + } + + /// + /// Test that the publishing pipeline uses the correct heading level when a custom report depth is specified. + /// + [TestMethod] + public void PublishingSubsystem_Format_WithCustomDepth_UsesCorrectHeadingLevel() + { + // Arrange + var versionInfo = new VersionInfo("job-1", new Dictionary + { + { "dotnet", "8.0.100" } + }); + var versionInfos = new[] { versionInfo }; + + // Act + var report = MarkdownFormatter.Format(versionInfos, reportDepth: 3); + + // Assert + StringAssert.Contains(report, "###"); + } + + /// + /// Test that the publishing pipeline requires the --report parameter and reports an error when it is missing. + /// + [TestMethod] + public void PublishingSubsystem_Run_WithoutReport_ReportsError() + { + // Arrange - Create a publish context without --report + var originalError = Console.Error; + try + { + using var errWriter = new StringWriter(); + Console.SetError(errWriter); + using var context = Context.Create(["--publish"]); + + // Act - Run the publish pipeline without --report + Program.Run(context); + + // Assert - An error should be reported and exit code should be non-zero + Assert.AreEqual(1, context.ExitCode, + "Publishing without --report should result in a non-zero exit code"); + StringAssert.Contains(errWriter.ToString(), "--report", + "Error message should mention the missing --report parameter"); + } + finally + { + Console.SetError(originalError); + } + } + + /// + /// Test that the publishing pipeline accepts glob patterns after -- and reads all matching files. + /// + [TestMethod] + public void PublishingSubsystem_Run_WithGlobPattern_ReadsMatchingFiles() + { + // Arrange - Create a temp directory with JSON files and use a glob pattern to match them + var currentDir = Directory.GetCurrentDirectory(); + var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); + try + { + Directory.CreateDirectory(tempDir); + var versionInfo = new VersionInfo("job-glob", new Dictionary { ["dotnet"] = "8.0.100" }); + versionInfo.SaveToFile(PathHelpers.SafePathCombine(tempDir, "versionmark-glob-job.json")); + Directory.SetCurrentDirectory(tempDir); + + using var context = Context.Create([ + "--publish", "--report", reportFile, "--silent", "--", "versionmark-*.json" + ]); + + // Act - Run the publish pipeline with a glob pattern + Program.Run(context); + + // Assert - The report should have been generated from the matched file + Assert.AreEqual(0, context.ExitCode, + "Publishing with a valid glob pattern should succeed"); + Assert.IsTrue(File.Exists(reportFile), + "Report file should be created when glob pattern matches files"); + StringAssert.Contains(File.ReadAllText(reportFile), "dotnet", + "Report should contain content from the matched JSON file"); + } + finally + { + Directory.SetCurrentDirectory(currentDir); + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } + } + + /// + /// Test that the publishing pipeline reports an error when a JSON file is malformed. + /// + [TestMethod] + public void PublishingSubsystem_Run_WithMalformedJsonFile_ReportsError() + { + // Arrange - Create a temp directory with a malformed JSON file + var currentDir = Directory.GetCurrentDirectory(); + var tempDir = PathHelpers.SafePathCombine(Path.GetTempPath(), Path.GetRandomFileName()); + var reportFile = PathHelpers.SafePathCombine(tempDir, "report.md"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText( + PathHelpers.SafePathCombine(tempDir, "versionmark-bad.json"), + "{ this is not valid JSON }"); + Directory.SetCurrentDirectory(tempDir); + + var originalError = Console.Error; + try + { + using var errWriter = new StringWriter(); + Console.SetError(errWriter); + using var context = Context.Create([ + "--publish", "--report", reportFile, "--", "versionmark-*.json" + ]); + + // Act - Run the publish pipeline with a malformed JSON file + Program.Run(context); + + // Assert - An error should be reported + Assert.AreEqual(1, context.ExitCode, + "Publishing with malformed JSON should result in a non-zero exit code"); + } + finally + { + Console.SetError(originalError); + } + } + finally + { + Directory.SetCurrentDirectory(currentDir); + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } + } +} diff --git a/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestSubsystemTests.cs b/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestSubsystemTests.cs new file mode 100644 index 0000000..f6829ed --- /dev/null +++ b/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestSubsystemTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.VersionMark.Cli; +using DemaConsulting.VersionMark.SelfTest; + +namespace DemaConsulting.VersionMark.Tests.SelfTest; + +/// +/// Subsystem tests for the SelfTest subsystem (Validation and PathHelpers working together). +/// +[TestClass] +public class SelfTestSubsystemTests +{ + /// + /// Test that PathHelpers prevents path traversal attacks within the self-test subsystem context. + /// + [TestMethod] + public void SelfTestSubsystem_PathHelpers_PathTraversal_ThrowsArgumentException() + { + // Arrange - Define a base directory and an attacker-controlled traversal path + var baseDir = AppContext.BaseDirectory; + const string traversalPath = "../../../etc/passwd"; + + // Act & Assert - The self-test subsystem path helper should reject traversal attempts + Assert.ThrowsExactly(() => + PathHelpers.SafePathCombine(baseDir, traversalPath), + "PathHelpers should reject path traversal attempts that escape the base directory"); + } + + /// + /// Test that PathHelpers correctly combines valid paths within the self-test subsystem context. + /// + [TestMethod] + public void SelfTestSubsystem_PathHelpers_ValidRelativePath_ProducesExpectedPath() + { + // Arrange - Use the application base directory as the root + var baseDir = AppContext.BaseDirectory; + const string relativePath = "test-results/output.trx"; + + // Act - Combine the base directory with a valid relative path + var result = PathHelpers.SafePathCombine(baseDir, relativePath); + + // Assert - The combined path should be under the base directory + Assert.IsFalse(string.IsNullOrEmpty(result), + "Valid path combination should produce a non-empty result"); + Assert.IsTrue(result.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase) || + Path.GetFullPath(result).StartsWith(Path.GetFullPath(baseDir), StringComparison.OrdinalIgnoreCase), + "Combined path should be rooted within the base directory"); + } + + /// + /// Test that the self-test subsystem can locate the main DLL in the base directory. + /// + [TestMethod] + public void SelfTestSubsystem_FindsDllInBaseDirectory() + { + // Arrange + var dllPath = PathHelpers.SafePathCombine(AppContext.BaseDirectory, "DemaConsulting.VersionMark.dll"); + + // Act & Assert + Assert.IsTrue(File.Exists(dllPath)); + } + + /// + /// Test that the self-validation pipeline writes results to a TRX file when --results is specified. + /// + [TestMethod] + public void SelfTestSubsystem_Run_WithResultsFlag_WritesResultsFile() + { + // Arrange - Set up a TRX results file path + var resultsFile = Path.GetTempFileName() + ".trx"; + try + { + using var context = Context.Create(["--validate", "--silent", "--results", resultsFile]); + + // Act - Run self-validation with --results to write TRX output + Program.Run(context); + + // Assert - The TRX file should exist and contain XML content + Assert.AreEqual(0, context.ExitCode); + Assert.IsTrue(File.Exists(resultsFile), + "Self-validation should write results to the file specified by --results"); + var content = File.ReadAllText(resultsFile); + Assert.IsTrue(content.Contains("TestRun") || content.Contains("testsuites"), + "Results file should contain TRX or JUnit test result data"); + } + finally + { + if (File.Exists(resultsFile)) + { + File.Delete(resultsFile); + } + } + } +}