diff --git a/.cspell.yaml b/.cspell.yaml index 89c9935..3b9cdbf 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -19,7 +19,7 @@ words: - Dema - fileassert - msbuild - - mstest + - opencover - pandoc - reqstream - reviewmark @@ -29,6 +29,8 @@ words: - versionmark - weasyprint - Weasy + - xunit + - Xunit - yamlfix # Exclude common build artifacts, dependencies, and vendored third-party code @@ -39,6 +41,7 @@ ignorePaths: - "**/thirdparty/**" - "**/third-party/**" - "**/3rd-party/**" + - "**/generated/**" - "**/AGENT_REPORT_*.md" - "**/.agent-logs/**" - "**/bin/**" diff --git a/.fileassert.yaml b/.fileassert.yaml index 5de9a7c..e81cfbd 100644 --- a/.fileassert.yaml +++ b/.fileassert.yaml @@ -1,7 +1,7 @@ --- # FileAssert document validation tests for VersionMark. # Tests are tagged by document group to allow per-group execution during the build pipeline. -# Tags: build-notes, code-quality, code-review, design, user-guide, requirements. +# Tags: build-notes, code-quality, code-review, design, verification, user-guide, requirements. # # NOTE: build-notes through user-guide tests provide OTS evidence for Pandoc and WeasyPrint # and run before ReqStream. The requirements tests run after ReqStream and validate the @@ -15,7 +15,7 @@ tests: description: "Build Notes HTML was generated by Pandoc" tags: [build-notes] files: - - pattern: "docs/build_notes/build_notes.html" + - pattern: "docs/build_notes/generated/build_notes.html" count: 1 html: - query: "//head/title" @@ -27,7 +27,7 @@ tests: description: "Build Notes PDF was generated by WeasyPrint" tags: [build-notes] files: - - pattern: "docs/VersionMark Build Notes.pdf" + - pattern: "docs/generated/VersionMark Build Notes.pdf" count: 1 pdf: metadata: @@ -48,7 +48,7 @@ tests: description: "Code Quality HTML was generated by Pandoc" tags: [code-quality] files: - - pattern: "docs/code_quality/quality.html" + - pattern: "docs/code_quality/generated/quality.html" count: 1 html: - query: "//head/title" @@ -60,7 +60,7 @@ tests: description: "Code Quality PDF was generated by WeasyPrint" tags: [code-quality] files: - - pattern: "docs/VersionMark Code Quality.pdf" + - pattern: "docs/generated/VersionMark Code Quality.pdf" count: 1 pdf: metadata: @@ -81,7 +81,7 @@ tests: description: "Code Review Plan HTML was generated by Pandoc" tags: [code-review] files: - - pattern: "docs/code_review_plan/plan.html" + - pattern: "docs/code_review_plan/generated/plan.html" count: 1 html: - query: "//head/title" @@ -93,7 +93,7 @@ tests: description: "Code Review Plan PDF was generated by WeasyPrint" tags: [code-review] files: - - pattern: "docs/VersionMark Review Plan.pdf" + - pattern: "docs/generated/VersionMark Review Plan.pdf" count: 1 pdf: metadata: @@ -114,7 +114,7 @@ tests: description: "Code Review Report HTML was generated by Pandoc" tags: [code-review] files: - - pattern: "docs/code_review_report/report.html" + - pattern: "docs/code_review_report/generated/report.html" count: 1 html: - query: "//head/title" @@ -126,7 +126,7 @@ tests: description: "Code Review Report PDF was generated by WeasyPrint" tags: [code-review] files: - - pattern: "docs/VersionMark Review Report.pdf" + - pattern: "docs/generated/VersionMark Review Report.pdf" count: 1 pdf: metadata: @@ -147,7 +147,7 @@ tests: description: "Design HTML was generated by Pandoc" tags: [design] files: - - pattern: "docs/design/design.html" + - pattern: "docs/design/generated/design.html" count: 1 html: - query: "//head/title" @@ -159,7 +159,7 @@ tests: description: "Design PDF was generated by WeasyPrint" tags: [design] files: - - pattern: "docs/VersionMark Software Design.pdf" + - pattern: "docs/generated/VersionMark Software Design.pdf" count: 1 pdf: metadata: @@ -174,13 +174,46 @@ tests: text: - contains: "Design" + # --- VERIFICATION --- + + - name: Pandoc_VerificationHtml + description: "Verification HTML was generated by Pandoc" + tags: [verification] + files: + - pattern: "docs/verification/generated/verification.html" + count: 1 + html: + - query: "//head/title" + count: 1 + text: + - contains: "Verification" + + - name: WeasyPrint_VerificationPdf + description: "Verification PDF was generated by WeasyPrint" + tags: [verification] + files: + - pattern: "docs/generated/VersionMark Software Verification Design.pdf" + count: 1 + pdf: + metadata: + - field: "Title" + contains: "Verification" + - field: "Author" + contains: "DEMA Consulting" + - field: "Subject" + contains: "Verification design document" + pages: + min: 3 + text: + - contains: "Verification" + # --- USER GUIDE --- - name: Pandoc_UserGuideHtml description: "User Guide HTML was generated by Pandoc" tags: [user-guide] files: - - pattern: "docs/user_guide/user_guide.html" + - pattern: "docs/user_guide/generated/user_guide.html" count: 1 html: - query: "//head/title" @@ -192,7 +225,7 @@ tests: description: "User Guide PDF was generated by WeasyPrint" tags: [user-guide] files: - - pattern: "docs/VersionMark User Guide.pdf" + - pattern: "docs/generated/VersionMark User Guide.pdf" count: 1 pdf: metadata: @@ -201,7 +234,7 @@ tests: - field: "Author" contains: "DEMA Consulting" - field: "Subject" - contains: "Reference Implementation" + contains: "User guide" pages: min: 3 text: @@ -214,7 +247,7 @@ tests: description: "Requirements HTML was generated by Pandoc" tags: [requirements] files: - - pattern: "docs/requirements_doc/requirements.html" + - pattern: "docs/requirements_doc/generated/requirements.html" count: 1 html: - query: "//head/title" @@ -226,7 +259,7 @@ tests: description: "Requirements PDF was generated by WeasyPrint" tags: [requirements] files: - - pattern: "docs/VersionMark Requirements.pdf" + - pattern: "docs/generated/VersionMark Requirements.pdf" count: 1 pdf: metadata: @@ -248,7 +281,7 @@ tests: description: "Trace Matrix HTML was generated by Pandoc" tags: [requirements] files: - - pattern: "docs/requirements_report/trace_matrix.html" + - pattern: "docs/requirements_report/generated/trace_matrix.html" count: 1 html: - query: "//head/title" @@ -260,7 +293,7 @@ tests: description: "Trace Matrix PDF was generated by WeasyPrint" tags: [requirements] files: - - pattern: "docs/VersionMark Trace Matrix.pdf" + - pattern: "docs/generated/VersionMark Trace Matrix.pdf" count: 1 pdf: metadata: diff --git a/.github/agents/developer.agent.md b/.github/agents/developer.agent.md index 35f5dda..a95c562 100644 --- a/.github/agents/developer.agent.md +++ b/.github/agents/developer.agent.md @@ -21,7 +21,7 @@ Perform software development tasks by determining and applying appropriate stand 5. **Formatting**: Run `pwsh ./fix.ps1` to silently apply all available auto-fixers (dotnet format, markdown, YAML) before committing 6. **Build and test** (code changes only): Run `pwsh ./build.ps1` and confirm it - passes — report FAILED if the build or any tests fail + passes - report FAILED if the build or any tests fail 7. **Generate completion report** per the AGENTS.md reporting requirements - save to `.agent-logs/{agent-name}-{subject}-{unique-id}.md` and return the summary to the caller diff --git a/.github/agents/formal-review.agent.md b/.github/agents/formal-review.agent.md index 88b0691..7dd8e84 100644 --- a/.github/agents/formal-review.agent.md +++ b/.github/agents/formal-review.agent.md @@ -20,6 +20,8 @@ Before reviewing, read these standards to inform review judgments: hierarchy and categorization review judgments - **`design-documentation.md`** - defines mandatory sections, structural conventions, and coverage expected at each level; informs all design documentation review judgments +- **`verification-documentation.md`** - defines mandatory sections, structural conventions, + and coverage expected at each level; informs all verification design review judgments For review sets that include source code or tests, also consult the relevant standards from the selection matrix in AGENTS.md. diff --git a/.github/agents/lint-fix.agent.md b/.github/agents/lint-fix.agent.md index 83ad8cb..549e751 100644 --- a/.github/agents/lint-fix.agent.md +++ b/.github/agents/lint-fix.agent.md @@ -36,7 +36,12 @@ submission, not during normal development. - **markdownlint MD013 (line length)**: Wrap long lines at natural break points, after commas, before conjunctions, or at sentence boundaries. Do not break - in the middle of a code span or URL. + in the middle of a code span or URL. **Pipe-tables that cannot be wrapped + without breaking structure** are a special case - convert them to a bullet + list if the data reads naturally that way, or rewrite as a + [grid table](https://pandoc.org/MANUAL.html#tables) if a tabular layout is + essential. Do not get stuck trying to squeeze a wide pipe-table into 120 + characters. - **markdownlint other rules**: Apply the specific fix indicated in the output (e.g., missing blank lines, heading levels, code fence languages). diff --git a/.github/agents/repo-consistency.agent.md b/.github/agents/repo-consistency.agent.md deleted file mode 100644 index 5dbe99f..0000000 --- a/.github/agents/repo-consistency.agent.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: repo-consistency -description: > - Ensures downstream repositories remain consistent with the TemplateDotNetTool - template patterns and best practices. -user-invocable: true ---- - -# Repo Consistency Agent - -Maintain consistency between downstream projects and the TemplateDotNetTool template, ensuring repositories -benefit from template evolution while respecting project-specific customizations. - -# Consistency Workflow (MANDATORY) - -**CRITICAL**: This agent MUST follow these steps systematically to ensure proper template consistency analysis: - -1. **Fetch Recent Template Changes**: Use GitHub search to fetch the 20 most recently merged PRs - (`is:pr is:merged sort:updated-desc`) from -2. **Analyze Template Evolution**: For each relevant PR, determine the intent and scope of changes - (what files were modified, what improvements were made) -3. **Assess Downstream Applicability**: Evaluate which template changes would benefit this repository - while respecting project-specific customizations -4. **Apply Appropriate Updates**: Implement applicable template improvements with proper translation for project context -5. **Validate Consistency**: Verify that applied changes maintain functionality and follow project patterns -6. **Generate completion report** per the AGENTS.md reporting requirements - save to - `.agent-logs/{agent-name}-{subject}-{unique-id}.md` and return the summary to the caller - -## Key Principles - -- **Evolutionary Consistency**: Template improvements should enhance downstream projects systematically -- **Intelligent Customization Respect**: Distinguish valid customizations from unintentional drift -- **Incremental Template Adoption**: Support phased adoption of template improvements based on project capacity - -# Don't Do These Things - -- **Never recommend changes without understanding project context** (some differences are intentional) -- **Never flag valid project-specific customizations** as consistency problems -- **Never apply template changes blindly** without assessing downstream project impact -- **Never ignore template evolution benefits** when they clearly improve downstream projects -- **Never recommend breaking changes** without migration guidance and impact assessment -- **Never skip validation** of preserved functionality after template alignment -- **Never assume all template patterns apply universally** (assess project-specific needs) - -# Report Template - -```markdown -# Repo Consistency Report - -**Result**: (SUCCEEDED|FAILED) - -## Consistency Analysis - -- **Template PRs Analyzed**: {Number and timeframe of PRs reviewed} -- **Template Changes Identified**: {Count and types of template improvements} -- **Applicable Updates**: {Changes determined suitable for this repository} -- **Project Customizations Preserved**: {Valid differences maintained} - -## Template Evolution Applied - -- **Files Modified**: {List of files updated for template consistency} -- **Improvements Adopted**: {Specific template enhancements implemented} -- **Configuration Updates**: {Tool configurations, workflows, or standards updated} - -## Consistency Status - -- **Template Alignment**: {Overall consistency rating with template} -- **Customization Respect**: {How project-specific needs were preserved} -- **Functionality Validation**: {Verification that changes don't break existing features} -- **Future Consistency**: {Recommendations for ongoing template alignment} - -## Issues Resolved - -- **Drift Corrections**: {Template drift issues addressed} -- **Enhancement Adoptions**: {Template improvements successfully integrated} -- **Validation Results**: {Testing and validation outcomes} -``` diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 1f4b90f..82a413e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -26,16 +26,11 @@ Before submitting this pull request, ensure you have completed the following: ### Build and Test -- [ ] Code builds successfully: `dotnet build --configuration Release` -- [ ] All unit tests pass: `dotnet test --configuration Release` -- [ ] Self-validation tests pass: - `dotnet run --project src/DemaConsulting.VersionMark --configuration Release --framework net10.0` - `--no-build -- --validate` +- [ ] Code builds successfully and all tests pass: `pwsh ./build.ps1` - [ ] Code produces zero warnings ### Code Quality -- [ ] Code formatting is correct: `dotnet format --verify-no-changes` - [ ] New code has appropriate XML documentation comments - [ ] Static analyzer warnings have been addressed @@ -43,9 +38,7 @@ Before submitting this pull request, ensure you have completed the following: Please run the following checks before submitting: -- [ ] **Spell checker passes**: `cspell "**/*.{md,cs}"` -- [ ] **Markdown linter passes**: `markdownlint "**/*.md"` -- [ ] **YAML linter passes**: `yamllint .` +- [ ] **All linters pass**: `pwsh ./lint.ps1` ### Testing @@ -57,7 +50,7 @@ Please run the following checks before submitting: ### Documentation - [ ] Updated README.md (if applicable) -- [ ] Updated ARCHITECTURE.md (if applicable) +- [ ] Updated docs/ documentation (if applicable) - [ ] Added code examples for new features (if applicable) - [ ] Updated requirements.yaml (if applicable) diff --git a/.github/standards/coding-principles.md b/.github/standards/coding-principles.md index 213c031..9e67fbb 100644 --- a/.github/standards/coding-principles.md +++ b/.github/standards/coding-principles.md @@ -20,11 +20,35 @@ All code MUST follow literate programming principles: matches design intent without reading the full codebase - **Logical Separation**: Complex functions use block comments to separate and describe logical steps within the implementation -- **Public Documentation**: All public interfaces have comprehensive documentation - because consumers and auditors rely on interface contracts for integration - and compliance verification +- **Full Symbol Documentation**: ALL symbols have comprehensive documentation + because reviewers and auditors must verify every implementation detail, not + just the public interface - access-level specifics (public, protected, + private, internal, etc.) vary by language; see the language-specific standard - **Clarity Over Cleverness**: Code should be immediately understandable by team members +## API Documentation + +Good API documentation enables consumers, reviewers, and agents to use an +interface correctly without reading the implementation: + +- **Self-Contained**: Each member's documentation must be fully understandable + in isolation - consumers must not need to read the implementation to call it + correctly +- **Intent-Focused**: Explain WHY the member exists and WHAT problem it solves, + not just restate the name - this lets reviewers verify the implementation + matches design intent +- **Parameter and Return Contracts**: Document valid ranges, null handling, and + boundary cases - agents and consumers rely on these contracts to call the API + correctly +- **Error Conditions**: Document every exception or error code, the condition + that triggers it, and how the caller should respond - undocumented errors + cannot be handled correctly +- **Side Effects**: Document I/O, state mutation, resource allocation, or + network calls - hidden side effects cause integration bugs that are hard to + diagnose +- **Thread Safety**: State whether the API is safe for concurrent use - missing + this forces consumers to read the implementation or risk data races + ## Universal Code Architecture Principles ### Design Patterns diff --git a/.github/standards/csharp-language.md b/.github/standards/csharp-language.md index 707b0f9..6df39cd 100644 --- a/.github/standards/csharp-language.md +++ b/.github/standards/csharp-language.md @@ -4,37 +4,63 @@ description: Follow these standards when developing C# source code. globs: ["**/*.cs"] --- -# C# Language Development Standard - -## Required Standards +# Required Standards Read these standards first before applying this standard: - **`coding-principles.md`** - Universal coding principles and quality gates -# File Patterns - -- **Source Files**: `**/*.cs` +# API Documentation and Literate Coding Example -# Literate Coding Example +The example below demonstrates good XmlDoc API documentation combined with +literate coding comments. ```csharp -// Validate input parameters to prevent downstream errors -if (string.IsNullOrEmpty(input)) +/// +/// Converts a raw sensor reading into a validated measurement ready for downstream consumers. +/// +/// +/// Clamping is preferred over throwing on out-of-range values because sensor drift at +/// range boundaries is expected; clamping produces a usable result where rejection would +/// discard valid near-boundary readings. Stateless and thread-safe; the calibration +/// profile is read but never modified. +/// +/// Raw sensor value. Must be finite (NaN and infinities are rejected). +/// Calibration profile providing offset and range. Must not be null. +/// Corrected value clamped to [calibration.Minimum, calibration.Maximum]. +/// Thrown when is NaN or infinite. +/// Thrown when is null. +public double ProcessReading(double reading, CalibrationProfile calibration) { - throw new ArgumentException("Input cannot be null or empty", nameof(input)); -} - -// Transform input data using the configured processing pipeline -var processedData = ProcessingPipeline.Transform(input); + // Reject invalid inputs before any calculation - non-finite readings cannot be + // corrected, and a null calibration profile provides no offset or range to apply + if (!double.IsFinite(reading)) + throw new ArgumentException("Reading must be a finite number.", nameof(reading)); + ArgumentNullException.ThrowIfNull(calibration); -// Apply business rules and validation logic -var validatedResults = BusinessRuleEngine.ValidateAndProcess(processedData); + // Apply the calibration offset to convert raw counts to physical units + var corrected = reading + calibration.Offset; -// Return formatted results matching the expected output contract -return OutputFormatter.Format(validatedResults); + // Clamp to the operational range so consumers can rely on the documented contract + return Math.Clamp(corrected, calibration.Minimum, calibration.Maximum); +} ``` +Key qualities demonstrated above: + +- **``** is a brief one-liner explaining *what* the method does +- **``** sits directly after summary and carries the extended intent - + *why* it exists, design decisions, thread-safety, and side-effect disclosures +- **`` tags** state constraints (finite, non-null) so callers know what + is valid without reading the body +- **``** documents the boundary guarantee so consumers can rely on the + contract +- **`` tags** name every thrown exception and the condition that + triggers each one +- **Inline block comments** follow the Literate Coding principles from + `coding-principles.md`, separating logical steps so reviewers can verify each + step against design intent + # Code Formatting - **Format entire solution**: `dotnet format` diff --git a/.github/standards/csharp-testing.md b/.github/standards/csharp-testing.md index 1591eeb..181de02 100644 --- a/.github/standards/csharp-testing.md +++ b/.github/standards/csharp-testing.md @@ -4,115 +4,74 @@ description: Follow these standards when developing C# tests. globs: ["**/test/**/*.cs", "**/tests/**/*.cs", "**/*Tests.cs", "**/*Test.cs"] --- -# C# Testing Standards (MSTest) - -This document defines standards for C# test development using -MSTest within Continuous Compliance environments. - -## Required Standards +# Required Standards Read these standards first before applying this standard: - **`testing-principles.md`** - Universal testing principles and dependency boundaries - **`csharp-language.md`** - C# language development standards -# C# AAA Pattern Implementation +# Package Reference -```csharp -[TestMethod] -public void ServiceName_MethodName_Scenario_ExpectedBehavior() -{ - // Arrange: description of setup (omit if nothing to set up) +Every xUnit v3 test project requires the following package references for +`dotnet test` to discover and execute tests: - // Act: description of action (can combine with Assert when action occurs within assertion) +| Package | Purpose | +| ------- | ------- | +| `xunit.v3` | xUnit v3 framework (monolithic - includes assertions and fixtures) | +| `Microsoft.NET.Test.Sdk` | Required by the VSTest/`dotnet test` host for test discovery | +| `xunit.runner.visualstudio` | VSTest adapter that bridges xUnit v3 to `dotnet test` | - // Assert: description of verification -} -``` +Omitting `Microsoft.NET.Test.Sdk` or `xunit.runner.visualstudio` causes tests +to be silently undiscoverable by `dotnet test`. + +If tests require mocking of dependencies, add `NSubstitute` as a package +reference - it is recommended when mocking is needed but is not required for +every test project. -# Test Naming Standards +# Test Style -Use descriptive test names because test names appear in requirements traceability matrices and compliance reports. +Test names appear in requirements traceability matrices - use the hierarchical +naming pattern, and follow AAA with labeled comments: - **System tests**: `{SystemName}_{Functionality}_{Scenario}_{ExpectedBehavior}` - **Subsystem tests**: `{SubsystemName}_{Functionality}_{Scenario}_{ExpectedBehavior}` - **Unit tests**: `{ClassName}_{MethodUnderTest}_{Scenario}_{ExpectedBehavior}` -- **Descriptive Scenarios**: Clearly describe the input condition being tested -- **Expected Behavior**: State the expected outcome or exception - -## Examples - -- `UserValidator_ValidateEmail_ValidFormat_ReturnsTrue` -- `UserValidator_ValidateEmail_InvalidFormat_ThrowsArgumentException` -- `PaymentProcessor_ProcessPayment_InsufficientFunds_ReturnsFailureResult` - -# Mock Dependencies - -Mock external dependencies using NSubstitute (preferred) because tests must run in isolation to generate -reliable evidence. - -- **Isolate System Under Test**: Mock all external dependencies (databases, web services, file systems) -- **Verify Interactions**: Assert that expected method calls occurred with correct parameters -- **Predictable Behavior**: Set up mocks to return known values for consistent test results - -# MSTest V4 Anti-patterns - -Avoid these common MSTest V4 patterns because they produce poor error messages or cause tests to be silently ignored. - -# Avoid Assertions in Catch Blocks (MSTEST0058) - -Instead of wrapping code in try/catch and asserting in the catch block, use `Assert.ThrowsExactly()`: - -```csharp -var ex = Assert.ThrowsExactly(() => SomeWork()); -Assert.Contains("Some message", ex.Message); -``` - -# Avoid Assert.IsTrue/IsFalse for Equality Checks - -Use `Assert.AreEqual`/`Assert.AreNotEqual` instead, as they provide better failure messages: - -```csharp -// ❌ Bad: Assert.IsTrue(result == expected); -// ✅ Good: Assert.AreEqual(expected, result); -``` - -# Avoid Non-Public Test Classes and Methods - -Test classes and `[TestMethod]` methods must be `public` or they will be silently ignored: ```csharp -// ❌ Bad: internal class MyTests -// ✅ Good: public class MyTests -``` - -# Avoid Assert.IsTrue for Collection Count - -Use `Assert.HasCount` for count assertions: +/// +/// Validates that an invalid email format throws an ArgumentException. +/// +[Fact] +public void UserValidator_ValidateEmail_InvalidFormat_ThrowsArgumentException() +{ + // Arrange: create a validator with default configuration + var validator = new UserValidator(); -```csharp -// ❌ Bad: Assert.IsTrue(collection.Count == 3); -// ✅ Good: Assert.HasCount(3, collection); + // Act / Assert: email with no domain throws + Assert.Throws(() => validator.ValidateEmail("not-an-email")); +} ``` -# Avoid Assert.IsTrue for String Prefix Checks +# xUnit v3 Specifics -Use `Assert.StartsWith` instead, as it produces clearer failure messages: +These are non-obvious v3 behaviors that differ from v2 or common assumptions: -```csharp -// ❌ Bad: Assert.IsTrue(value.StartsWith("prefix")); -// ✅ Good: Assert.StartsWith("prefix", value); -``` +- **`IAsyncLifetime`**: Both `InitializeAsync` and `DisposeAsync` return `ValueTask` + in v3, not `Task` - using `Task` compiles but does not satisfy the v3 interface +- **`Assert.Multiple`**: Use to collect all assertion failures in a single test + rather than stopping at the first +- **`[Collection]` without `[CollectionDefinition]`**: Silently disables parallelism + without providing any shared fixture - always pair them or remove `[Collection]` # Quality Checks Before submitting C# tests, verify: - [ ] All tests follow AAA pattern with clear section comments -- [ ] Test names follow hierarchical patterns defined in Test Naming Standards section -- [ ] Each test verifies single, specific behavior (no shared state) +- [ ] Test names follow hierarchical naming pattern above +- [ ] Each test verifies single, specific behavior (no shared state between tests) - [ ] Both success and failure scenarios covered including edge cases -- [ ] External dependencies mocked with NSubstitute or equivalent +- [ ] External dependencies mocked with NSubstitute (when mocking is needed) - [ ] Tests linked to requirements with source filters where needed -- [ ] Test results generate TRX format for ReqStream compatibility -- [ ] MSTest V4 anti-patterns avoided (proper assertions, public visibility, etc.) +- [ ] Test results generated in TRX format for ReqStream compatibility (`dotnet test --logger trx`) diff --git a/.github/standards/design-documentation.md b/.github/standards/design-documentation.md index 30becb5..768bf3f 100644 --- a/.github/standards/design-documentation.md +++ b/.github/standards/design-documentation.md @@ -35,16 +35,21 @@ design to implementation: ```text docs/design/ -├── introduction.md # Design overview with software structure -└── {system-name}/ # System-level design folder (one per system) - ├── {system-name}.md # System-level design documentation - ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively - │ ├── {subsystem-name}.md # Subsystem overview and design - │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) - │ └── {unit-name}.md # Unit-level design documents - └── {unit-name}.md # Top-level unit design documents (if not in subsystem) +├── introduction.md # Document overview - heading depth # +├── {system-name}.md # System-level design - heading depth # +└── {system-name}/ # System folder (one per system) + ├── {subsystem-name}.md # Subsystem overview - heading depth ## + ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively + │ ├── {child-subsystem}.md # Child subsystem overview - heading depth ### + │ ├── {child-subsystem}/ # Child subsystem folder (same structure as parent) + │ └── {unit-name}.md # Unit design - heading depth ### + └── {unit-name}.md # System-level unit design - heading depth ## ``` +Each scope's overview file lives in its **parent** folder, not inside the scope's own +subfolder - this aligns heading depth with folder depth so the compiled PDF has a +meaningful multi-level outline (see Heading Depth Rule in `technical-documentation.md`). + ## introduction.md (MANDATORY) The `introduction.md` file serves as the design entry point and MUST include @@ -108,6 +113,13 @@ src/Project2Name/ └── HelperClass.cs - Helper functions ``` +### References Section (RECOMMENDED) + +If the design references external documents (standards, specifications), include +a `## References` section in `introduction.md`. This is the **only** place in the +design document collection where a References section should appear - do not add +one to any other design file. + ### Companion Artifact Structure (RECOMMENDED) Include a brief note explaining that each software item has parallel artifacts @@ -117,22 +129,30 @@ artifact to all related files: Example format: ```text -Each software item in the structure above has corresponding artifacts in -parallel directory trees: - -- Requirements: `docs/reqstream/{system}/.../{item}.yaml` (kebab-case) -- Design docs: `docs/design/{system}/.../{item}.md` (kebab-case) -- Source code: `src/{System}/.../{Item}.{ext}` (cased per language - see `software-items.md`) -- Tests: `test/{System}.Tests/.../{Item}Tests.{ext}` (cased per language - see `software-items.md`) -- Review-sets: defined in `.reviewmark.yaml` +Each in-house software item has corresponding artifacts in parallel directory trees: + +- Requirements: `docs/reqstream/{system-name}.yaml`, `docs/reqstream/{system-name}/.../{item}.yaml` +- Design docs: `docs/design/{system-name}.md`, `docs/design/{system-name}/.../{item}.md` +- Verification: `docs/verification/{system-name}.md`, `docs/verification/{system-name}/.../{item}.md` +- Source code: `src/{SystemName}/.../{Item}.{ext}` (cased per language - see `software-items.md`) +- Tests: `test/{SystemName}.Tests/.../{Item}Tests.{ext}` (cased per language) + +OTS items have no design documentation; their artifacts sit parallel to system folders: + +- Requirements: `docs/reqstream/ots/{ots-name}.yaml` +- Verification: `docs/verification/ots/{ots-name}.md` +- Tests (optional): `test/{OtsSoftwareTests}/...` (cased per language - see `software-items.md`) + +Review-sets: defined in `.reviewmark.yaml` ``` ## System Design Documentation (MANDATORY) For each system identified in the repository: -- Create a kebab-case folder matching the system name -- Include `{system-name}.md` with system-level design documentation such as: +- Create `{system-name}.md` directly under `docs/design/` (heading depth `#`) +- Create a kebab-case folder `{system-name}/` to hold its subsystems and units +- `{system-name}.md` must cover: - System architecture and major components - External interfaces and dependencies - Data flow and control flow @@ -143,16 +163,20 @@ For each system identified in the repository: For each subsystem identified in the software structure: -- Create a kebab-case folder matching the subsystem name (enables automated tooling) -- Include `{subsystem-name}.md` with subsystem overview and design -- Include unit design documents for ALL units within the subsystem +- Place `{subsystem-name}.md` inside the **parent** folder (the system folder, or parent + subsystem folder) - not inside its own subfolder +- Create a kebab-case folder `{subsystem-name}/` to hold its child units and subsystems +- `{subsystem-name}.md` must cover subsystem overview and design For every unit identified in the software structure: +- Place `{unit-name}.md` inside its parent scope's folder (system or subsystem folder) - Document data models, algorithms, and key methods - Describe interactions with other units - Include sufficient detail for formal code review -- Place in appropriate subsystem folder or at design root level + +Follow the Heading Depth Rule from `technical-documentation.md` - a file's top-level +heading depth equals its folder depth under `docs/design/`. # Software Items Integration (CRITICAL) @@ -168,6 +192,9 @@ implementation specification for formal code review: - **Implementation Detail**: Provide sufficient detail for code review and implementation - **Architectural Clarity**: Clearly define component boundaries and interfaces - **Traceability**: Link to requirements where applicable using ReqStream patterns +- **Verbal Cross-References**: Reference other parts of the design by name (e.g., + "See *Parser Design* for more details") - do not use markdown hyperlinks, which + break in compiled PDFs # Mermaid Diagram Integration @@ -180,9 +207,11 @@ Before submitting design documentation, verify: - [ ] `introduction.md` includes both Software Structure and Folder Layout sections - [ ] Software structure correctly categorizes items as System/Subsystem/Unit per `software-items.md` - [ ] Folder layout mirrors software structure organization +- [ ] Files organized under `docs/design/` following the folder structure pattern above +- [ ] Each file's top-level heading depth matches its folder depth per the Heading Depth Rule - [ ] Design documents provide sufficient detail for code review - [ ] System documentation provides comprehensive system-level design -- [ ] Subsystem documentation folders use kebab-case names while mirroring source subsystem names and structure +- [ ] All documentation folders use kebab-case names mirroring source code structure - [ ] All documents follow technical documentation formatting standards - [ ] Content is current with implementation and requirements - [ ] Documents are integrated into ReviewMark review-sets for formal review diff --git a/.github/standards/reqstream-usage.md b/.github/standards/reqstream-usage.md index ae5e565..303bb43 100644 --- a/.github/standards/reqstream-usage.md +++ b/.github/standards/reqstream-usage.md @@ -18,20 +18,25 @@ because ReqStream discovers files via the includes chain in `requirements.yaml` and organizes report output by this hierarchy: ```text -requirements.yaml # Root file (includes only) +requirements.yaml # Root file (includes only) docs/reqstream/ -├── {system-name}/ # System-level requirements folder (one per system) -│ ├── {system-name}.yaml # System-level requirements +├── {system-name}.yaml # System-level requirements +├── {system-name}/ # System folder (one per system) │ ├── platform-requirements.yaml # Platform support requirements -│ ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively -│ │ ├── {subsystem-name}.yaml # Requirements for this subsystem -│ │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) -│ │ └── {unit-name}.yaml # Requirements for units within this subsystem -│ └── {unit-name}.yaml # Requirements for top-level units (outside subsystems) -└── ots/ # OTS items appear as a distinct section in reports - └── {ots-name}.yaml # Requirements for OTS components +│ ├── {subsystem-name}.yaml # Subsystem requirements +│ ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively +│ │ ├── {child-subsystem}.yaml # Child subsystem requirements +│ │ ├── {child-subsystem}/ # Child subsystem folder +│ │ └── {unit-name}.yaml # Unit requirements +│ └── {unit-name}.yaml # System-level unit requirements +└── ots/ # OTS items appear as a distinct section in reports + └── {ots-name}.yaml # Requirements for OTS components ``` +In-house items have matching relative paths across `docs/reqstream/`, `docs/design/`, and +`docs/verification/`. OTS items appear only in `docs/reqstream/ots/` and +`docs/verification/ots/` - they have no design documentation. + # Requirements File Format ```yaml @@ -62,7 +67,7 @@ sections: sections: - title: System.Text.Json requirements: - - id: TemplateTool-SystemTextJson-ReadJson + - id: SystemTextJson-Core-ReadJson title: System.Text.Json shall be able to read JSON files. tests: - JsonReaderTests.TestReadValidJson @@ -104,16 +109,16 @@ dotnet reqstream --requirements requirements.yaml --lint # Generate requirements document for compliance record dotnet reqstream --requirements requirements.yaml \ - --report docs/requirements_doc/requirements.md + --report docs/requirements_doc/generated/requirements.md # Generate justifications document for compliance record dotnet reqstream --requirements requirements.yaml \ - --justifications docs/requirements_doc/justifications.md + --justifications docs/requirements_doc/generated/justifications.md # Generate trace matrix proving each requirement is covered by passing tests dotnet reqstream --requirements requirements.yaml \ --tests "artifacts/**/*.trx" \ - --matrix docs/requirements_report/trace_matrix.md + --matrix docs/requirements_report/generated/trace_matrix.md ``` # Quality Checks @@ -124,8 +129,8 @@ Before submitting requirements, verify: - [ ] Every requirement links to at least one passing test - [ ] Platform-specific requirements use source filters (`platform@TestName`) - [ ] Comprehensive justification explains business/regulatory need -- [ ] Files organized under `docs/reqstream/` following folder structure patterns -- [ ] Subsystem folders use kebab-case naming matching source code +- [ ] Files organized under `docs/reqstream/` following the folder structure pattern above +- [ ] All documentation folders use kebab-case names matching source code structure - [ ] OTS requirements placed in `ots/` subfolder - [ ] Valid YAML syntax passes yamllint validation - [ ] Test result formats compatible (TRX, JUnit XML) diff --git a/.github/standards/requirements-principles.md b/.github/standards/requirements-principles.md index 7d2d572..b6cf136 100644 --- a/.github/standards/requirements-principles.md +++ b/.github/standards/requirements-principles.md @@ -29,6 +29,10 @@ implementation code. - **Valid**: "The parser shall report the line number of the first syntax error." - **Not a requirement (design decision)**: "The parser shall use a `TokenStream` class." +A unit may use its own name freely - that is identity, not HOW. What is +forbidden is describing *internal construction*: class names, method signatures, +algorithms, or data structures. + # Requirements at Every Level (MANDATORY) Every identified subsystem and unit MUST have its own requirements file because diff --git a/.github/standards/reviewmark-usage.md b/.github/standards/reviewmark-usage.md index 5d6219e..2d95832 100644 --- a/.github/standards/reviewmark-usage.md +++ b/.github/standards/reviewmark-usage.md @@ -20,7 +20,7 @@ review, organizes them into review-sets, and generates review plans and reports. - **Lint Configuration**: `dotnet reviewmark --lint` - **Elaborate Review-Set**: `dotnet reviewmark --elaborate {review-set}` -- **Generate Plan**: `dotnet reviewmark --plan docs/code_review_plan/plan.md --enforce` +- **Generate Plan**: `dotnet reviewmark --plan docs/code_review_plan/generated/plan.md --enforce` > **Note**: `--enforce` causes the plan to fail with a non-zero exit code if any repository > files are not covered by a review-set. Uncovered files indicate a gap in review-set @@ -31,7 +31,8 @@ review, organizes them into review-sets, and generates review plans and reports. Required repository items for ReviewMark operation: - `.reviewmark.yaml` - Configuration for review-sets, file-patterns, and review evidence-source. -- `docs/code_review_plan/` - Review planning artifacts +- `docs/code_review_plan/generated/` - Generated review plan (build output, do not edit) +- `docs/code_review_report/generated/` - Generated review report (build output, do not edit) # Review Definition Structure @@ -55,10 +56,22 @@ needs-review: - "README.md" # Root level README - "docs/user_guide/**/*.md" # User guide - "docs/design/**/*.md" # Design documentation + - "docs/verification/**/*.md" # Verification design documentation # Source of review evidence evidence-source: type: none + +# Review-sets (each focuses on a single compliance question) +reviews: + - id: Purpose + title: Review of user-facing capabilities and system promises + paths: + - "README.md" + - "docs/user_guide/**/*.md" + - "docs/reqstream/{system-name}.yaml" + - "docs/design/introduction.md" + - "docs/design/{system-name}.md" ``` # Review-Set Design Principles @@ -93,9 +106,9 @@ Reviews user-facing capabilities and system promises: - **File Path Patterns**: - README: `README.md` - User guide: `docs/user_guide/**/*.md` - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - - System design: `docs/design/{system-name}/{system-name}.md` + - System design: `docs/design/{system-name}.md` ## `{System}-Architecture` Review (one per system) @@ -106,9 +119,11 @@ Reviews system architecture and operational validation: - **Scope**: Excludes subsystem and unit files, relying on system-level design to describe what subsystems and units it uses - **File Path Patterns**: - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - - System design: `docs/design/{system-name}/{system-name}.md` + - System design: `docs/design/{system-name}.md` + - Verification introduction: `docs/verification/introduction.md` + - System verification design: `docs/verification/{system-name}.md` - System integration tests: `test/{SystemName}.Tests/{SystemName}Tests.{ext}` ## `{System}-Design` Review (one per system) @@ -119,9 +134,10 @@ Reviews architectural and design consistency: - **Title**: "Review that {System} Design is Consistent and Complete" - **Scope**: Only brings in top-level requirements and relies on brevity of design documentation - **File Path Patterns**: - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Platform requirements: `docs/reqstream/{system-name}/platform-requirements.yaml` - Design introduction: `docs/design/introduction.md` + - System design: `docs/design/{system-name}.md` - System design files: `docs/design/{system-name}/**/*.md` ## `{System}-AllRequirements` Review (one per system) @@ -133,8 +149,8 @@ Reviews requirements quality and traceability: - **Scope**: Only brings in requirements files to keep review manageable - **File Path Patterns**: - Root requirements: `requirements.yaml` - - System requirements: `docs/reqstream/{system-name}/**/*.yaml` - - OTS requirements: `docs/reqstream/ots/**/*.yaml` (if applicable) + - System requirements: `docs/reqstream/{system-name}.yaml` + - Subsystem/unit requirements: `docs/reqstream/{system-name}/**/*.yaml` ## `{System}-{Subsystem[-Child...]}` Review (one per subsystem at any depth) @@ -145,8 +161,9 @@ Reviews subsystem architecture and interfaces: - **Scope**: Excludes units under the subsystem, relying on subsystem design to describe what units it uses - **File Path Patterns**: - - Requirements: `docs/reqstream/{system-name}/.../{subsystem-name}/{subsystem-name}.yaml` - - Design: `docs/design/{system-name}/.../{subsystem-name}/{subsystem-name}.md` + - Requirements: `docs/reqstream/{system-name}/.../{subsystem-name}.yaml` + - Design: `docs/design/{system-name}/.../{subsystem-name}.md` + - Verification design: `docs/verification/{system-name}/.../{subsystem-name}.md` - Tests: `test/{SystemName}.Tests/.../{SubsystemName}/{SubsystemName}Tests.{ext}` ## `{System}-{Subsystem[-Child...]}-{Unit}` Review (one per unit) @@ -159,9 +176,23 @@ Reviews individual software unit implementation: - **File Path Patterns**: - Requirements: `docs/reqstream/{system-name}/.../{unit-name}.yaml` - Design: `docs/design/{system-name}/.../{unit-name}.md` + - Verification design: `docs/verification/{system-name}/.../{unit-name}.md` - Source: `src/{SystemName}/.../{UnitName}.{ext}` - Tests: `test/{SystemName}.Tests/.../{UnitName}Tests.{ext}` +## `OTS-{OtsName}` Review (one per OTS item) + +Reviews OTS item requirements and verification evidence: + +- **Purpose**: Proves that the OTS item provides the required functionality +- **Title**: "Review that {OtsName} Provides Required Functionality" +- **Scope**: OTS items have no in-house design or source; review covers requirements and + verification evidence only +- **File Path Patterns**: + - OTS requirements: `docs/reqstream/ots/{ots-name}.yaml` + - OTS verification: `docs/verification/ots/{ots-name}.md` + - Tests (if applicable): `test/{OtsSoftwareTests}/...` (cased per language) + **Note**: File path patterns use `{ext}` as a placeholder for language-specific extensions (`.cs`, `.cpp`/`.hpp`, `.py`, etc.). Adapt to your repository's languages. @@ -175,6 +206,10 @@ Before submitting ReviewMark configuration, verify: - [ ] System-level reviews follow hierarchical scope principle (exclude subsystem/unit details) - [ ] Subsystem reviews follow hierarchical scope principle (exclude unit source code) - [ ] Only unit reviews include actual source code files +- [ ] Architecture review-sets include system verification design alongside system design +- [ ] Subsystem review-sets include subsystem verification design +- [ ] Unit review-sets include unit verification design +- [ ] OTS review-sets include OTS requirements and verification evidence - [ ] Each review-set focuses on a single compliance question (single focus principle) - [ ] File patterns use correct glob syntax and match intended files - [ ] Review-set file counts remain manageable (context management principle) diff --git a/.github/standards/software-items.md b/.github/standards/software-items.md index bb67b1d..6be029f 100644 --- a/.github/standards/software-items.md +++ b/.github/standards/software-items.md @@ -81,14 +81,23 @@ Choose the appropriate category based on scope and testability: consumes it - Tested through integration tests proving required functionality works - Examples: System.Text.Json, Entity Framework, third-party APIs +- **Artifact locations** (OTS items have no design documentation): + - Requirements: `docs/reqstream/ots/{ots-name}.yaml` + - Verification: `docs/verification/ots/{ots-name}.md` + - These folders sit parallel to system folders (not inside any system folder) +- System design documentation records which OTS items each system depends on +- **OTS test project**: If no other verification evidence is available (e.g., vendor test results, + published compliance reports), a dedicated test project (`OtsSoftwareTests` / `ots_software_tests`, + cased per language) holds OTS integration tests - one test file per OTS item requiring tests. # Software Item Artifact Model -Each software item has four artifact types that together form a complete review +Each software item has five artifact types that together form a complete review unit - because reviewing any one artifact in isolation cannot determine whether the item is correct, well-designed, and proven to work: - **Requirements** - WHAT the item must do (drives all other artifacts; applies to all item types) - **Design** - HOW the item satisfies its requirements (in-house items only: system, subsystem, unit) +- **Verification Design** - HOW the requirements will be tested (applies to all item types) - **Source code** - The implementation of the design (in-house units only) - **Tests** - PROOF the item does WHAT it is required to do (applies to all item types) diff --git a/.github/standards/technical-documentation.md b/.github/standards/technical-documentation.md index 455b2fd..2ac29f4 100644 --- a/.github/standards/technical-documentation.md +++ b/.github/standards/technical-documentation.md @@ -1,7 +1,7 @@ --- name: Technical Documentation description: Follow these standards when creating technical documentation. -globs: ["docs/**/*.md", "README.md"] +globs: ["docs/**/*.md", "README.md", "!docs/**/generated/**"] --- # Technical Documentation Standards @@ -23,63 +23,25 @@ for regulatory review: - **Review Integration**: Documentation follows ReviewMark patterns for formal review tracking -# Documentation Organization +# Pandoc Document Structure (MANDATORY) -Structure documentation under `docs/` following standard patterns for -consistency and tool compatibility: +Each document collection under `docs/` follows this layout: ```text -docs/ - build_notes.md # Generated by BuildMark - build_notes/ # Auto-generated build notes - versions.md # Generated by VersionMark - code_review_plan/ # Auto-generated review plans - plan.md # Generated by ReviewMark - code_review_report/ # Auto-generated review reports - report.md # Generated by ReviewMark - design/ # Design documentation - introduction.md # Design overview - {system-name}/ # System architecture folder - {system-name}.md # System architecture - {subsystem-name}/ # Subsystem folder; may nest recursively - {subsystem-name}.md # Subsystem-specific designs - {child-subsystem}/ # Child subsystem (same structure) - {unit-name}.md # Unit-specific designs - {unit-name}.md # Top-level unit design - reqstream/ # Requirements source files - {system-name}/ # System requirements folder - {system-name}.yaml # System requirements - platform-requirements.yaml # Platform requirements - {subsystem-name}/ # Subsystem folder; may nest recursively - {subsystem-name}.yaml # Subsystem requirements - {child-subsystem}/ # Child subsystem (same structure) - {unit-name}.yaml # Unit-specific requirements - {unit-name}.yaml # Top-level unit requirements - ots/ # OTS requirement files - {ots-name}.yaml # OTS requirements - requirements_doc/ # Auto-generated requirements reports - requirements.md # Generated by ReqStream - justifications.md # Generated by ReqStream - requirements_report/ # Auto-generated trace matrices - trace_matrix.md # Generated by ReqStream - user_guide/ # User-facing documentation - introduction.md # User guide overview - {section}.md # User guide sections +docs/{collection}/ + title.txt # MANDATORY - YAML document metadata (title, author, etc.) + definition.yaml # MANDATORY - Pandoc build definition (inputs, template, paths) + introduction.md # MANDATORY - document introduction (Purpose, Scope, References) + {section}.md # optional checked-in content sections (zero or more) + generated/ # BUILD OUTPUT - never read, edit, or lint these files + {report}.md # generated by CI tools (ReqStream, ReviewMark, SarifMark, etc.) + {collection}.html # generated by Pandoc ``` -# Pandoc Document Structure (MANDATORY) - -All document collections processed by Pandoc MUST include all four files below - -without `title.txt` and `definition.yaml` the pipeline cannot generate the document: - -- `title.txt` - YAML metadata (title, subtitle, author, description, lang, keywords) -- `definition.yaml` - Pandoc build definition (resource paths, input file list, template) -- `introduction.md` - document introduction -- `{sections}.md` - additional content sections - -When creating a new document collection, create `title.txt` and `definition.yaml` -alongside `introduction.md`. Use the existing files under `docs/` as templates - -they share a consistent structure across all collections. +Without `title.txt` and `definition.yaml` the pipeline cannot generate the document. +When creating a new document collection, create these three files together and use +the existing collections under `docs/` as templates - they share a consistent +structure across all collections. **`title.txt`** - YAML front matter with document metadata. Use the existing files under `docs/` as a pattern and keep fields consistent with the rest of @@ -106,13 +68,39 @@ Include regulatory or business drivers where applicable. Define what is covered and what is explicitly excluded from this documentation. Specify version, system boundaries, and applicability constraints. + +## References + +- [REF-1] Document Title, Author, Version, Date +- [REF-2] Standard Name (e.g., IEEE 12207, ISO 9001) ``` +The `Purpose`, `Scope`, and `References` sections are **unique to `introduction.md`** and must +**not** be replicated in other markdown files within the same document collection. Including them +elsewhere causes duplicate sections in the compiled PDF. + ## Document Ordering List documents in logical reading order in Pandoc configuration because readers need coherent information flow from general to specific topics. +## Heading Depth Rule (MANDATORY) + +A file's top-level heading depth must equal its folder depth under the document +collection root - this ensures Pandoc can concatenate all files in `definition.yaml` +order and produce a coherent outline with no heading-shift configuration: + +| Folder depth | Top heading | +| --- | --- | +| 0 - collection root | `#` | +| 1 - one subfolder deep | `##` | +| 2 - two subfolders deep | `###` | +| N - N subfolders deep | `#` × (N+1) | + +Internal sections use the next heading level down (e.g. a `##` file uses `###` +for *Overview*, *Interfaces*, etc.). Deeply nested files have fewer heading levels +available - keep internal structure flat to avoid excessive nesting. + # Writing Guidelines Write technical documentation for clarity and compliance verification: @@ -135,6 +123,19 @@ References in design/technical documents must point to **external specifications - **INCLUDE**: Requirements documents, system specifications, program documents, standards (IEEE, ISO, etc.) - **NEVER INCLUDE**: Internal development standards (`.github/standards/` files) - these are agent guides +## Cross-References (Within-Document and Cross-Document) + +Do **not** use markdown hyperlinks to reference other sections or documents. Markdown anchor links +(`[text](#heading)`) and relative file links work in a browser but break when compiled to a PDF. + +Instead use **verbal references** - plain prose that identifies the target by name: + +> See *XYZ Design* for more details. +> +> Refer to the *System Requirements* document for the full specification. + +Verbal references are readable by both AI agents and humans in any rendering environment. + # Markdown Format Requirements Markdown documentation in this repository must follow the formatting standards @@ -156,14 +157,13 @@ for consistency and professional presentation: # Auto-Generated Content (CRITICAL) -**NEVER modify auto-generated markdown files** because changes will be -overwritten and break compliance automation: +**NEVER read, lint, or modify files inside any `generated/` folder** - they are +build outputs that are overwritten on every CI run: -- **Read-Only Files**: Generated reports under `docs/requirements_doc/`, - `docs/requirements_report/`, `docs/code_review_plan/`, and - `docs/code_review_report/` are regenerated on every build -- **Source Modification**: Update source files (requirements YAML, code - comments) instead of generated output +- **Location**: All generated files live in `generated/` subfolders within their + respective `docs/` sections, or in `docs/generated/` for final release artifacts +- **Source Modification**: Update source files (requirements YAML, `.reviewmark.yaml`, + tool configuration) instead of generated output - **Tool Integration**: Generated content integrates with CI/CD pipelines and manual changes disrupt automation diff --git a/.github/standards/verification-documentation.md b/.github/standards/verification-documentation.md new file mode 100644 index 0000000..8eea3b7 --- /dev/null +++ b/.github/standards/verification-documentation.md @@ -0,0 +1,144 @@ +--- +name: Verification Documentation +description: Follow these standards when creating software verification design documentation. +globs: ["docs/verification/**/*.md"] +--- + +# Required Standards + +Read these standards first before applying this standard: + +- **`technical-documentation.md`** - General technical documentation standards +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) + +# Core Principles + +Verification design is the bridge between requirements and tests - it documents HOW +requirements will be verified, enabling reviewers to confirm test completeness without +reading implementation code. + +# Required Structure and Documents + +Organize under `docs/verification/` mirroring the software item hierarchy: + +```text +docs/verification/ +├── introduction.md # Document overview - heading depth # +├── {system-name}.md # System-level verification - heading depth # +├── {system-name}/ # System folder (one per system) +│ ├── {subsystem-name}.md # Subsystem verification - heading depth ## +│ ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively +│ │ ├── {child-subsystem}.md # Child subsystem verification - heading depth ### +│ │ ├── {child-subsystem}/ # Child subsystem folder (same structure as parent) +│ │ └── {unit-name}.md # Unit verification - heading depth ### +│ └── {unit-name}.md # System-level unit verification - heading depth ## +├── ots.md # OTS section overview - heading depth # (MANDATORY if OTS items exist) +└── ots/ # OTS items - parallel to system folders (not inside them) + └── {ots-name}.md # OTS item verification evidence - heading depth ## +``` + +Each scope's overview file lives in its **parent** folder, not inside the scope's own +subfolder - this keeps artifact locations consistent with design and requirements trees +so any item's files are deterministically locatable, and aligns heading depth with folder +depth for correct PDF structure (see Heading Depth Rule in `technical-documentation.md`). + +## introduction.md (MANDATORY) + +Follow the standard `introduction.md` format from `technical-documentation.md`. Scope +covers all software items including OTS items (via self-validation if appropriate). + +Include a Companion Artifact Structure note so agents and reviewers can navigate from any +artifact to all related files: + +```text +In-house items have parallel artifacts in: +- Requirements: `docs/reqstream/{system-name}.yaml`, `docs/reqstream/{system-name}/.../{item}.yaml` +- Design: `docs/design/{system-name}.md`, `docs/design/{system-name}/.../{item}.md` +- Verification: `docs/verification/{system-name}.md`, `docs/verification/{system-name}/.../{item}.md` +- Source: `src/{SystemName}/.../{Item}.{ext}` (cased per language) +- Tests: `test/{SystemName}.Tests/.../{Item}Tests.{ext}` (cased per language) + +OTS items (no design documentation) have artifacts parallel to system folders: +- Requirements: `docs/reqstream/ots/{ots-name}.yaml` +- Verification: `docs/verification/ots/{ots-name}.md` +- Tests (if required): `test/{OtsSoftwareTests}/...` (cased per language - see `software-items.md`) + +Review-sets: defined in `.reviewmark.yaml` +``` + +If the verification design references external documents (standards, specifications), include +a `## References` section in `introduction.md` only - do not add one to any other verification file. + +## System Verification Design (MANDATORY) + +For each system, create `{system-name}.md` at `docs/verification/` root and a +`{system-name}/` folder for subsystems. Cover: + +- System verification strategy and overall test approach +- Test environments and configuration required +- External interface simulation and test-harness design +- End-to-end and integration test scenarios covering system requirements +- Acceptance criteria and pass/fail conditions at the system boundary +- Coverage mapping of system requirements to system-level test scenarios + +## Subsystem Verification Design (MANDATORY) + +For each subsystem, place `{subsystem-name}.md` in the parent (system or subsystem) +folder and create a `{subsystem-name}/` folder for its units. Cover: + +- Subsystem verification strategy and integration test approach +- Dependencies that must be mocked or stubbed at the subsystem boundary +- Integration test scenarios covering subsystem requirements +- Coverage mapping of subsystem requirements to subsystem-level test scenarios + +## Unit Verification Design (MANDATORY) + +Place `{unit-name}.md` in the parent (system or subsystem) folder. Cover: + +- Verification approach for each unit requirement +- Named test scenarios including boundary conditions, error paths, and normal-operation cases +- Which dependencies are mocked and how they are configured +- Coverage mapping of every unit requirement to at least one named test scenario + +## OTS Verification Evidence (when OTS items are used) + +Create `docs/verification/ots.md` at the collection root with a `#` top-level heading. This +file introduces the OTS verification approach and ensures OTS items compile as a top-level +section in the PDF rather than as subsystems of the last in-house system. + +For each OTS item, create `docs/verification/ots/{ots-name}.md` covering: + +- The OTS item's required functionality (reference `docs/reqstream/ots/{ots-name}.yaml`) +- Verification of each requirement (using self-validation evidence if appropriate) +- Coverage mapping of OTS requirements to test scenarios + +# Writing Guidelines + +- **Test Coverage**: Map every requirement to at least one named test scenario so + reviewers can verify completeness without reading test code +- **Scenario Clarity**: Name each scenario clearly - "Valid input returns parsed result" not "Test 1" +- **Boundary Conditions**: Call out boundary values, error inputs, and edge cases explicitly +- **Isolation Strategy**: Describe what is mocked or stubbed and why at each level +- **Traceability**: Link to requirements where applicable using ReqStream patterns +- **Verbal Cross-References**: Reference other documents by name - do not use markdown + hyperlinks, which break in compiled PDFs + +Mermaid diagrams may supplement text descriptions where test flow benefits from visual +representation, but must not replace text content. + +# Quality Checks + +Before submitting verification documentation, verify: + +- [ ] Every requirement at each level is mapped to at least one named test scenario +- [ ] System verification documents cover end-to-end and integration scenarios +- [ ] Subsystem verification documents identify mocked boundaries and integration scenarios +- [ ] Unit verification documents identify individual scenarios including boundary and error paths +- [ ] Files organized under `docs/verification/` following the folder structure pattern above +- [ ] Each file's top-level heading depth matches its folder depth per the Heading Depth Rule +- [ ] All documentation folders use kebab-case names mirroring source code structure +- [ ] All documents follow technical documentation formatting standards +- [ ] Content is current with requirements and test implementation +- [ ] Every OTS item has `docs/verification/ots/{ots-name}.md` with requirement coverage +- [ ] `docs/verification/ots.md` exists with a `#` heading when OTS items are present +- [ ] Documents are integrated into ReviewMark review-sets for formal review diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9b01d1f..60ed015 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -466,6 +466,15 @@ jobs: buildmark versionmark reviewmark fileassert echo "✓ Tool versions captured" + # === PREPARE DOCUMENT OUTPUT === + # Creates the shared docs/generated/ folder that all document sections write PDFs into. + # This step is intentionally separate from the document sections so any individual + # section can be commented out without breaking the shared output directory. + + - name: Create documents output directory + shell: bash + run: mkdir -p docs/generated + # === COMPILE BUILD NOTES === # This section generates the Build Notes document. BuildMark and VersionMark self-validations # run here to co-locate their evidence with the document that depends on their output. @@ -473,6 +482,10 @@ jobs: # validates the outputs contain expected content. # Downstream projects: Add any additional build notes steps here. + - name: Create build notes output directories + shell: bash + run: mkdir -p docs/build_notes/generated + - name: Run BuildMark self-validation run: > dotnet buildmark @@ -492,20 +505,20 @@ jobs: run: > dotnet buildmark --build-version ${{ inputs.version }} - --report docs/build_notes.md + --report docs/build_notes/generated/build_notes.md --report-depth 1 - name: Display Build Notes Report shell: bash run: | echo "=== Build Notes Report ===" - cat docs/build_notes.md + cat docs/build_notes/generated/build_notes.md - name: Publish Tool Versions shell: bash run: | echo "Publishing tool versions..." - dotnet versionmark --publish --report docs/build_notes/versions.md --report-depth 1 \ + dotnet versionmark --publish --report docs/build_notes/generated/versions.md --report-depth 1 \ -- "artifacts/**/versionmark-*.json" echo "✓ Tool versions published" @@ -513,7 +526,7 @@ jobs: shell: bash run: | echo "=== Tool Versions Report ===" - cat docs/build_notes/versions.md + cat docs/build_notes/generated/versions.md - name: Generate Build Notes HTML with Pandoc shell: bash @@ -523,14 +536,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/build_notes/build_notes.html + --output docs/build_notes/generated/build_notes.html - name: Generate Build Notes PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/build_notes/build_notes.html - "docs/VersionMark Build Notes.pdf" + docs/build_notes/generated/build_notes.html + "docs/generated/VersionMark Build Notes.pdf" - name: Assert Build Notes Documents with FileAssert run: > @@ -538,6 +551,10 @@ jobs: --results artifacts/fileassert-build-notes.trx build-notes + - name: Copy Build Notes report to docs/generated + shell: bash + run: cp docs/build_notes/generated/build_notes.md docs/generated/build_notes.md + # === COMPILE CODE QUALITY REPORT === # This section generates the Code Quality document. SarifMark and SonarMark self-validations # run here to co-locate their evidence with the document that depends on their output. @@ -545,6 +562,10 @@ jobs: # validates the outputs contain expected content. # Downstream projects: Add any additional code quality steps here. + - name: Create code quality output directory + shell: bash + run: mkdir -p docs/code_quality/generated + - name: Run SarifMark self-validation run: > dotnet sarifmark @@ -561,7 +582,7 @@ jobs: run: > dotnet sarifmark --sarif artifacts/csharp.sarif - --report docs/code_quality/codeql-quality.md + --report docs/code_quality/generated/codeql-quality.md --heading "VersionMark CodeQL Analysis" --report-depth 1 @@ -569,7 +590,7 @@ jobs: shell: bash run: | echo "=== CodeQL Quality Report ===" - cat docs/code_quality/codeql-quality.md + cat docs/code_quality/generated/codeql-quality.md - name: Generate SonarCloud Quality Report shell: bash @@ -581,14 +602,14 @@ jobs: --project-key demaconsulting_VersionMark --branch ${{ github.ref_name }} --token "$SONAR_TOKEN" - --report docs/code_quality/sonar-quality.md + --report docs/code_quality/generated/sonar-quality.md --report-depth 1 - name: Display SonarCloud Quality Report shell: bash run: | echo "=== SonarCloud Quality Report ===" - cat docs/code_quality/sonar-quality.md + cat docs/code_quality/generated/sonar-quality.md - name: Generate Code Quality HTML with Pandoc shell: bash @@ -598,14 +619,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/code_quality/quality.html + --output docs/code_quality/generated/quality.html - name: Generate Code Quality PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/code_quality/quality.html - "docs/VersionMark Code Quality.pdf" + docs/code_quality/generated/quality.html + "docs/generated/VersionMark Code Quality.pdf" - name: Assert Code Quality Documents with FileAssert run: > @@ -620,6 +641,10 @@ jobs: # PDF, and FileAssert validates the outputs contain expected content. # Downstream projects: Add any additional code review steps here. + - name: Create code review output directories + shell: bash + run: mkdir -p docs/code_review_plan/generated docs/code_review_report/generated + - name: Run ReviewMark self-validation run: > dotnet reviewmark @@ -631,22 +656,22 @@ jobs: # TODO: Add --enforce once reviews branch is populated with review evidence PDFs and index.json run: > dotnet reviewmark - --plan docs/code_review_plan/plan.md + --plan docs/code_review_plan/generated/plan.md --plan-depth 1 - --report docs/code_review_report/report.md + --report docs/code_review_report/generated/report.md --report-depth 1 - name: Display Review Plan shell: bash run: | echo "=== Review Plan ===" - cat docs/code_review_plan/plan.md + cat docs/code_review_plan/generated/plan.md - name: Display Review Report shell: bash run: | echo "=== Review Report ===" - cat docs/code_review_report/report.md + cat docs/code_review_report/generated/report.md - name: Generate Review Plan HTML with Pandoc shell: bash @@ -656,14 +681,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/code_review_plan/plan.html + --output docs/code_review_plan/generated/plan.html - name: Generate Review Plan PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/code_review_plan/plan.html - "docs/VersionMark Review Plan.pdf" + docs/code_review_plan/generated/plan.html + "docs/generated/VersionMark Review Plan.pdf" - name: Generate Review Report HTML with Pandoc shell: bash @@ -673,14 +698,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/code_review_report/report.html + --output docs/code_review_report/generated/report.html - name: Generate Review Report PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/code_review_report/report.html - "docs/VersionMark Review Report.pdf" + docs/code_review_report/generated/report.html + "docs/generated/VersionMark Review Report.pdf" - name: Assert Code Review Documents with FileAssert run: > @@ -693,6 +718,10 @@ jobs: # FileAssert validates that the HTML and PDF outputs contain expected content. # Downstream projects: Add any additional design document steps here. + - name: Create design output directory + shell: bash + run: mkdir -p docs/design/generated + - name: Generate Design HTML with Pandoc shell: bash run: > @@ -701,14 +730,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/design/design.html + --output docs/design/generated/design.html - name: Generate Design PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/design/design.html - "docs/VersionMark Software Design.pdf" + docs/design/generated/design.html + "docs/generated/VersionMark Software Design.pdf" - name: Assert Design Documents with FileAssert run: > @@ -716,11 +745,46 @@ jobs: --results artifacts/fileassert-design.trx design + # === COMPILE VERIFICATION DOCUMENT === + # This section generates the Verification document using Pandoc and WeasyPrint. + # FileAssert validates that the HTML and PDF outputs contain expected content. + # Downstream projects: Add any additional verification steps here. + + - name: Create verification output directory + shell: bash + run: mkdir -p docs/verification/generated + + - name: Generate Verification HTML with Pandoc + shell: bash + run: > + dotnet pandoc + --defaults docs/verification/definition.yaml + --metadata version="${{ inputs.version }}" + --metadata date="$(date +'%Y-%m-%d')" + --output docs/verification/generated/verification.html + + - name: Generate Verification PDF with WeasyPrint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/verification/generated/verification.html + "docs/generated/VersionMark Software Verification Design.pdf" + + - name: Assert Verification Documents with FileAssert + run: > + dotnet fileassert + --results artifacts/fileassert-verification.trx + verification + # === COMPILE USER GUIDE === # This section generates the User Guide document using Pandoc and WeasyPrint. # FileAssert validates that the HTML and PDF outputs contain expected content. # Downstream projects: Add any additional user guide steps here. + - name: Create user guide output directory + shell: bash + run: mkdir -p docs/user_guide/generated + - name: Generate User Guide HTML with Pandoc shell: bash run: > @@ -729,14 +793,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/user_guide/user_guide.html + --output docs/user_guide/generated/user_guide.html - name: Generate User Guide PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/user_guide/user_guide.html - "docs/VersionMark User Guide.pdf" + docs/user_guide/generated/user_guide.html + "docs/generated/VersionMark User Guide.pdf" - name: Assert User Guide Documents with FileAssert run: > @@ -745,8 +809,8 @@ jobs: user-guide # === FILEASSERT SELF-VALIDATION === - # By this point Pandoc and WeasyPrint have each produced 6 validated documents - # (Build Notes, Code Quality, Review Plan, Review Report, Design, User Guide), + # By this point Pandoc and WeasyPrint have each produced 7 validated documents + # (Build Notes, Code Quality, Review Plan, Review Report, Design, Verification, User Guide), # providing strong OTS evidence for both tools before ReqStream runs. FileAssert # self-validation confirms the assertion tool itself is operational. # Downstream projects: Add any additional FileAssert self-validation steps here. @@ -766,6 +830,10 @@ jobs: # confirm the requirements pipeline produced well-formed documents. # Downstream projects: Add any additional requirements steps here. + - name: Create requirements output directories + shell: bash + run: mkdir -p docs/requirements_doc/generated docs/requirements_report/generated + - name: Run ReqStream self-validation run: > dotnet reqstream @@ -777,9 +845,9 @@ jobs: dotnet reqstream --requirements requirements.yaml --tests "artifacts/**/*.trx" - --report docs/requirements_doc/requirements.md - --justifications docs/requirements_doc/justifications.md - --matrix docs/requirements_report/trace_matrix.md + --report docs/requirements_doc/generated/requirements.md + --justifications docs/requirements_doc/generated/justifications.md + --matrix docs/requirements_report/generated/trace_matrix.md --enforce - name: Generate Requirements HTML with Pandoc @@ -790,14 +858,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/requirements_doc/requirements.html + --output docs/requirements_doc/generated/requirements.html - name: Generate Requirements PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/requirements_doc/requirements.html - "docs/VersionMark Requirements.pdf" + docs/requirements_doc/generated/requirements.html + "docs/generated/VersionMark Requirements.pdf" - name: Generate Trace Matrix HTML with Pandoc shell: bash @@ -807,14 +875,14 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --metadata version="${{ inputs.version }}" --metadata date="$(date +'%Y-%m-%d')" - --output docs/requirements_report/trace_matrix.html + --output docs/requirements_report/generated/trace_matrix.html - name: Generate Trace Matrix PDF with WeasyPrint run: > dotnet weasyprint --pdf-variant pdf/a-3u - docs/requirements_report/trace_matrix.html - "docs/VersionMark Trace Matrix.pdf" + docs/requirements_report/generated/trace_matrix.html + "docs/generated/VersionMark Trace Matrix.pdf" - name: Assert Requirements Documents with FileAssert run: > @@ -830,6 +898,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: documents - path: |- - docs/*.pdf - docs/build_notes.md + path: docs/generated/* diff --git a/.gitignore b/.gitignore index e37b9c4..7dc68dc 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,7 @@ _ReSharper*/ *.snupkg **/packages/* -# MSTest test Results +# Test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -88,18 +88,7 @@ __pycache__/ .venv/ # Generated documentation -docs/**/*.html -docs/**/*.pdf -!docs/template/** -docs/requirements/requirements.md -docs/justifications/justifications.md -docs/tracematrix/tracematrix.md -docs/quality/codeql-quality.md -docs/quality/sonar-quality.md -docs/code_review_plan/plan.md -docs/code_review_report/report.md -docs/buildnotes.md -docs/buildnotes/versions.md +**/generated/ # Test results TestResults/ diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index c16c443..4942746 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -50,5 +50,6 @@ ignores: - "**/thirdparty/**" - "**/third-party/**" - "**/3rd-party/**" + - "**/generated/**" - "**/AGENT_REPORT_*.md" - "**/.agent-logs/**" diff --git a/.reviewmark.yaml b/.reviewmark.yaml index a95e8d5..29d990c 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -1,19 +1,22 @@ --- # ReviewMark Configuration File # This file defines which files require review, where the evidence store is located, -# and how files are grouped into named review-sets following software unit boundaries. +# and how files are grouped into named review-sets following hierarchical scope principles. # Patterns identifying all files that require review. # Processed in order; prefix a pattern with '!' to exclude. needs-review: - - "README.md" # Project readme - - "**/*.cs" # All C# source and test files - - "requirements.yaml" # Root requirements file - - "docs/reqstream/**/*.yaml" # Requirements files - - "docs/user_guide/**/*.md" # User guide documents - - "docs/design/**/*.md" # Design documents - - "!**/obj/**" # Exclude build output - - "!**/bin/**" # Exclude build output + - "README.md" # Root README file + - "requirements.yaml" # Root requirements file + - "**/*.cs" # All C# source and test files + - "docs/reqstream/**/*.yaml" # Requirements files + - "docs/design/**/*.md" # Design documentation files + - "docs/verification/**/*.md" # Verification documentation files + - "docs/user_guide/**/*.md" # User guide documentation + - "!**/obj/**" # Exclude build output + - "!**/bin/**" # Exclude build output + - "!node_modules/**" # Exclude npm dependencies + - "!**/.venv/**" # Exclude Python virtual environment # Evidence source: review data and index.json are located in the 'reviews' branch # of this repository, accessed through the GitHub public HTTPS raw content access. @@ -23,23 +26,24 @@ evidence-source: # Review sets following standardized patterns for hierarchical compliance coverage reviews: - # Purpose Review (only one per repository) + # Purpose - id: Purpose title: Review that Advertised Features Match System Design paths: - "README.md" - "docs/user_guide/**/*.md" - - "docs/reqstream/version-mark/version-mark.yaml" # system requirements + - "docs/reqstream/version-mark.yaml" # system requirements - "docs/design/introduction.md" # design introduction and architecture - - "docs/design/version-mark/version-mark.md" # system design + - "docs/design/version-mark.md" # system design - # System-level reviews + # VersionMark - Specials - id: VersionMark-Architecture title: Review that VersionMark Architecture Satisfies Requirements paths: - - "docs/reqstream/version-mark/version-mark.yaml" # system requirements + - "docs/reqstream/version-mark.yaml" # system requirements - "docs/design/introduction.md" # design introduction and architecture - - "docs/design/version-mark/version-mark.md" # system design + - "docs/design/version-mark.md" # system design + - "docs/verification/version-mark.md" # system verification - "test/**/IntegrationTests.cs" # integration tests - "test/**/AssemblyInfo.cs" # test infrastructure - "test/**/Runner.cs" # test infrastructure @@ -47,10 +51,12 @@ reviews: - id: VersionMark-Design title: Review that VersionMark Design is Consistent and Complete paths: - - "docs/reqstream/version-mark/version-mark.yaml" # system requirements + - "docs/reqstream/version-mark.yaml" # system requirements - "docs/reqstream/version-mark/platform-requirements.yaml" # platform requirements - "docs/design/introduction.md" # design introduction - "docs/design/version-mark/**/*.md" # all system design documents + - "docs/verification/introduction.md" # verification introduction + - "docs/verification/ots.md" # OTS verification overview - id: VersionMark-AllRequirements title: Review that All VersionMark Requirements are Complete @@ -58,12 +64,13 @@ reviews: - "requirements.yaml" # root requirements file - "docs/reqstream/**/*.yaml" # all requirements files - # Cli Subsystem + # VersionMark - Cli - id: VersionMark-Cli title: Review that VersionMark Cli Satisfies Subsystem Requirements paths: - - "docs/reqstream/version-mark/cli/cli.yaml" # subsystem requirements - - "docs/design/version-mark/cli/cli.md" # subsystem design + - "docs/reqstream/version-mark/cli.yaml" # subsystem requirements + - "docs/design/version-mark/cli.md" # subsystem design + - "docs/verification/version-mark/cli.md" # subsystem verification - "test/**/Cli/CliTests.cs" # subsystem tests - id: VersionMark-Cli-Program @@ -71,6 +78,7 @@ reviews: paths: - "docs/reqstream/version-mark/cli/program.yaml" # requirements - "docs/design/version-mark/cli/program.md" # design + - "docs/verification/version-mark/cli/program.md" # verification - "src/**/Program.cs" # implementation - "test/**/ProgramTests.cs" # unit tests @@ -79,15 +87,17 @@ reviews: paths: - "docs/reqstream/version-mark/cli/context.yaml" # requirements - "docs/design/version-mark/cli/context.md" # design + - "docs/verification/version-mark/cli/context.md" # verification - "src/**/Cli/Context.cs" # implementation - "test/**/Cli/ContextTests.cs" # unit tests - # Configuration Subsystem + # VersionMark - Configuration - id: VersionMark-Configuration title: Review that VersionMark Configuration Satisfies Subsystem Requirements paths: - - "docs/reqstream/version-mark/configuration/configuration.yaml" # subsystem requirements - - "docs/design/version-mark/configuration/configuration.md" # subsystem design + - "docs/reqstream/version-mark/configuration.yaml" # subsystem requirements + - "docs/design/version-mark/configuration.md" # subsystem design + - "docs/verification/version-mark/configuration.md" # subsystem verification - "test/**/Configuration/ConfigurationTests.cs" # subsystem tests - id: VersionMark-Configuration-VersionMarkConfig @@ -95,6 +105,7 @@ reviews: paths: - "docs/reqstream/version-mark/configuration/version-mark-config.yaml" # requirements - "docs/design/version-mark/configuration/version-mark-config.md" # design + - "docs/verification/version-mark/configuration/version-mark-config.md" # verification - "src/**/Configuration/VersionMarkConfig.cs" # implementation - "test/**/Configuration/VersionMarkConfigTests.cs" # unit tests - "test/**/Configuration/VersionMarkConfigLoadTests.cs" # Load method unit tests @@ -104,6 +115,7 @@ reviews: paths: - "docs/reqstream/version-mark/configuration/tool-config.yaml" # requirements - "docs/design/version-mark/configuration/tool-config.md" # design + - "docs/verification/version-mark/configuration/tool-config.md" # verification - "src/**/Configuration/VersionMarkConfig.cs" # implementation - "test/**/Configuration/VersionMarkConfigTests.cs" # unit tests @@ -112,15 +124,17 @@ reviews: paths: - "docs/reqstream/version-mark/configuration/load.yaml" # requirements - "docs/design/version-mark/configuration/lint-issue.md" # design + - "docs/verification/version-mark/configuration/lint-issue.md" # verification - "src/**/Configuration/LintIssue.cs" # implementation - "test/**/Configuration/LintIssueTests.cs" # unit tests - # Capture Subsystem + # VersionMark - Capture - id: VersionMark-Capture title: Review that VersionMark Capture Satisfies Subsystem Requirements paths: - - "docs/reqstream/version-mark/capture/capture.yaml" # subsystem requirements - - "docs/design/version-mark/capture/capture.md" # subsystem design + - "docs/reqstream/version-mark/capture.yaml" # subsystem requirements + - "docs/design/version-mark/capture.md" # subsystem design + - "docs/verification/version-mark/capture.md" # subsystem verification - "test/**/Capture/CaptureTests.cs" # subsystem tests - id: VersionMark-Capture-VersionInfo @@ -128,15 +142,17 @@ reviews: paths: - "docs/reqstream/version-mark/capture/version-info.yaml" # requirements - "docs/design/version-mark/capture/version-info.md" # design + - "docs/verification/version-mark/capture/version-info.md" # verification - "src/**/Capture/VersionInfo.cs" # implementation - "test/**/Capture/VersionInfoTests.cs" # unit tests - # Publishing Subsystem + # VersionMark - Publishing - id: VersionMark-Publishing title: Review that VersionMark Publishing Satisfies Subsystem Requirements paths: - - "docs/reqstream/version-mark/publishing/publishing.yaml" # subsystem requirements - - "docs/design/version-mark/publishing/publishing.md" # subsystem design + - "docs/reqstream/version-mark/publishing.yaml" # subsystem requirements + - "docs/design/version-mark/publishing.md" # subsystem design + - "docs/verification/version-mark/publishing.md" # subsystem verification - "test/**/Publishing/PublishingTests.cs" # subsystem tests - id: VersionMark-Publishing-MarkdownFormatter @@ -144,15 +160,17 @@ reviews: paths: - "docs/reqstream/version-mark/publishing/markdown-formatter.yaml" # requirements - "docs/design/version-mark/publishing/markdown-formatter.md" # design + - "docs/verification/version-mark/publishing/markdown-formatter.md" # verification - "src/**/Publishing/MarkdownFormatter.cs" # implementation - "test/**/Publishing/MarkdownFormatterTests.cs" # unit tests - # SelfTest Subsystem + # VersionMark - SelfTest - id: VersionMark-SelfTest title: Review that VersionMark SelfTest Satisfies Subsystem Requirements paths: - - "docs/reqstream/version-mark/self-test/self-test.yaml" # subsystem requirements - - "docs/design/version-mark/self-test/self-test.md" # subsystem design + - "docs/reqstream/version-mark/self-test.yaml" # subsystem requirements + - "docs/design/version-mark/self-test.md" # subsystem design + - "docs/verification/version-mark/self-test.md" # subsystem verification - "test/**/SelfTest/SelfTestTests.cs" # subsystem tests - id: VersionMark-SelfTest-Validation @@ -160,6 +178,7 @@ reviews: paths: - "docs/reqstream/version-mark/self-test/validation.yaml" # requirements - "docs/design/version-mark/self-test/validation.md" # design + - "docs/verification/version-mark/self-test/validation.md" # verification - "src/**/SelfTest/Validation.cs" # implementation - "test/**/SelfTest/SelfTestTests.cs" # subsystem tests (no separate unit test file) @@ -168,5 +187,61 @@ reviews: paths: - "docs/reqstream/version-mark/self-test/path-helpers.yaml" # requirements - "docs/design/version-mark/self-test/path-helpers.md" # design + - "docs/verification/version-mark/self-test/path-helpers.md" # verification - "src/**/SelfTest/PathHelpers.cs" # implementation - "test/**/SelfTest/PathHelpersTests.cs" # unit tests + + # OTS Items + - id: OTS-BuildMark + title: Review of BuildMark OTS verification evidence + paths: + - "docs/reqstream/ots/buildmark.yaml" + - "docs/verification/ots/buildmark.md" + + - id: OTS-FileAssert + title: Review of FileAssert OTS verification evidence + paths: + - "docs/reqstream/ots/fileassert.yaml" + - "docs/verification/ots/fileassert.md" + + - id: OTS-xUnit + title: Review of xUnit OTS verification evidence + paths: + - "docs/reqstream/ots/xunit.yaml" + - "docs/verification/ots/xunit.md" + + - id: OTS-Pandoc + title: Review of Pandoc OTS verification evidence + paths: + - "docs/reqstream/ots/pandoc.yaml" + - "docs/verification/ots/pandoc.md" + + - id: OTS-ReqStream + title: Review of ReqStream OTS verification evidence + paths: + - "docs/reqstream/ots/reqstream.yaml" + - "docs/verification/ots/reqstream.md" + + - id: OTS-ReviewMark + title: Review of ReviewMark OTS verification evidence + paths: + - "docs/reqstream/ots/reviewmark.yaml" + - "docs/verification/ots/reviewmark.md" + + - id: OTS-SarifMark + title: Review of SarifMark OTS verification evidence + paths: + - "docs/reqstream/ots/sarifmark.yaml" + - "docs/verification/ots/sarifmark.md" + + - id: OTS-SonarMark + title: Review of SonarMark OTS verification evidence + paths: + - "docs/reqstream/ots/sonarmark.yaml" + - "docs/verification/ots/sonarmark.md" + + - id: OTS-WeasyPrint + title: Review of WeasyPrint OTS verification evidence + paths: + - "docs/reqstream/ots/weasyprint.yaml" + - "docs/verification/ots/weasyprint.md" diff --git a/.yamllint.yaml b/.yamllint.yaml index 4fbc811..79c3aee 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -15,13 +15,14 @@ extends: default # Exclude common build artifacts, dependencies, and vendored third-party code ignore: | - .git/ - node_modules/ - .venv/ - thirdparty/ - third-party/ - 3rd-party/ - .agent-logs/ + **/.git/** + **/node_modules/** + **/.venv/** + **/thirdparty/** + **/third-party/** + **/3rd-party/** + **/generated/** + **/.agent-logs/** rules: # Allow 'on:' in GitHub Actions workflows (not a boolean value) diff --git a/AGENTS.md b/AGENTS.md index 738f61b..6776bfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,12 @@ +# Project Overview + +- **name**: VersionMark +- **description**: A .NET tool for capturing and publishing tool version + information across CI/CD environments. It tracks which versions of build + tools, compilers, and dependencies are used in different jobs and environments. +- **languages**: C# +- **technologies**: .NET 8/9/10, NuGet, YAML, GitHub Actions + # Project Structure ```text @@ -10,7 +19,8 @@ │ ├── requirements_doc/ │ ├── requirements_report/ │ ├── reqstream/ -│ └── user_guide/ +│ ├── user_guide/ +│ └── verification/ ├── src/ │ └── DemaConsulting.VersionMark/ └── test/ @@ -45,16 +55,17 @@ before searching the filesystem. Before performing any work, agents must read and apply the relevant standards from `.github/standards/`. Use this matrix to determine which to load: -| Work involves... | Load these standards | -|----------------------|------------------------------------------------------------------------------| -| Any code | `coding-principles.md` | -| C# code | `coding-principles.md`, `csharp-language.md` | -| Any tests | `testing-principles.md` | -| C# tests | `testing-principles.md`, `csharp-testing.md` | -| Requirements | `requirements-principles.md`, `software-items.md`, `reqstream-usage.md` | -| Design docs | `software-items.md`, `design-documentation.md`, `technical-documentation.md` | -| Review configuration | `software-items.md`, `reviewmark-usage.md` | -| Any documentation | `technical-documentation.md` | +| Work involves... | Load these standards | +|----------------------|------------------------------------------------------------------------------------| +| Any code | `coding-principles.md` | +| C# code | `coding-principles.md`, `csharp-language.md` | +| Any tests | `testing-principles.md` | +| C# tests | `testing-principles.md`, `csharp-testing.md` | +| Requirements | `requirements-principles.md`, `software-items.md`, `reqstream-usage.md` | +| Design docs | `software-items.md`, `design-documentation.md`, `technical-documentation.md` | +| Verification docs | `software-items.md`, `verification-documentation.md`, `technical-documentation.md` | +| Review configuration | `software-items.md`, `reviewmark-usage.md` | +| Any documentation | `technical-documentation.md` | Load only the standards relevant to your specific task scope. @@ -69,26 +80,10 @@ Delegate to specialized agents only for specific scenarios: - **Formal feature implementation** (complex, multi-step) → Call the implementation agent - **Formal bug resolution** (complex debugging, systematic fixes) → Call the implementation agent - **Formal reviews** (compliance verification, detailed analysis) → Call the formal-review agent -- **Template consistency** (downstream repository alignment) → Call the repo-consistency agent - -## Available Specialized Agents - -- **lint-fix** - Pre-PR lint sweep agent that loops running `pwsh ./lint.ps1`, - fixing issues until the repository is lint-clean -- **developer** - General-purpose software development agent that applies appropriate - standards based on the work being performed -- **formal-review** - Agent for performing formal reviews using standardized review processes -- **implementation** - Orchestrator agent that manages quality implementations - through a formal state machine workflow -- **quality** - Quality assurance agent that grades developer work against project - standards and Continuous Compliance practices -- **repo-consistency** - Ensures downstream repositories remain consistent with - the TemplateDotNetTool template patterns and best practices # Agent Reporting (Specialized Agents Must Follow) -Specialized agents (lint-fix, developer, quality, implementation, -formal-review, repo-consistency) MUST generate a completion report: +Specialized agents MUST generate a completion report: 1. Save to `.agent-logs/{agent-name}-{subject}-{unique-id}.md` where `{subject}` is a kebab-case task summary (max 5 words) and @@ -107,7 +102,7 @@ Result semantics for orchestrator decision-making: # Formatting (After Making Changes) After making changes, run the auto-fix pass. This applies all available fixers -silently and **always exits 0** — agents do not need to respond to its output. +silently and **always exits 0** - agents do not need to respond to its output. ```pwsh pwsh ./fix.ps1 @@ -115,7 +110,7 @@ pwsh ./fix.ps1 This automatically handles: `dotnet format`, markdown formatting, and YAML formatting. Full lint compliance is a **pre-PR responsibility**, not an agent -responsibility — invoke the lint-fix agent once before submitting a pull request. +responsibility - invoke the lint-fix agent once before submitting a pull request. ## CI Quality Tools @@ -124,6 +119,8 @@ reqstream, versionmark, and reviewmark. # Scope Discipline (ALL Agents Must Follow) +- **No generated file access**: Files inside any `generated/` folder are build + outputs - do not read, lint, or modify them - **Minimum necessary changes**: Only modify files directly required by the task - **No speculative refactoring**: Do not refactor code adjacent to the change unless the task explicitly requests it diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4052529..eb58afa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,7 +126,7 @@ Note the spaces after `///` for proper indentation in summary blocks. ### Test Framework -We use MSTest v4 for unit and integration tests. +We use xUnit v3 for unit and integration tests. ### Test Naming Convention @@ -141,9 +141,9 @@ Examples: ### Writing Tests - Write tests that are clear and focused -- Use modern MSTest v4 assertions: - - `Assert.HasCount(expectedCount, collection)` - - `Assert.IsEmpty(collection)` +- Use xUnit assertions: + - `Assert.Equal(expected, actual)` + - `Assert.Empty(collection)` - `Assert.DoesNotContain(item, collection)` - Always clean up resources (use `try/finally` for console redirection) - Link tests to requirements in `requirements.yaml` when applicable diff --git a/docs/build_notes/definition.yaml b/docs/build_notes/definition.yaml index 207a375..ba1360b 100644 --- a/docs/build_notes/definition.yaml +++ b/docs/build_notes/definition.yaml @@ -5,8 +5,8 @@ resource-path: input-files: - docs/build_notes/title.txt - docs/build_notes/introduction.md - - docs/build_notes.md - - docs/build_notes/versions.md + - docs/build_notes/generated/build_notes.md + - docs/build_notes/generated/versions.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_quality/definition.yaml b/docs/code_quality/definition.yaml index 68c58f2..fed5f02 100644 --- a/docs/code_quality/definition.yaml +++ b/docs/code_quality/definition.yaml @@ -5,8 +5,8 @@ resource-path: input-files: - docs/code_quality/title.txt - docs/code_quality/introduction.md - - docs/code_quality/codeql-quality.md - - docs/code_quality/sonar-quality.md + - docs/code_quality/generated/codeql-quality.md + - docs/code_quality/generated/sonar-quality.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_review_plan/definition.yaml b/docs/code_review_plan/definition.yaml index 3a24f0b..56989bf 100644 --- a/docs/code_review_plan/definition.yaml +++ b/docs/code_review_plan/definition.yaml @@ -5,7 +5,7 @@ resource-path: input-files: - docs/code_review_plan/title.txt - docs/code_review_plan/introduction.md - - docs/code_review_plan/plan.md + - docs/code_review_plan/generated/plan.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_review_report/definition.yaml b/docs/code_review_report/definition.yaml index 6498e6c..b238d43 100644 --- a/docs/code_review_report/definition.yaml +++ b/docs/code_review_report/definition.yaml @@ -5,7 +5,7 @@ resource-path: input-files: - docs/code_review_report/title.txt - docs/code_review_report/introduction.md - - docs/code_review_report/report.md + - docs/code_review_report/generated/report.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index 76e68e8..378d38e 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -5,19 +5,19 @@ resource-path: input-files: - docs/design/title.txt - docs/design/introduction.md - - docs/design/version-mark/version-mark.md - - docs/design/version-mark/cli/cli.md + - docs/design/version-mark.md + - docs/design/version-mark/cli.md - docs/design/version-mark/cli/program.md - docs/design/version-mark/cli/context.md - - docs/design/version-mark/configuration/configuration.md + - docs/design/version-mark/configuration.md - docs/design/version-mark/configuration/tool-config.md - docs/design/version-mark/configuration/version-mark-config.md - docs/design/version-mark/configuration/lint-issue.md - - docs/design/version-mark/capture/capture.md + - docs/design/version-mark/capture.md - docs/design/version-mark/capture/version-info.md - - docs/design/version-mark/publishing/publishing.md + - docs/design/version-mark/publishing.md - docs/design/version-mark/publishing/markdown-formatter.md - - docs/design/version-mark/self-test/self-test.md + - docs/design/version-mark/self-test.md - docs/design/version-mark/self-test/validation.md - docs/design/version-mark/self-test/path-helpers.md template: template.html diff --git a/docs/design/introduction.md b/docs/design/introduction.md index 5143e27..9828179 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -29,8 +29,8 @@ This document covers the design of five subsystems within VersionMark: together provide built-in verification of the tool's core functionality This document does not cover installation, end-user usage patterns, or the CI/CD pipeline -configuration. Those topics are addressed in the [User Guide][user-guide] and the -[Requirements document][requirements-doc]. +configuration. Those topics are addressed in the *VersionMark User Guide* and the +*VersionMark Requirements Document*. ## Software Structure @@ -102,6 +102,3 @@ The source code in `src/DemaConsulting.VersionMark/` is the authoritative implem This document describes the intent and structure of that code; any discrepancy between this document and the code should be resolved by updating this document to reflect the actual implementation, or by raising a defect against the code. - -[user-guide]: ../user_guide/introduction.md -[requirements-doc]: ../requirements_doc/introduction.md diff --git a/docs/design/version-mark/version-mark.md b/docs/design/version-mark.md similarity index 97% rename from docs/design/version-mark/version-mark.md rename to docs/design/version-mark.md index 033db7b..ee656a9 100644 --- a/docs/design/version-mark/version-mark.md +++ b/docs/design/version-mark.md @@ -80,8 +80,8 @@ Cli Subsystem → Configuration Subsystem (VersionMarkConfig.Load) defaulting to `.versionmark.yaml`. 2. `RunLint` delegates to `VersionMarkConfig.Load`, which validates the YAML structure and returns a `VersionMarkLoadResult` containing all `LintIssue` records found. -3. `RunLint` calls `result.ReportIssues` to write all issues to the context, then confirms - success when no issues were found. +3. `RunLint` calls `result.ReportIssues` to write all issues to the context, then exits + silently with exit code 0 when no issues are found. ### Validate Mode @@ -105,7 +105,7 @@ that the tool is functioning correctly after installation: | `VersionMark_LintPassesForValidConfig` | Lint mode passes for a valid `.versionmark.yaml` configuration | | `VersionMark_LintReportsErrorsForInvalidConfig` | Lint mode reports errors for an invalid configuration | -These test names appear in requirements files (e.g., `system.yaml`, +These test names appear in requirements files (e.g., `version-mark.yaml`, `platform-requirements.yaml`) as traceability evidence. When `--validate` is run in CI, each matrix job runs on a specific platform/runtime and produces a TRX results file whose filename and CI job context (e.g., `artifacts/validation-windows-latest-dotnet8.x.trx`) diff --git a/docs/design/version-mark/capture.md b/docs/design/version-mark/capture.md new file mode 100644 index 0000000..fe96c04 --- /dev/null +++ b/docs/design/version-mark/capture.md @@ -0,0 +1,25 @@ +## Capture Subsystem + +### Overview + +The capture subsystem is responsible for persisting tool version information for the +current CI/CD job environment. It receives structured version results produced by the +configuration subsystem (for example, by `VersionMarkConfig.FindVersions`) and saves +them to a JSON file. The captured data is later consumed by the publish subsystem to +generate the version report. + +The capture subsystem consists of a single unit: `VersionInfo`, which is the data transfer +record for captured version data. + +The `VersionInfo` unit is described in full in the *VersionInfo Unit Design* section of this document. + +### Cross-Subsystem Dependencies + +- **`Cli.Context`** (Caller → Capture): Provides `JobId`, `OutputFile`, `ToolNames` to + `RunCapture` +- **`Configuration.VersionMarkConfig`** (Caller → Capture): `FindVersions` produces the + `VersionInfo` record saved by `RunCapture` +- **`Program.RunCapture`** (Caller → Capture): Orchestrates load-config → find-versions → + save pipeline +- **`Program.RunPublish`** (Caller → Capture): Calls `VersionInfo.LoadFromFile` for each + matched JSON file diff --git a/docs/design/version-mark/capture/capture.md b/docs/design/version-mark/capture/capture.md deleted file mode 100644 index aa99d45..0000000 --- a/docs/design/version-mark/capture/capture.md +++ /dev/null @@ -1,93 +0,0 @@ -# Capture Subsystem - -## Overview - -The capture subsystem is responsible for persisting tool version information for the -current CI/CD job environment. It receives structured version results produced by the -configuration subsystem (for example, by `VersionMarkConfig.FindVersions`) and saves -them to a JSON file. The captured data is later consumed by the publish subsystem to -generate the version report. - -The capture subsystem consists of a single unit: `VersionInfo`, which is the data transfer -record for captured version data. - -## VersionInfo Unit Design - -`VersionInfo` is a positional record with two properties: - -| Property | Type | Description | -|------------|------------------------------|-------------------------------------------------------| -| `JobId` | `string` | Identifies the CI/CD job that captured these versions | -| `Versions` | `Dictionary` | Maps tool names to their captured version strings | - -### Constructor - -The record constructor accepts `JobId` and `Versions` as positional parameters. -These values are immutable once set. No validation is performed in the constructor -because `VersionInfo` is a pure data-transfer record; validation of the source data -occurs in the configuration and CLI layers. - -### SaveToFile Method - -`SaveToFile(string filePath)` serializes the record to indented JSON using -`JsonSerializer.Serialize` with `WriteIndented = true` and writes the result to the -specified path using UTF-8 encoding. - -- **Success**: Creates or overwrites the file at `filePath` with JSON content. -- **Error contract**: Any exception other than `InvalidOperationException` is caught and - re-thrown as `InvalidOperationException` with an explanatory message and the original - exception as inner cause. This lets callers rely on a single exception type for - file-write failures. - -### LoadFromFile Method - -`LoadFromFile(string filePath)` is the symmetric counterpart to `SaveToFile`. It: - -1. Checks that the file exists; throws `ArgumentException` if not - (message contains "not found"). -2. Reads the file content as UTF-8. -3. Deserializes using `JsonSerializer.Deserialize`. -4. Validates the result is not null; throws `ArgumentException` if it is - (message contains "deserialize"). - -`JsonException` from step 3 is caught and re-thrown as `ArgumentException` (message -contains "parse"). Any other non-`ArgumentException` error is wrapped similarly. - -## JSON File Schema - -The JSON file produced by `SaveToFile` has this structure: - -```json -{ - "JobId": "build-linux", - "Versions": { - "dotnet": "9.0.1", - "node": "22.12.0" - } -} -``` - -Property names in JSON match the C# property names exactly because no -`JsonPropertyName` attributes are applied. The file is UTF-8 encoded with -indentation for human readability and version-control diff-friendliness. - -## Error Handling Contract - -| Situation | Exception type | Message contains | -|---------------------------------------|-----------------------------|-------------------| -| File not found (`LoadFromFile`) | `ArgumentException` | "not found" | -| JSON deserialization returns null | `ArgumentException` | "deserialize" | -| JSON is malformed (`LoadFromFile`) | `ArgumentException` | "parse" | -| Other read failure (`LoadFromFile`) | `ArgumentException` | "Failed to read" | -| Write failure (`SaveToFile`) | `InvalidOperationException` | "Failed to save" | - -## Cross-Subsystem Dependencies - -- **`Cli.Context`** (Caller → Capture): Provides `JobId`, `OutputFile`, `ToolNames` to - `RunCapture` -- **`Configuration.VersionMarkConfig`** (Caller → Capture): `FindVersions` produces the - `VersionInfo` record saved by `RunCapture` -- **`Program.RunCapture`** (Caller → Capture): Orchestrates load-config → find-versions → - save pipeline -- **`Program.RunPublish`** (Caller → Capture): Calls `VersionInfo.LoadFromFile` for each - matched JSON file diff --git a/docs/design/version-mark/capture/version-info.md b/docs/design/version-mark/capture/version-info.md index b2eaa91..1e66bf5 100644 --- a/docs/design/version-mark/capture/version-info.md +++ b/docs/design/version-mark/capture/version-info.md @@ -1,6 +1,6 @@ -# VersionInfo Unit +### VersionInfo Unit -## Overview +#### Overview The `VersionInfo` record (`VersionInfo.cs`) is a positional record with two properties: @@ -13,7 +13,7 @@ The `VersionInfo` record (`VersionInfo.cs`) is a positional record with two prop produces it by executing commands and saving to JSON; publish reads it back and passes it to `MarkdownFormatter`. -## SaveToFile Method +#### SaveToFile Method `SaveToFile` serializes the record to indented JSON using `JsonSerializer.Serialize` with `WriteIndented = true` and writes it to the specified path using UTF-8 encoding. @@ -22,7 +22,7 @@ Non-`InvalidOperationException` errors are wrapped and re-thrown as `VersionMark-VersionInfo-Save`. The default output filename (`versionmark-.json`) is determined by the CLI layer and contributes to satisfying `VersionMark-Capture-DefaultOutput`. -## LoadFromFile Method +#### LoadFromFile Method `LoadFromFile` is the symmetric counterpart to `SaveToFile`. It: @@ -36,7 +36,7 @@ non-`ArgumentException` errors are wrapped similarly. This satisfies `VersionMark-VersionInfo-Load`, `VersionMark-VersionInfo-Error`, `VersionMark-Publish-Consolidate`, and `VersionMark-Publish-FileError`. -## JSON Schema +#### JSON Schema The JSON file produced by `SaveToFile` has this structure: diff --git a/docs/design/version-mark/cli/cli.md b/docs/design/version-mark/cli.md similarity index 90% rename from docs/design/version-mark/cli/cli.md rename to docs/design/version-mark/cli.md index f518840..7d1481e 100644 --- a/docs/design/version-mark/cli/cli.md +++ b/docs/design/version-mark/cli.md @@ -1,6 +1,6 @@ -# Cli Subsystem +## Cli Subsystem -## Overview +### Overview The command-line interface subsystem is responsible for parsing command-line arguments, routing program flow to the appropriate subsystem, and managing all output (console, diff --git a/docs/design/version-mark/cli/context.md b/docs/design/version-mark/cli/context.md index fce564d..b02d7c1 100644 --- a/docs/design/version-mark/cli/context.md +++ b/docs/design/version-mark/cli/context.md @@ -1,11 +1,11 @@ -# Context Unit +### Context Unit -## Overview +#### Overview The `Context` class (`Context.cs`) is a sealed, disposable container for all parsed command-line state and output routing. It is constructed via the `Create` factory method. -## Properties +#### Properties | Property | Type | Default | Description | |---------------|------------|---------|-------------------------------------------| @@ -32,14 +32,14 @@ This satisfies requirements `VersionMark-CommandLine-Context`, `VersionMark-Comm `VersionMark-CommandLine-Results`, `VersionMark-CommandLine-Log`, `VersionMark-CommandLine-ExitCode`, `VersionMark-CommandLine-Lint`, and `VersionMark-Context-Create`. -## ArgumentParser +#### ArgumentParser The private `ArgumentParser` class performs the actual token-by-token parsing. It handles the `--` separator, which switches subsequent tokens to either tool names (capture mode) or glob patterns (publish mode). Unknown arguments throw `ArgumentException`, satisfying `VersionMark-CommandLine-InvalidArgs`. -## WriteLine and WriteError +#### WriteLine and WriteError `WriteLine` writes to `Console.Out` unless `Silent` is set, and also writes to the log file if one was opened. `WriteError` additionally sets `_hasErrors = true` (making @@ -47,7 +47,7 @@ file if one was opened. `WriteError` additionally sets `_hasErrors = true` (maki `VersionMark-CommandLine-Silent`, `VersionMark-CommandLine-ErrorOutput`, `VersionMark-CommandLine-ExitCode`, `VersionMark-Context-WriteLine`, `VersionMark-Context-WriteError`, and `VersionMark-Context-WriteErrorExitCode`. -## Log File +#### Log File The `OpenLogFile` method opens a `StreamWriter` with `AutoFlush = true`. If opening fails, an `InvalidOperationException` is thrown with contextual information. The writer is diff --git a/docs/design/version-mark/cli/program.md b/docs/design/version-mark/cli/program.md index 82fe34c..518298c 100644 --- a/docs/design/version-mark/cli/program.md +++ b/docs/design/version-mark/cli/program.md @@ -1,25 +1,25 @@ -# Program Unit +### Program Unit -## Overview +#### Overview The `Program` class (`Program.cs`) is the top-level entry point for the tool. It owns the `Main` method, constructs the `Context`, dispatches to the appropriate mode, and handles top-level exception translation. -## Version Property +#### Version Property The static `Version` property reads the assembly's `AssemblyInformationalVersionAttribute` at runtime and falls back to `AssemblyVersion` or `"0.0.0"` if neither is available. This satisfies requirement `VersionMark-Program-Version`. -## Main Method +#### Main Method `Main` creates a `Context` from the command-line arguments, calls `Run`, and returns `context.ExitCode`. `ArgumentException` and `InvalidOperationException` are caught and written to `Console.Error`, returning exit code 1. Unexpected exceptions are re-thrown to generate event-log entries. This satisfies requirement `VersionMark-Program-Dispatch`. -## Run Method +#### Run Method `Run` implements priority-ordered dispatch: @@ -36,14 +36,14 @@ generate event-log entries. This satisfies requirement `VersionMark-Program-Disp This dispatch order satisfies requirement `VersionMark-Program-Dispatch`. -## Capture and Publish Orchestration +#### Capture and Publish Orchestration `RunCapture` and `RunPublish` are private helpers called from `Run`. They validate required arguments, invoke configuration loading and version capture/report generation, and delegate error handling to `context.WriteError`. These methods satisfy requirements `VersionMark-Program-RunCapture` and `VersionMark-Program-RunPublish`. -## RunLint +#### RunLint `RunLint` is a private helper called from `Run`. It resolves the configuration file path, defaulting to `.versionmark.yaml` when `context.LintFile` is `null`, then calls diff --git a/docs/design/version-mark/configuration/configuration.md b/docs/design/version-mark/configuration.md similarity index 91% rename from docs/design/version-mark/configuration/configuration.md rename to docs/design/version-mark/configuration.md index 44c5087..f868f79 100644 --- a/docs/design/version-mark/configuration/configuration.md +++ b/docs/design/version-mark/configuration.md @@ -1,6 +1,6 @@ -# Configuration Subsystem +## Configuration Subsystem -## Overview +### Overview The configuration subsystem reads and interprets the `.versionmark.yaml` file that defines which tools to capture and how to extract their versions, and reports any validation issues diff --git a/docs/design/version-mark/configuration/lint-issue.md b/docs/design/version-mark/configuration/lint-issue.md index 9daad3c..a339d3c 100644 --- a/docs/design/version-mark/configuration/lint-issue.md +++ b/docs/design/version-mark/configuration/lint-issue.md @@ -1,12 +1,12 @@ -# LintIssue Unit +### LintIssue Unit -## Overview +#### Overview `LintIssue.cs` contains the types used to surface validation issues found while loading a `.versionmark.yaml` configuration file. It defines three public types: the `LintSeverity` enumeration, the `LintIssue` record, and the `VersionMarkLoadResult` record. -## LintSeverity Enumeration +#### LintSeverity Enumeration `LintSeverity` classifies the severity of a validation issue: @@ -15,7 +15,7 @@ enumeration, the `LintIssue` record, and the `VersionMarkLoadResult` record. | `Warning` | Non-fatal advisory message; loading continues. | | `Error` | Fatal validation failure that prevents the configuration from being built. | -## LintIssue Record +#### LintIssue Record `LintIssue` represents a single issue found during configuration loading. It carries: @@ -31,7 +31,7 @@ enumeration, the `LintIssue` record, and the `VersionMarkLoadResult` record. where `{severity}` is emitted in lowercase (`warning` or `error`), producing output in the familiar `file(line,col): error: message` format understood by CI systems and editors. -## VersionMarkLoadResult Record +#### VersionMarkLoadResult Record `VersionMarkLoadResult` is the return type of `VersionMarkConfig.Load`. It bundles two properties: diff --git a/docs/design/version-mark/configuration/tool-config.md b/docs/design/version-mark/configuration/tool-config.md index 2ee0020..20dd9de 100644 --- a/docs/design/version-mark/configuration/tool-config.md +++ b/docs/design/version-mark/configuration/tool-config.md @@ -1,6 +1,6 @@ -# ToolConfig Unit +### ToolConfig Unit -## Overview +#### Overview The `ToolConfig` record (`VersionMarkConfig.cs`) represents the configuration for a single tool entry. It holds two dictionaries keyed by OS name: @@ -10,7 +10,7 @@ tool entry. It holds two dictionaries keyed by OS name: | `Command` | `""` (default), `"win"`, `"linux"`, `"macos"` | Shell command to run | | `Regex` | `""` (default), `"win"`, `"linux"`, `"macos"` | Regex pattern with `version` group | -## OS-Specific Overrides +#### OS-Specific Overrides `GetEffectiveCommand` and `GetEffectiveRegex` resolve the active OS at runtime using `RuntimeInformation.IsOSPlatform` and then look up the OS-specific key first, falling back @@ -18,7 +18,7 @@ to the default (`""`) key. When no default (`""`) key is present either, an `InvalidOperationException` is thrown. This satisfies requirements `VersionMark-ToolConfig-EffectiveCommand` and `VersionMark-ToolConfig-EffectiveRegex`. -## YAML Parsing +#### YAML Parsing Tool YAML parsing is performed by the private `VersionMarkConfig.ValidateTool` method. It reads a `YamlMappingNode` and populates the command and regex dictionaries. Known keys diff --git a/docs/design/version-mark/configuration/version-mark-config.md b/docs/design/version-mark/configuration/version-mark-config.md index 5807947..2688bee 100644 --- a/docs/design/version-mark/configuration/version-mark-config.md +++ b/docs/design/version-mark/configuration/version-mark-config.md @@ -1,12 +1,12 @@ -# VersionMarkConfig Unit +### VersionMarkConfig Unit -## Overview +#### Overview The `VersionMarkConfig` record holds a `Dictionary` mapping tool names to their configurations. It is the top-level entry point for loading configuration from the `.versionmark.yaml` file. -## Load Method +#### Load Method `Load` is the primary entry point for loading configuration with integrated linting. It: @@ -21,7 +21,7 @@ a list of `LintIssue` records. YAML parse errors are captured as error-level iss source location. This satisfies requirements `VersionMark-Configuration-YamlConfig`, `VersionMark-Configuration-ValidateTools`, and `VersionMark-Configuration-ParseError`. -### Error-Handling Strategy +##### Error-Handling Strategy `Load` uses an accumulate-and-continue approach: rather than aborting on the first error, all warnings and errors are collected in a single `issues` list across the entire file. @@ -36,7 +36,7 @@ The returned `VersionMarkLoadResult` carries a `null` `Config` property when any error-severity issue exists, so callers can distinguish a warnings-only load from a failure without iterating the issue list themselves. -## ValidateTool Helper +#### ValidateTool Helper The private `ValidateTool` method processes a single tool's `YamlMappingNode`. It: @@ -49,7 +49,7 @@ The private `ValidateTool` method processes a single tool's `YamlMappingNode`. I 6. Sets `toolConfig` to `null` when any new errors were added since the snapshot; otherwise returns a `ToolConfig` constructed from the validated dictionaries. -## TryCompileRegex Helper +#### TryCompileRegex Helper The private `TryCompileRegex` method attempts to compile a regex pattern with `RegexOptions.Multiline | RegexOptions.IgnoreCase` and a one-second timeout. If @@ -57,13 +57,13 @@ compilation fails (invalid pattern syntax), it appends an error-level `LintIssue the shared list and returns `null`. On success it returns the compiled `Regex` for group-name inspection by `ValidateTool`. -## ReadFromFile Method +#### ReadFromFile Method `ReadFromFile` is a backward-compatibility wrapper that delegates to `Load`. It throws `ArgumentException` if any error-level lint issues are present. Use `Load` directly when you need access to lint issues. -## FindVersions Method +#### FindVersions Method `FindVersions` accepts a list of tool names and a job ID. For each named tool it: @@ -76,14 +76,14 @@ you need access to lint issues. The method returns a `VersionInfo` record. This satisfies requirements `VersionMark-Capture-Command` and `VersionMark-Capture-MultipleTools`. -## RunCommand Helper +#### RunCommand Helper `RunCommand` runs the command through the OS shell (`cmd.exe /c` on Windows, `/bin/sh -c` on other platforms) using `Process.Start` with redirected stdout and stderr. Output and error streams are read asynchronously to prevent pipe-deadlock. A non-zero exit code raises `InvalidOperationException`. This satisfies `VersionMark-Capture-Command`. -## ExtractVersion Helper +#### ExtractVersion Helper `ExtractVersion` compiles the regex with `Multiline | IgnoreCase` and a 1-second timeout, matches against the command output, and returns the value of the named `version` capture diff --git a/docs/design/version-mark/publishing/publishing.md b/docs/design/version-mark/publishing.md similarity index 93% rename from docs/design/version-mark/publishing/publishing.md rename to docs/design/version-mark/publishing.md index 03079f2..613c3f2 100644 --- a/docs/design/version-mark/publishing/publishing.md +++ b/docs/design/version-mark/publishing.md @@ -1,6 +1,6 @@ -# Publishing Subsystem +## Publishing Subsystem -## Overview +### Overview The publish subsystem is responsible for generating a human-readable markdown version report from captured JSON files. It reads the version data produced by the capture @@ -9,7 +9,7 @@ subsystem and consolidates identical versions across jobs, flagging any conflict The publish subsystem consists of a single unit: `MarkdownFormatter`, which converts a collection of `VersionInfo` records into a markdown string. -## MarkdownFormatter.Format Interface +### MarkdownFormatter.Format Interface `MarkdownFormatter.Format` is an `internal static` method with the signature: @@ -27,7 +27,7 @@ public static string Format(IEnumerable versionInfos, int reportDep `reportDepth` must be greater than zero. A value of `0` or less causes `ArgumentOutOfRangeException` to be thrown. -## Normal-Operation Walkthrough +### Normal-Operation Walkthrough The `--publish` command follows this pipeline in `Program.RunPublish`: @@ -41,7 +41,7 @@ The `--publish` command follows this pipeline in `Program.RunPublish`: together with `context.ReportDepth`. 7. Write the returned markdown string to the file specified by `--report`. -## Error Handling +### Error Handling - **`--report` not specified**: `context.WriteError` with mention of `--report`; exit 1 - **No files match the glob patterns**: `context.WriteError` listing the patterns; exit 1 @@ -50,7 +50,7 @@ The `--publish` command follows this pipeline in `Program.RunPublish`: - **`reportDepth <= 0`**: `ArgumentOutOfRangeException` thrown by `MarkdownFormatter.Format` -## reportDepth Configuration and Conflict-Display Logic +### reportDepth Configuration and Conflict-Display Logic `context.ReportDepth` is populated from the `--report-depth` CLI argument (default: the value of `--depth`, which itself defaults to `1`). diff --git a/docs/design/version-mark/publishing/markdown-formatter.md b/docs/design/version-mark/publishing/markdown-formatter.md index 8d0a782..f204aff 100644 --- a/docs/design/version-mark/publishing/markdown-formatter.md +++ b/docs/design/version-mark/publishing/markdown-formatter.md @@ -1,6 +1,6 @@ -# MarkdownFormatter Unit +### MarkdownFormatter Unit -## Overview +#### Overview The `MarkdownFormatter` class (`MarkdownFormatter.cs`) provides the `Format` static method that converts a collection of `VersionInfo` records into a markdown string. This satisfies @@ -8,7 +8,7 @@ requirements `VersionMark-Formatter-Structure`, `VersionMark-Formatter-JobId`, `VersionMark-Formatter-Versions`, `VersionMark-Formatter-MarkdownList`, and `VersionMark-Formatter-MarkdownConsolidation`. -## Format Method +#### Format Method `Format` accepts an `IEnumerable` and an optional `reportDepth` (default 2), and returns a markdown string. @@ -21,7 +21,7 @@ The method delegates to two helpers: 2. **`GenerateMarkdown`**: writes the heading, sorts tools alphabetically, and calls `FormatVersionEntries` for each tool. -## Output Structure +#### Output Structure The markdown output begins with a `Tool Versions` section heading whose level is controlled by `reportDepth`. For example, with `reportDepth = 2` the heading is @@ -29,7 +29,7 @@ controlled by `reportDepth`. For example, with `reportDepth = 2` the heading is Each tool is then listed as one or more markdown bullet items below the heading. -## Version Consolidation Logic +#### Version Consolidation Logic `FormatVersionEntries` implements the consolidation rule: @@ -44,7 +44,7 @@ Both cases use bold tool names. In the multi-version case, each unique version a its own line with the alphabetically-sorted job IDs that produced it enclosed in parentheses. -## Heading Depth +#### Heading Depth The heading prefix is constructed as `new string('#', reportDepth)`, so `reportDepth = 2` yields `## Tool Versions`, `reportDepth = 1` yields `# Tool Versions`, and so on. This @@ -53,7 +53,7 @@ satisfies `VersionMark-Formatter-MarkdownConsolidation`. `reportDepth` must be greater than zero. Passing `0` or a negative value causes `ArgumentOutOfRangeException` to be thrown before any output is generated. -## Error Handling +#### Error Handling | Input condition | Behavior | |----------------------------------|---------------------------------------------------| diff --git a/docs/design/version-mark/self-test/self-test.md b/docs/design/version-mark/self-test.md similarity index 89% rename from docs/design/version-mark/self-test/self-test.md rename to docs/design/version-mark/self-test.md index 0a9e841..c11ce1b 100644 --- a/docs/design/version-mark/self-test/self-test.md +++ b/docs/design/version-mark/self-test.md @@ -1,6 +1,6 @@ -# SelfTest Subsystem +## SelfTest Subsystem -## Overview +### Overview The SelfTest subsystem provides built-in verification of the tool's core functionality and safe path construction for use within that verification. It consists of two units: @@ -11,27 +11,27 @@ The validation subsystem is invoked when the `--validate` flag is passed and can results to a TRX or JUnit XML file when `--results` is also provided. This satisfies requirements `VersionMark-CommandLine-Validate` and `VersionMark-CommandLine-Results`. -## Units +### Units -### Validation +#### Validation The `Validation` class (`Validation.cs`) is the self-validation test runner. It exposes a single public method, `Run`, which orchestrates all internal self-tests against the tool's core modes (capture, publish, lint), collects results, prints a summary, and optionally writes a structured results file. -See [validation.md](validation.md) for the full unit design. +See *Validation Unit Design* for the full unit design. -### PathHelpers +#### PathHelpers The `PathHelpers` class (`PathHelpers.cs`) provides a single static method, `SafePathCombine`, used internally by `Validation` when constructing paths inside temporary directories. It protects against path-traversal attacks by ensuring the resolved combined path stays within the intended base directory. -See [path-helpers.md](path-helpers.md) for the full unit design. +See *PathHelpers Unit Design* for the full unit design. -## Subsystem Interactions +### Subsystem Interactions `Validation.Run` creates temporary directories via the private `TemporaryDirectory` helper class and uses `PathHelpers.SafePathCombine` for all path construction within those diff --git a/docs/design/version-mark/self-test/path-helpers.md b/docs/design/version-mark/self-test/path-helpers.md index a74ecda..7ff8908 100644 --- a/docs/design/version-mark/self-test/path-helpers.md +++ b/docs/design/version-mark/self-test/path-helpers.md @@ -1,6 +1,6 @@ -# PathHelpers Unit +### PathHelpers Unit -## Overview +#### Overview `PathHelpers` is a static utility class that provides a safe path-combination method. It protects callers against path-traversal attacks by verifying the resolved combined path stays @@ -8,7 +8,7 @@ within the base directory. Note that `Path.GetFullPath` normalizes `.`/`..` segm not resolve symlinks or reparse points, so this check guards against string-level traversal only. -## SafePathCombine Method +#### SafePathCombine Method ```csharp internal static string SafePathCombine(string basePath, string relativePath) @@ -28,7 +28,7 @@ the base directory. or `Path.AltDirectorySeparatorChar`, or is itself rooted (absolute), which would indicate the combined path escapes the base directory. -## Design Decisions +#### Design Decisions - **`Path.GetRelativePath` for containment check**: Using `GetRelativePath` to verify containment handles root paths (e.g. `/`, `C:\`), platform case-sensitivity, and diff --git a/docs/design/version-mark/self-test/validation.md b/docs/design/version-mark/self-test/validation.md index 7feb4ba..b5d27a1 100644 --- a/docs/design/version-mark/self-test/validation.md +++ b/docs/design/version-mark/self-test/validation.md @@ -1,11 +1,11 @@ -# Validation Unit +### Validation Unit -## Overview +#### Overview The `Validation` class (`Validation.cs`) exposes a single public method, `Run`, and organizes all test execution internally. -## Run Method +#### Run Method `Run` orchestrates the self-validation sequence: @@ -19,7 +19,7 @@ organizes all test execution internally. failed count if any tests failed (which also sets the process exit code to 1). 5. If `context.ResultsFile` is set, calls `WriteResultsFile` to persist the results. -## RunCaptureTest +#### RunCaptureTest `RunCaptureTest` verifies the capture mode end-to-end: @@ -33,7 +33,7 @@ organizes all test execution internally. The test name is `VersionMark_CapturesVersions`, satisfying `VersionMark-Validation-Capture`. -## RunPublishTest +#### RunPublishTest `RunPublishTest` verifies the publish mode end-to-end: @@ -47,7 +47,7 @@ The test name is `VersionMark_CapturesVersions`, satisfying `VersionMark-Validat The test name is `VersionMark_GeneratesMarkdownReport`, satisfying `VersionMark-Validation-Publish`. -## RunLintValidTest +#### RunLintValidTest `RunLintValidTest` verifies that lint mode exits successfully for a valid configuration file: @@ -59,7 +59,7 @@ The test name is `VersionMark_GeneratesMarkdownReport`, satisfying `VersionMark- The test name is `VersionMark_LintPassesForValidConfig`, satisfying `VersionMark-Validation-Lint`. -## RunLintInvalidTest +#### RunLintInvalidTest `RunLintInvalidTest` verifies that lint mode reports errors for an invalid configuration file: @@ -72,7 +72,7 @@ The test name is `VersionMark_LintPassesForValidConfig`, satisfying `VersionMark The test name is `VersionMark_LintReportsErrorsForInvalidConfig`, satisfying `VersionMark-Validation-Lint`. -## WriteResultsFile +#### WriteResultsFile `WriteResultsFile` inspects the file extension of `context.ResultsFile`: @@ -86,7 +86,7 @@ in the future. This satisfies requirement `VersionMark-Validation-WriteResults`. -## TemporaryDirectory +#### TemporaryDirectory `TemporaryDirectory` is a private nested class that implements `IDisposable`. It creates a uniquely named directory under `Path.GetTempPath()` using `PathHelpers.SafePathCombine` diff --git a/docs/reqstream/ots/mstest.yaml b/docs/reqstream/ots/xunit.yaml similarity index 73% rename from docs/reqstream/ots/mstest.yaml rename to docs/reqstream/ots/xunit.yaml index d7304cb..b40be69 100644 --- a/docs/reqstream/ots/mstest.yaml +++ b/docs/reqstream/ots/xunit.yaml @@ -1,17 +1,17 @@ --- -# MSTest OTS Software Requirements +# xUnit OTS Software Requirements # -# Requirements for the MSTest testing framework functionality. +# Requirements for the xUnit testing framework functionality. sections: - title: OTS Software Requirements sections: - - title: MSTest Requirements + - title: xUnit Requirements requirements: - - id: VersionMark-OTS-MSTest - title: MSTest shall execute unit tests and report results. + - id: VersionMark-OTS-xUnit + title: xUnit shall execute unit tests and report results. justification: | - MSTest (MSTest.TestFramework and MSTest.TestAdapter) is the unit-testing framework used + xUnit (xunit.v3 and xunit.runner.visualstudio) is the unit-testing framework used by the project. It discovers and runs all test methods and writes TRX result files that feed into coverage reporting and requirements traceability. Passing tests confirm the framework is functioning correctly. diff --git a/docs/reqstream/version-mark/version-mark.yaml b/docs/reqstream/version-mark.yaml similarity index 54% rename from docs/reqstream/version-mark/version-mark.yaml rename to docs/reqstream/version-mark.yaml index b3b296c..bd217df 100644 --- a/docs/reqstream/version-mark/version-mark.yaml +++ b/docs/reqstream/version-mark.yaml @@ -8,6 +8,19 @@ sections: The primary purpose of VersionMark is to record which tool versions were present in each CI/CD job so that version differences across environments can be detected and reported. + children: + - VersionMark-Capture-Capture + - VersionMark-Capture-JobId + - VersionMark-Capture-Output + - VersionMark-Capture-DefaultOutput + - VersionMark-Capture-ToolFilter + - VersionMark-Capture-MultipleTools + - VersionMark-Capture-Config + - VersionMark-Capture-Command + - VersionMark-Capture-JsonOutput + - VersionMark-Capture-Display + - VersionMark-Capture-ConfigError + - VersionMark-Capture-CommandFailure tests: - IntegrationTest_CaptureCommand_CapturesToolVersions @@ -17,6 +30,16 @@ sections: Teams need a human-readable summary of tool versions across all jobs. Publishing to markdown enables the report to be included in release documentation and artifact archives. + children: + - VersionMark-Publish-Publish + - VersionMark-Publish-Report + - VersionMark-Publish-ReportDepth + - VersionMark-Publish-RequireReport + - VersionMark-Publish-GlobPattern + - VersionMark-Publish-Consolidate + - VersionMark-Publish-ConflictReport + - VersionMark-Publish-ConflictDisplay + - VersionMark-Publish-FileError tests: - VersionMark_PublishCommand_GeneratesMarkdownReport @@ -26,6 +49,18 @@ sections: Detecting configuration errors early, before a CI/CD run proceeds, reduces wasted build time and provides precise error locations so users can fix problems quickly. + children: + - VersionMark-Load-FileExistence + - VersionMark-Load-YamlParsing + - VersionMark-Load-ToolsSection + - VersionMark-Load-ToolCommand + - VersionMark-Load-ToolRegex + - VersionMark-Load-RegexValid + - VersionMark-Load-RegexVersion + - VersionMark-Load-OsOverrides + - VersionMark-Load-UnknownKeys + - VersionMark-Load-ErrorLocation + - VersionMark-Load-AllIssues tests: - IntegrationTest_LintFlag_ValidConfig_ReturnsSuccess - IntegrationTest_LintFlag_InvalidConfig_ReturnsError @@ -36,5 +71,10 @@ sections: A built-in self-validation suite lets operators confirm that the installed tool is functioning correctly in the deployment environment without requiring external fixtures or test data. + children: + - VersionMark-Validate-Capture + - VersionMark-Validate-Publish + - VersionMark-Validate-Lint + - VersionMark-Validate-Results tests: - IntegrationTest_ValidateFlag_RunsValidation diff --git a/docs/reqstream/version-mark/capture/capture.yaml b/docs/reqstream/version-mark/capture.yaml similarity index 97% rename from docs/reqstream/version-mark/capture/capture.yaml rename to docs/reqstream/version-mark/capture.yaml index 76b39bc..4f1906a 100644 --- a/docs/reqstream/version-mark/capture/capture.yaml +++ b/docs/reqstream/version-mark/capture.yaml @@ -116,8 +116,9 @@ sections: - Capture_Run_MissingConfig_ReportsError - id: VersionMark-Capture-CommandFailure - title: The tool shall report an error and abort capture when a configured command fails - or produces no regex match. + title: >- + The tool shall abort capture and report an error when a configured command + fails or produces no regex match. justification: | Fail-fast behavior ensures the user is immediately informed of capture failures rather than silently continuing with missing version data, preventing incomplete diff --git a/docs/reqstream/version-mark/cli/cli.yaml b/docs/reqstream/version-mark/cli.yaml similarity index 100% rename from docs/reqstream/version-mark/cli/cli.yaml rename to docs/reqstream/version-mark/cli.yaml diff --git a/docs/reqstream/version-mark/cli/context.yaml b/docs/reqstream/version-mark/cli/context.yaml index bb8ef15..fea651b 100644 --- a/docs/reqstream/version-mark/cli/context.yaml +++ b/docs/reqstream/version-mark/cli/context.yaml @@ -52,8 +52,7 @@ sections: title: The Context.WriteLine method shall write output respecting the silent flag. justification: | Silent mode suppresses console output so automated scripts can run the tool - without cluttering their log. The log file always receives output regardless - of silent mode. + without cluttering their log. tests: - Context_WriteLine_NotSilent_WritesToConsole - Context_WriteLine_Silent_DoesNotWriteToConsole @@ -61,7 +60,8 @@ sections: - id: VersionMark-Context-WriteError title: >- The Context.WriteError method shall write error messages to stderr, - unless silent mode is active. + unless silent mode is active, and shall write error messages to the log file + when one is open. justification: | Silent mode suppresses all console output, including error messages written to stderr. The log file always receives error output regardless of silent mode. diff --git a/docs/reqstream/version-mark/configuration/configuration.yaml b/docs/reqstream/version-mark/configuration.yaml similarity index 100% rename from docs/reqstream/version-mark/configuration/configuration.yaml rename to docs/reqstream/version-mark/configuration.yaml diff --git a/docs/reqstream/version-mark/configuration/load.yaml b/docs/reqstream/version-mark/configuration/load.yaml index cce4fe0..94c76b2 100644 --- a/docs/reqstream/version-mark/configuration/load.yaml +++ b/docs/reqstream/version-mark/configuration/load.yaml @@ -110,7 +110,7 @@ sections: - VersionMarkConfig_Load_IssuesContainFilePath - id: VersionMark-Load-AllIssues - title: The tool shall report all load issues in a single pass without stopping at the first error. + title: The tool shall report all load issues discovered in one invocation without stopping at the first error. justification: | Reporting all issues at once lets users fix everything in one edit cycle rather than repeatedly re-running to discover each subsequent problem. diff --git a/docs/reqstream/version-mark/configuration/version-mark-config.yaml b/docs/reqstream/version-mark/configuration/version-mark-config.yaml index eee3195..98e222d 100644 --- a/docs/reqstream/version-mark/configuration/version-mark-config.yaml +++ b/docs/reqstream/version-mark/configuration/version-mark-config.yaml @@ -21,7 +21,7 @@ sections: - id: VersionMark-VersionMarkConfig-Load title: >- - The VersionMarkConfig.Load method shall perform a full single-pass validation + The VersionMarkConfig.Load method shall perform a full validation of a .versionmark.yaml file and return both the parsed configuration and all discovered issues. justification: | diff --git a/docs/reqstream/version-mark/publishing/publishing.yaml b/docs/reqstream/version-mark/publishing.yaml similarity index 96% rename from docs/reqstream/version-mark/publishing/publishing.yaml rename to docs/reqstream/version-mark/publishing.yaml index 863fdde..2c1b46f 100644 --- a/docs/reqstream/version-mark/publishing/publishing.yaml +++ b/docs/reqstream/version-mark/publishing.yaml @@ -77,8 +77,9 @@ sections: - Publishing_Run_WithGlobPatternMatchingNoFiles_ReportsError - id: VersionMark-Publish-ConflictDisplay - title: The tool shall display one bullet per distinct version with contributing job IDs - when different jobs report different versions for the same tool. + title: >- + The tool shall display per-job version bullets when different jobs report + different versions for the same tool. justification: | Grouping by distinct version with job attribution shows exactly which version each job produced, enabling users to identify and resolve version inconsistencies diff --git a/docs/reqstream/version-mark/self-test/self-test.yaml b/docs/reqstream/version-mark/self-test.yaml similarity index 91% rename from docs/reqstream/version-mark/self-test/self-test.yaml rename to docs/reqstream/version-mark/self-test.yaml index a050c08..220e6be 100644 --- a/docs/reqstream/version-mark/self-test/self-test.yaml +++ b/docs/reqstream/version-mark/self-test.yaml @@ -13,7 +13,6 @@ sections: tags: - validation tests: - - SelfTest_PathHelpers_FindsDllInBaseDirectory_FileExists - SelfTest_Run_WithResultsFlag_WritesResultsFile - id: VersionMark-Validate-Publish @@ -24,7 +23,6 @@ sections: tags: - validation tests: - - SelfTest_PathHelpers_FindsDllInBaseDirectory_FileExists - SelfTest_Run_WithResultsFlag_WritesResultsFile - id: VersionMark-Validate-Lint @@ -37,7 +35,6 @@ sections: tags: - validation tests: - - SelfTest_PathHelpers_FindsDllInBaseDirectory_FileExists - SelfTest_Run_WithResultsFlag_WritesResultsFile - id: VersionMark-Validate-Results diff --git a/docs/requirements_doc/definition.yaml b/docs/requirements_doc/definition.yaml index 0f4ccd2..628b789 100644 --- a/docs/requirements_doc/definition.yaml +++ b/docs/requirements_doc/definition.yaml @@ -5,8 +5,8 @@ resource-path: input-files: - docs/requirements_doc/title.txt - docs/requirements_doc/introduction.md - - docs/requirements_doc/requirements.md - - docs/requirements_doc/justifications.md + - docs/requirements_doc/generated/requirements.md + - docs/requirements_doc/generated/justifications.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/requirements_report/definition.yaml b/docs/requirements_report/definition.yaml index 918a645..9ee62a4 100644 --- a/docs/requirements_report/definition.yaml +++ b/docs/requirements_report/definition.yaml @@ -5,7 +5,7 @@ resource-path: input-files: - docs/requirements_report/title.txt - docs/requirements_report/introduction.md - - docs/requirements_report/trace_matrix.md + - docs/requirements_report/generated/trace_matrix.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index b2a1887..a8e0300 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -593,14 +593,16 @@ tools: If a tool command fails because the tool is not installed: -- VersionMark will report an error for that tool -- The capture will continue for other tools -- The published output will note which tools failed to capture +- VersionMark aborts capture immediately and reports an error to stderr +- No JSON output file is created for that job +- Install the missing tool (or remove it from configuration), then re-run capture ## Version Not Matched If the regex doesn't match the command output: +- VersionMark aborts capture immediately and reports an error to stderr +- No JSON output file is created for that job - Check the actual output of the command manually - Adjust the regex to match the format - Use online regex testers to validate your pattern diff --git a/docs/user_guide/title.txt b/docs/user_guide/title.txt index 28d5716..53d9187 100644 --- a/docs/user_guide/title.txt +++ b/docs/user_guide/title.txt @@ -1,13 +1,13 @@ --- title: VersionMark User Guide -subtitle: A .NET Reference Implementation for CLI Tools +subtitle: User Guide author: DEMA Consulting -description: A .NET Reference Implementation for CLI Tools +description: User guide for the VersionMark .NET tool for capturing and publishing tool version information lang: en-US keywords: - VersionMark - .NET - Command-Line Tool - Documentation - - Best Practices + - Version Capture --- diff --git a/docs/verification/definition.yaml b/docs/verification/definition.yaml new file mode 100644 index 0000000..03019e1 --- /dev/null +++ b/docs/verification/definition.yaml @@ -0,0 +1,43 @@ +--- +resource-path: + - docs/verification + - docs/verification/version-mark + - docs/verification/version-mark/cli + - docs/verification/version-mark/configuration + - docs/verification/version-mark/capture + - docs/verification/version-mark/publishing + - docs/verification/version-mark/self-test + - docs/verification/ots + - docs/template + +input-files: + - docs/verification/title.txt + - docs/verification/introduction.md + - docs/verification/version-mark.md + - docs/verification/version-mark/cli.md + - docs/verification/version-mark/cli/program.md + - docs/verification/version-mark/cli/context.md + - docs/verification/version-mark/configuration.md + - docs/verification/version-mark/configuration/version-mark-config.md + - docs/verification/version-mark/configuration/tool-config.md + - docs/verification/version-mark/configuration/lint-issue.md + - docs/verification/version-mark/capture.md + - docs/verification/version-mark/capture/version-info.md + - docs/verification/version-mark/publishing.md + - docs/verification/version-mark/publishing/markdown-formatter.md + - docs/verification/version-mark/self-test.md + - docs/verification/version-mark/self-test/validation.md + - docs/verification/version-mark/self-test/path-helpers.md + - docs/verification/ots.md + - docs/verification/ots/buildmark.md + - docs/verification/ots/fileassert.md + - docs/verification/ots/xunit.md + - docs/verification/ots/pandoc.md + - docs/verification/ots/reqstream.md + - docs/verification/ots/reviewmark.md + - docs/verification/ots/sarifmark.md + - docs/verification/ots/sonarmark.md + - docs/verification/ots/weasyprint.md +template: template.html +table-of-contents: true +number-sections: true diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md new file mode 100644 index 0000000..f0eb164 --- /dev/null +++ b/docs/verification/introduction.md @@ -0,0 +1,90 @@ +# Introduction + +This document describes the verification design for the VersionMark .NET tool. It defines +how each requirement is verified through named test scenarios, providing traceability from +requirements to tests for regulatory review. + +## Purpose + +The purpose of this document is to: + +- Define the verification approach for all in-house VersionMark software items +- Map every requirement to at least one named test scenario +- Identify what is tested at each level (system, subsystem, unit) and how +- Provide OTS verification evidence for all third-party components used +- Enable reviewers to confirm test completeness without reading implementation code + +## Scope + +This document covers verification of five in-house subsystems within the VersionMark system: + +- The **Cli Subsystem**: argument parsing and program dispatch via `Program` and `Context` +- The **Configuration Subsystem**: YAML configuration loading and validation via + `VersionMarkConfig`, `ToolConfig`, and `LintIssue` +- The **Capture Subsystem**: tool version capture and JSON serialization via `VersionInfo` +- The **Publishing Subsystem**: markdown report generation via `MarkdownFormatter` +- The **SelfTest Subsystem**: built-in self-validation via `Validation` and `PathHelpers` + +It also covers verification evidence for the following Off-The-Shelf (OTS) components: + +- **BuildMark** - build notes generation tool +- **FileAssert** - file content assertion tool +- **xUnit** - unit testing framework +- **Pandoc** - document conversion tool +- **ReqStream** - requirements traceability tool +- **ReviewMark** - code review enforcement tool +- **SarifMark** - SARIF report tool +- **SonarMark** - SonarCloud report tool +- **WeasyPrint** - HTML-to-PDF conversion tool + +This document does not cover installation, end-user usage patterns, or CI/CD pipeline +configuration. Those topics are addressed in the VersionMark User Guide and the +VersionMark Requirements documents. + +## Software Structure + +The following tree shows how the VersionMark software items are organized across the +system, subsystem, and unit levels: + +```text +VersionMark (System) +├── Cli (Subsystem) +│ ├── Program (Unit) +│ └── Context (Unit) +├── Configuration (Subsystem) +│ ├── VersionMarkConfig (Unit) +│ ├── ToolConfig (Unit) +│ └── LintIssue (Unit) +├── Capture (Subsystem) +│ └── VersionInfo (Unit) +├── Publishing (Subsystem) +│ └── MarkdownFormatter (Unit) +└── SelfTest (Subsystem) + ├── Validation (Unit) + └── PathHelpers (Unit) +``` + +## Companion Artifact Structure + +In-house items have parallel artifacts in the following locations: + +- Requirements: `docs/reqstream/version-mark.yaml`, + `docs/reqstream/version-mark/{subsystem}/{item}.yaml` +- Design: `docs/design/version-mark.md`, + `docs/design/version-mark/{subsystem}/{item}.md` +- Verification: `docs/verification/version-mark.md`, + `docs/verification/version-mark/{subsystem}/{item}.md` +- Source: `src/DemaConsulting.VersionMark/{Subsystem}/{Unit}.cs` +- Tests: `test/DemaConsulting.VersionMark.Tests/{Subsystem}/{Unit}Tests.cs` + +OTS items (no design documentation) have artifacts in these locations: + +- Requirements: `docs/reqstream/ots/{ots-name}.yaml` +- Verification: `docs/verification/ots/{ots-name}.md` + +Review-sets are defined in `.reviewmark.yaml`. + +## References + +- [REF-1] VersionMark Software Design Document, DEMA Consulting +- [REF-2] VersionMark Requirements Document, DEMA Consulting diff --git a/docs/verification/ots.md b/docs/verification/ots.md new file mode 100644 index 0000000..92b91ad --- /dev/null +++ b/docs/verification/ots.md @@ -0,0 +1,25 @@ +# Off-The-Shelf Component Verification + +## Overview + +This section documents the verification evidence for each Off-The-Shelf (OTS) component +used by VersionMark. OTS components are third-party tools and libraries that are not +developed in-house. Their verification relies on the vendor's own quality assurance and, +where available, self-validation mechanisms provided by the tool itself. + +For OTS tools that include a `--validate` flag, VersionMark's CI pipeline runs +self-validation and captures the results as TRX artifacts. For tools without a built-in +self-validation mode, verification is based on functional evidence produced during the +build and document generation pipeline. + +The following OTS components are covered in this section: + +- **BuildMark** - build notes generation tool +- **FileAssert** - file content assertion tool +- **xUnit** - unit testing framework +- **Pandoc** - document conversion tool +- **ReqStream** - requirements traceability tool +- **ReviewMark** - code review enforcement tool +- **SarifMark** - SARIF report tool +- **SonarMark** - SonarCloud report tool +- **WeasyPrint** - HTML-to-PDF conversion tool diff --git a/docs/verification/ots/buildmark.md b/docs/verification/ots/buildmark.md new file mode 100644 index 0000000..915d739 --- /dev/null +++ b/docs/verification/ots/buildmark.md @@ -0,0 +1,23 @@ +## BuildMark Verification + +### Overview + +BuildMark is an OTS tool developed by DEMA Consulting that generates build notes +documenting which versions of tools were used during a build. VersionMark uses BuildMark +in its CI/CD pipeline to capture and publish build notes as part of the compliance +evidence package. + +### Verification Approach + +BuildMark is verified through its built-in self-validation mechanism. The CI pipeline +runs `dotnet buildmark --validate --results artifacts/buildmark-self-validation.trx`, +which executes BuildMark's internal test suite and writes results to a TRX file. The +presence of a passing TRX file serves as evidence that BuildMark is functioning correctly +in the CI environment. + +### Requirements Coverage + +The following list maps BuildMark requirements to verification evidence: + +- **`VersionMark-OTS-BuildMark`**: `artifacts/buildmark-self-validation.trx` + (BuildMark self-validation passing in CI) diff --git a/docs/verification/ots/fileassert.md b/docs/verification/ots/fileassert.md new file mode 100644 index 0000000..2550828 --- /dev/null +++ b/docs/verification/ots/fileassert.md @@ -0,0 +1,28 @@ +## FileAssert Verification + +### Overview + +FileAssert is an OTS tool developed by DEMA Consulting that asserts the content of +generated files against expected patterns. VersionMark uses FileAssert in its CI/CD +pipeline to verify that generated documents (HTML, PDF, and other output types) are +produced correctly and contain the expected content. + +### Verification Approach + +FileAssert is verified through two mechanisms: + +1. **Self-validation**: The CI pipeline runs + `dotnet fileassert --validate --results artifacts/fileassert-self-validation.trx`, + which executes FileAssert's internal test suite and writes results to a TRX file. + +2. **Functional evidence**: FileAssert is exercised in CI by asserting the content + of each of the seven generated document types (requirements report, design document, + verification document, and others). A passing CI run with FileAssert assertions + provides functional evidence that FileAssert is operating correctly. + +### Requirements Coverage + +The following list maps FileAssert requirements to verification evidence: + +- **`VersionMark-OTS-FileAssert`**: `artifacts/fileassert-self-validation.trx` + (FileAssert self-validation passing in CI) and FileAssert assertions on generated documents diff --git a/docs/verification/ots/pandoc.md b/docs/verification/ots/pandoc.md new file mode 100644 index 0000000..450eab7 --- /dev/null +++ b/docs/verification/ots/pandoc.md @@ -0,0 +1,22 @@ +## Pandoc Verification + +### Overview + +Pandoc is an OTS document conversion tool used in the VersionMark CI/CD pipeline to +compile the multiple markdown input files for each document collection into a single HTML +output file. It processes `definition.yaml` files to determine input ordering, template, +and table-of-contents settings. + +### Verification Approach + +Pandoc is verified through functional evidence. The CI pipeline generates seven document +types (requirements report, design document, verification document, user guide, build +notes, code quality report, and code review report) using Pandoc. FileAssert then asserts +the content of each generated document. A passing CI run with all FileAssert assertions +provides evidence that Pandoc is converting documents correctly. + +### Requirements Coverage + +The following list maps Pandoc requirements to verification evidence: + +- **`VersionMark-OTS-Pandoc`**: FileAssert TRX evidence covering seven generated document types in CI diff --git a/docs/verification/ots/reqstream.md b/docs/verification/ots/reqstream.md new file mode 100644 index 0000000..5f84bfa --- /dev/null +++ b/docs/verification/ots/reqstream.md @@ -0,0 +1,23 @@ +## ReqStream Verification + +### Overview + +ReqStream is an OTS requirements traceability tool developed by DEMA Consulting. It +processes requirements YAML files and generates traceability reports showing which +requirements are covered by which tests. VersionMark uses ReqStream to generate the +requirements traceability report as part of the compliance evidence package. + +### Verification Approach + +ReqStream is verified through its built-in self-validation mechanism. The CI pipeline +runs `dotnet reqstream --validate --results artifacts/reqstream-self-validation.trx`, +which executes ReqStream's internal test suite and writes results to a TRX file. A +passing TRX file serves as evidence that ReqStream is functioning correctly in the CI +environment. + +### Requirements Coverage + +The following list maps ReqStream requirements to verification evidence: + +- **`VersionMark-OTS-ReqStream`**: `artifacts/reqstream-self-validation.trx` + (ReqStream self-validation passing in CI) diff --git a/docs/verification/ots/reviewmark.md b/docs/verification/ots/reviewmark.md new file mode 100644 index 0000000..4046209 --- /dev/null +++ b/docs/verification/ots/reviewmark.md @@ -0,0 +1,23 @@ +## ReviewMark Verification + +### Overview + +ReviewMark is an OTS code review enforcement tool developed by DEMA Consulting. It tracks +the review status of files using a `.reviewmark.yaml` configuration and generates code +review reports. VersionMark uses ReviewMark to enforce formal review coverage across all +source, documentation, and requirements files. + +### Verification Approach + +ReviewMark is verified through its built-in self-validation mechanism. The CI pipeline +runs `dotnet reviewmark --validate --results artifacts/reviewmark-self-validation.trx`, +which executes ReviewMark's internal test suite and writes results to a TRX file. A +passing TRX file serves as evidence that ReviewMark is functioning correctly in the CI +environment. + +### Requirements Coverage + +The following list maps ReviewMark requirements to verification evidence: + +- **`VersionMark-OTS-ReviewMark`**: `artifacts/reviewmark-self-validation.trx` + (ReviewMark self-validation passing in CI) diff --git a/docs/verification/ots/sarifmark.md b/docs/verification/ots/sarifmark.md new file mode 100644 index 0000000..7f5cddf --- /dev/null +++ b/docs/verification/ots/sarifmark.md @@ -0,0 +1,23 @@ +## SarifMark Verification + +### Overview + +SarifMark is an OTS SARIF report tool developed by DEMA Consulting. It converts SARIF +(Static Analysis Results Interchange Format) files produced by code analysis tools into +human-readable markdown reports. VersionMark uses SarifMark in its CI/CD pipeline to +generate code quality reports from static analysis output. + +### Verification Approach + +SarifMark is verified through its built-in self-validation mechanism. The CI pipeline +runs `dotnet sarifmark --validate --results artifacts/sarifmark-self-validation.trx`, +which executes SarifMark's internal test suite and writes results to a TRX file. A +passing TRX file serves as evidence that SarifMark is functioning correctly in the CI +environment. + +### Requirements Coverage + +The following list maps SarifMark requirements to verification evidence: + +- **`VersionMark-OTS-SarifMark`**: `artifacts/sarifmark-self-validation.trx` + (SarifMark self-validation passing in CI) diff --git a/docs/verification/ots/sonarmark.md b/docs/verification/ots/sonarmark.md new file mode 100644 index 0000000..237dd23 --- /dev/null +++ b/docs/verification/ots/sonarmark.md @@ -0,0 +1,23 @@ +## SonarMark Verification + +### Overview + +SonarMark is an OTS SonarCloud report tool developed by DEMA Consulting. It retrieves +code quality metrics from SonarCloud and generates summary reports. VersionMark uses +SonarMark in its CI/CD pipeline to include SonarCloud quality gate results in the +compliance evidence package. + +### Verification Approach + +SonarMark is verified through its built-in self-validation mechanism. The CI pipeline +runs `dotnet sonarmark --validate --results artifacts/sonarmark-self-validation.trx`, +which executes SonarMark's internal test suite and writes results to a TRX file. A +passing TRX file serves as evidence that SonarMark is functioning correctly in the CI +environment. + +### Requirements Coverage + +The following list maps SonarMark requirements to verification evidence: + +- **`VersionMark-OTS-SonarMark`**: `artifacts/sonarmark-self-validation.trx` + (SonarMark self-validation passing in CI) diff --git a/docs/verification/ots/weasyprint.md b/docs/verification/ots/weasyprint.md new file mode 100644 index 0000000..fa6da61 --- /dev/null +++ b/docs/verification/ots/weasyprint.md @@ -0,0 +1,21 @@ +## WeasyPrint Verification + +### Overview + +WeasyPrint is an OTS HTML-to-PDF conversion tool. VersionMark uses WeasyPrint in its +CI/CD pipeline to convert the HTML documents generated by Pandoc into PDF format for +distribution and archiving as compliance evidence. + +### Verification Approach + +WeasyPrint is verified through functional evidence. The CI pipeline generates seven +document types using Pandoc followed by WeasyPrint to produce PDF output. FileAssert then +asserts the content and structure of each generated document. A passing CI run with all +FileAssert assertions provides evidence that WeasyPrint is converting documents correctly. + +### Requirements Coverage + +The following list maps WeasyPrint requirements to verification evidence: + +- **`VersionMark-OTS-WeasyPrint`**: FileAssert TRX evidence covering seven generated PDF + document types in CI diff --git a/docs/verification/ots/xunit.md b/docs/verification/ots/xunit.md new file mode 100644 index 0000000..140ed9b --- /dev/null +++ b/docs/verification/ots/xunit.md @@ -0,0 +1,30 @@ +## xUnit Verification + +### Overview + +xUnit is the OTS unit testing framework used by the VersionMark test project +`DemaConsulting.VersionMark.Tests`. It provides the test runner, assertion library, and +TRX results output used throughout this verification design document. + +### Verification Approach + +xUnit is verified through execution of the VersionMark test suite. The CI pipeline runs: + +```text +dotnet test --no-build --configuration Release + --collect "XPlat Code Coverage;Format=opencover" + --logger "trx;LogFilePrefix=" + --results-directory artifacts +``` + +This command is executed across multiple operating system and .NET version combinations +in the CI matrix (Windows, Linux, macOS against .NET 8, .NET 9, and .NET 10). A passing +test run on each combination provides evidence that xUnit is functioning correctly in +each environment. The resulting TRX files are collected as CI artifacts. + +### Requirements Coverage + +The following list maps xUnit requirements to verification evidence: + +- **`VersionMark-OTS-xUnit`**: TRX result files from `dotnet test` across all CI matrix + OS/dotnet combinations diff --git a/docs/verification/title.txt b/docs/verification/title.txt new file mode 100644 index 0000000..bf3cce9 --- /dev/null +++ b/docs/verification/title.txt @@ -0,0 +1,13 @@ +--- +title: VersionMark Verification Design Document +subtitle: Tool Version Capture and Publication Tool +author: DEMA Consulting +description: Verification design document for VersionMark +lang: en-US +keywords: + - VersionMark + - .NET + - Command-Line Tool + - Verification + - Verification Design Document +--- diff --git a/docs/verification/version-mark.md b/docs/verification/version-mark.md new file mode 100644 index 0000000..162699b --- /dev/null +++ b/docs/verification/version-mark.md @@ -0,0 +1,40 @@ +# VersionMark System Verification + +## Overview + +This section documents the verification design for the VersionMark system. VersionMark is +a .NET global tool that captures tool version information from CI/CD job environments and +publishes consolidated version reports as markdown. + +The verification strategy is organized around five subsystems: + +- **Cli** - command-line argument parsing and program dispatch +- **Configuration** - YAML configuration loading and validation +- **Capture** - tool version capture and JSON serialization +- **Publishing** - markdown report generation +- **SelfTest** - built-in self-validation + +## Verification Approach + +Each subsystem is verified through a combination of integration tests (at the subsystem +level) and unit tests (at the unit level). All tests are implemented using xUnit and +are located under `test/DemaConsulting.VersionMark.Tests/`. + +Tests are executed using `dotnet test` across multiple operating systems (Windows, Linux, +macOS) and multiple .NET versions (8, 9, 10). Each test run produces a TRX results file +which serves as compliance evidence. + +The built-in `--validate` mode exercises capture, publish, and lint workflows end-to-end +and produces a results file that can be used as post-deployment verification evidence. + +## System-Level Test Environments + +System-level verification is performed in the GitHub Actions CI/CD environment. Each matrix +job runs on a specific platform and .NET version combination, producing named TRX result +files. The file naming convention (`artifacts/validation-{os}-{dotnet}.trx`) and test names +provide the platform linkage used by ReqStream filters. + +## Requirements Coverage Summary + +The subsystem chapters that follow provide detailed test-scenario-to-requirement mappings. +Each requirement at every level is covered by at least one named test scenario. diff --git a/docs/verification/version-mark/capture.md b/docs/verification/version-mark/capture.md new file mode 100644 index 0000000..6b59dfa --- /dev/null +++ b/docs/verification/version-mark/capture.md @@ -0,0 +1,57 @@ +## Capture Subsystem Verification + +### Overview + +The Capture subsystem is responsible for executing tool version commands, extracting +version strings, and serializing the results to JSON. It consists of one unit: +`VersionInfo` (the JSON version data record). + +Subsystem-level integration tests are in `Capture/CaptureTests.cs` and cover the full +capture workflow including configuration loading, command execution, output file writing, +and loading the saved data. Unit-level verification for `VersionInfo` is in the chapter +that follows. + +### Verification Approach + +Integration tests use a temporary directory containing a `.versionmark.yaml` configuration +file. Tests invoke capture operations and assert on the written JSON files and displayed +output. No external API mocks are required. + +### Test Scenarios + +The following integration test scenarios verify Capture subsystem requirements: + +- **`Capture_Context_CaptureFlag_SetsCaptureMode`**: `--capture` sets capture mode in context. +- **`Capture_Context_WithJobId_SetsJobId`**: `--job-id` sets the job ID in context. +- **`Capture_Run_NoOutputFlagSpecified_UsesDefaultFilename`**: Default output filename is derived from job ID. +- **`Capture_Context_WithToolFilter_SetsToolNames`**: Tool filter patterns after `--` are captured. +- **`Capture_Run_NoToolFilter_CapturesAllConfiguredTools`**: No tool filter captures all configured tools. +- **`Capture_Config_ReadFromFile_LoadsToolDefinitions`**: Config file loads all tool definitions. +- **`Capture_FindVersions_ExecutesCommandAndExtractsVersion`**: Command is executed and version extracted via regex. +- **`Capture_Run_DisplaysCapturedVersionsAfterCapture`**: Captured versions are displayed after capture. +- **`Capture_Run_MissingConfig_ReportsError`**: Missing config file reports an error. +- **`Capture_SaveAndLoad_PreservesAllVersionData`**: Save and load cycle preserves all version data. +- **`Capture_MultipleCaptures_EachFileHasDistinctJobId`**: Multiple capture files each have a distinct job ID. + +### Dependencies + +No external mocks are required. Tests use temporary directories and configuration files +created during test setup. + +### Requirements Coverage + +The following list maps Capture subsystem requirements to test scenarios: + +- **`VersionMark-Capture-Capture`**: `Capture_Context_CaptureFlag_SetsCaptureMode` +- **`VersionMark-Capture-JobId`**: `Capture_Context_WithJobId_SetsJobId` +- **`VersionMark-Capture-Output`**: `Capture_SaveAndLoad_PreservesAllVersionData` +- **`VersionMark-Capture-DefaultOutput`**: `Capture_Run_NoOutputFlagSpecified_UsesDefaultFilename` +- **`VersionMark-Capture-ToolFilter`**: `Capture_Context_WithToolFilter_SetsToolNames` +- **`VersionMark-Capture-MultipleTools`**: `Capture_Run_NoToolFilter_CapturesAllConfiguredTools` +- **`VersionMark-Capture-Config`**: `Capture_Config_ReadFromFile_LoadsToolDefinitions` +- **`VersionMark-Capture-Command`**: `Capture_FindVersions_ExecutesCommandAndExtractsVersion` +- **`VersionMark-Capture-JsonOutput`**: `Capture_SaveAndLoad_PreservesAllVersionData`, + `Capture_MultipleCaptures_EachFileHasDistinctJobId` +- **`VersionMark-Capture-Display`**: `Capture_Run_DisplaysCapturedVersionsAfterCapture` +- **`VersionMark-Capture-ConfigError`**: `Capture_Run_MissingConfig_ReportsError` +- **`VersionMark-Capture-CommandFailure`**: `Capture_FindVersions_ExecutesCommandAndExtractsVersion` diff --git a/docs/verification/version-mark/capture/version-info.md b/docs/verification/version-mark/capture/version-info.md new file mode 100644 index 0000000..bc4072d --- /dev/null +++ b/docs/verification/version-mark/capture/version-info.md @@ -0,0 +1,44 @@ +### VersionInfo Unit Verification + +#### Overview + +The `VersionInfo` unit is a JSON-serializable record that holds captured version data for +a single CI/CD job. It provides `SaveToFile` to write the record to a JSON file and +`LoadFromFile` to read it back. Tests are in `Capture/VersionInfoTests.cs`. + +#### Test Scenarios + +The following test scenarios verify `VersionInfo`: + +- **`VersionInfo_SaveToFile_CreatesJsonFile`**: `SaveToFile` creates a JSON file at the specified path. +- **`VersionInfo_SaveAndLoad_RoundTripPreservesData`**: Save followed by load preserves all version entries. +- **`VersionInfo_EmptyVersions_SavesAndLoadsCorrectly`**: An empty versions dictionary saves and loads correctly. +- **`VersionInfo_SpecialCharacters_SavesAndLoadsCorrectly`**: Version strings with special characters are preserved. +- **`VersionInfo_LoadFromFile_ReadsJsonFile`**: `LoadFromFile` reads a pre-existing JSON file correctly. +- **`VersionInfo_LoadFromFile_NonExistentFile_ThrowsArgumentException`**: Non-existent file throws ArgumentException. +- **`VersionInfo_LoadFromFile_InvalidJson_ThrowsArgumentException`**: Invalid JSON content throws ArgumentException. +- **`VersionInfo_LoadFromFile_EmptyJson_ThrowsArgumentException`**: Empty JSON file throws ArgumentException. +- **`VersionInfo_LoadFromFile_NullJson_ThrowsArgumentException`**: JSON containing only null throws ArgumentException. +- **`VersionInfo_SaveToFile_InvalidPath_ThrowsInvalidOperationException`**: Invalid file path throws InvalidOperationException. + +#### Dependencies + +No external mocks are required. Tests read and write temporary JSON files. + +#### Requirements Coverage + +The following list maps `VersionInfo` unit requirements to test scenarios: + +- **`VersionMark-VersionInfo-Save`**: `VersionInfo_SaveToFile_CreatesJsonFile`, + `VersionInfo_SaveAndLoad_RoundTripPreservesData`, + `VersionInfo_EmptyVersions_SavesAndLoadsCorrectly`, + `VersionInfo_SpecialCharacters_SavesAndLoadsCorrectly` +- **`VersionMark-VersionInfo-Load`**: `VersionInfo_LoadFromFile_ReadsJsonFile`, + `VersionInfo_SaveAndLoad_RoundTripPreservesData`, + `VersionInfo_EmptyVersions_SavesAndLoadsCorrectly`, + `VersionInfo_SpecialCharacters_SavesAndLoadsCorrectly` +- **`VersionMark-VersionInfo-Error`**: `VersionInfo_LoadFromFile_NonExistentFile_ThrowsArgumentException`, + `VersionInfo_LoadFromFile_InvalidJson_ThrowsArgumentException`, + `VersionInfo_LoadFromFile_EmptyJson_ThrowsArgumentException`, + `VersionInfo_LoadFromFile_NullJson_ThrowsArgumentException`, + `VersionInfo_SaveToFile_InvalidPath_ThrowsInvalidOperationException` diff --git a/docs/verification/version-mark/cli.md b/docs/verification/version-mark/cli.md new file mode 100644 index 0000000..d96972b --- /dev/null +++ b/docs/verification/version-mark/cli.md @@ -0,0 +1,52 @@ +## Cli Subsystem Verification + +### Overview + +The Cli subsystem is responsible for parsing command-line arguments, routing program flow +to the appropriate subsystem, and managing all output (console, error, and log file). It +consists of two units: `Program` (the entry point) and `Context` (the argument and output +container). + +Subsystem-level integration tests are in `CliTests.cs` and cover the full CLI pipeline +with `Context` and `Program` working together. Unit-level verification for `Program` and +`Context` is in the chapters that follow. + +### Verification Approach + +Integration tests construct a `Context` with specific arguments and call `Program.Run`, +then assert on console output and exit code. `StringWriter` captures console output +without external mocks or file system interaction. + +### Test Scenarios + +The following integration test scenarios verify Cli subsystem requirements: + +- **`Cli_Run_VersionFlag_ExitsCleanly`**: Context with `--version`; Program.Run exits with code 0. +- **`Cli_Run_SilentWithVersionFlag_SuppressesOutput`**: Context with `--silent --version`; output is suppressed. +- **`Cli_Run_HelpFlag_DisplaysUsageInformation`**: `--help` shows usage; exit code 0. +- **`Cli_Run_ValidateFlag_RunsValidation`**: `--validate --silent` exits 0. +- **`Cli_Run_InvalidArgs_ThrowsArgumentException`**: `--unknown-flag-xyz` throws ArgumentException. +- **`Cli_Run_LintFlag_ValidConfig_Succeeds`**: `--lint ` on a valid config exits 0. +- **`Cli_Run_ResultsFlag_WritesResultsFile`**: `--validate --results ` creates a TRX file. +- **`Cli_Run_LogFlag_WritesOutputToLogFile`**: `--version --log ` writes to log. + +### Dependencies + +No external mocks are required. Tests use `StringWriter` to capture console output. + +### Requirements Coverage + +The following list maps Cli subsystem requirements to test scenarios: + +- **`VersionMark-CommandLine-Context`**: `Cli_Run_VersionFlag_ExitsCleanly`, + `Cli_Run_SilentWithVersionFlag_SuppressesOutput` +- **`VersionMark-CommandLine-Version`**: `Cli_Run_VersionFlag_ExitsCleanly` +- **`VersionMark-CommandLine-Help`**: `Cli_Run_HelpFlag_DisplaysUsageInformation` +- **`VersionMark-CommandLine-Silent`**: `Cli_Run_SilentWithVersionFlag_SuppressesOutput` +- **`VersionMark-CommandLine-Validate`**: `Cli_Run_ValidateFlag_RunsValidation` +- **`VersionMark-CommandLine-Results`**: `Cli_Run_ResultsFlag_WritesResultsFile` +- **`VersionMark-CommandLine-Log`**: `Cli_Run_LogFlag_WritesOutputToLogFile` +- **`VersionMark-CommandLine-ErrorOutput`**: `Cli_Run_InvalidArgs_ThrowsArgumentException` +- **`VersionMark-CommandLine-InvalidArgs`**: `Cli_Run_InvalidArgs_ThrowsArgumentException` +- **`VersionMark-CommandLine-ExitCode`**: `Cli_Run_InvalidArgs_ThrowsArgumentException` +- **`VersionMark-CommandLine-Lint`**: `Cli_Run_LintFlag_ValidConfig_Succeeds` diff --git a/docs/verification/version-mark/cli/context.md b/docs/verification/version-mark/cli/context.md new file mode 100644 index 0000000..1a32cd4 --- /dev/null +++ b/docs/verification/version-mark/cli/context.md @@ -0,0 +1,101 @@ +### Context Unit Verification + +#### Overview + +The `Context` unit parses command-line arguments and manages all output routing (console, +error stream, and log file). Unit tests are in `Cli/ContextTests.cs`. Each test +constructs a `Context` via `Context.Create()` with specific argument arrays and asserts on +the resulting property values. `StringWriter` captures console output. + +#### Test Scenarios + +The following test scenarios verify `Context` unit requirements: + +- **`Context_Create_NoArguments_ReturnsDefaultContext`**: No arguments creates a default context with all flags false. +- **`Context_Create_VersionFlag_SetsVersionTrue`**: `--version` sets `Version = true`. +- **`Context_Create_ShortVersionFlag_SetsVersionTrue`**: `-v` sets `Version = true`. +- **`Context_Create_HelpFlag_SetsHelpTrue`**: `--help` sets `Help = true`. +- **`Context_Create_ShortHelpFlag_H_SetsHelpTrue`**: `-h` sets `Help = true`. +- **`Context_Create_ShortHelpFlag_Question_SetsHelpTrue`**: `-?` sets `Help = true`. +- **`Context_Create_SilentFlag_SetsSilentTrue`**: `--silent` sets `Silent = true`. +- **`Context_Create_ValidateFlag_SetsValidateTrue`**: `--validate` sets `Validate = true`. +- **`Context_Create_ResultsFlag_SetsResultsFile`**: `--results file.trx` sets `ResultsFile`. +- **`Context_Create_ResultFlag_SetsResultsFile`**: `--result file.trx` (alias) sets `ResultsFile`. +- **`Context_Create_LogFlag_OpensLogFile`**: `--log file.log` opens a log file writer. +- **`Context_Create_UnknownArgument_ThrowsArgumentException`**: Unknown argument throws ArgumentException. +- **`Context_Create_LogFlag_WithoutValue_ThrowsArgumentException`**: `--log` without a value throws ArgumentException. +- **`Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException`**: `--results` without a value throws ArgumentException. +- **`Context_Create_PublishFlag_SetsPublishTrue`**: `--publish` sets `Publish = true`. +- **`Context_Create_ReportParameter_SetsReportFile`**: `--report file.md` sets `ReportFile`. +- **`Context_Create_ReportDepthParameter_SetsReportDepth`**: `--report-depth 2` sets `ReportDepth` to 2. +- **`Context_Create_NoReportDepth_DefaultsToDepthOne`**: Default `ReportDepth` is 1 when not specified. +- **`Context_Create_ReportDepthZero_ThrowsArgumentException`**: `--report-depth 0` throws ArgumentException. +- **`Context_Create_ReportDepthNegative_ThrowsArgumentException`**: Negative `--report-depth` throws ArgumentException. +- **`Context_Create_ReportDepthSeven_ThrowsArgumentException`**: `--report-depth 7` throws ArgumentException. +- **`Context_Create_DepthParameter_SetsDepth`**: `--depth 2` sets the depth. +- **`Context_Create_NoDepth_DefaultsToOne`**: Default depth is 1 when not specified. +- **`Context_Create_DepthParameter_SetsDefaultReportDepth`**: `--depth` sets the default report depth. +- **`Context_Create_ExplicitReportDepthOverridesDepth`**: Explicit `--report-depth` overrides the value set by `--depth`. +- **`Context_Create_DepthZero_ThrowsArgumentException`**: `--depth 0` throws ArgumentException. +- **`Context_Create_DepthNegative_ThrowsArgumentException`**: Negative `--depth` throws ArgumentException. +- **`Context_Create_DepthSeven_ThrowsArgumentException`**: `--depth 7` throws ArgumentException. +- **`Context_Create_GlobPatternsAfterSeparator_CapturesPatterns`**: + Patterns after `--` separator are captured in GlobPatterns. +- **`Context_Create_PublishWithoutReport_ParsesSuccessfully`**: `--publish` without `--report` parses without error. +- **`Context_Create_NoGlobPatterns_EmptyArray`**: No `--` separator produces an empty GlobPatterns array. +- **`Context_Create_LintFlag_SetsLintTrue`**: `--lint` sets `Lint = true`. +- **`Context_Create_LintFlag_WithFile_SetsLintFile`**: `--lint file.yaml` sets the lint config file path. +- **`Context_Create_LintFlag_FollowedByFlag_DoesNotConsumeFlagAsFile`**: + `--lint --version` does not consume `--version` as the lint file. +- **`Context_Create_CaptureFlag_SetsCaptureTrue`**: `--capture` sets `Capture = true`. +- **`Context_Create_JobIdFlag_SetsJobId`**: `--job-id build-1` sets `JobId`. +- **`Context_Create_OutputFlag_SetsOutputFile`**: `--output file.json` sets `OutputFile`. +- **`Context_WriteLine_NotSilent_WritesToConsole`**: `WriteLine` writes to console when not in silent mode. +- **`Context_WriteLine_Silent_DoesNotWriteToConsole`**: `WriteLine` suppresses output in silent mode. +- **`Context_WriteError_NotSilent_WritesToConsole`**: `WriteError` writes to console when not in silent mode. +- **`Context_WriteError_Silent_DoesNotWriteToConsole`**: `WriteError` suppresses output in silent mode. +- **`Context_WriteError_WritesToLogFile`**: `WriteError` writes the message to the log file. +- **`Context_WriteError_SetsErrorExitCode`**: `WriteError` sets the exit code to 1. + +#### Dependencies + +Tests use `StringWriter` to capture console output. No file system access is required +except for `Context_Create_LogFlag_OpensLogFile`, which uses a temporary log file. + +#### Requirements Coverage + +The following list maps `Context` unit requirements to test scenarios: + +- **`VersionMark-Context-Create`**: `Context_Create_NoArguments_ReturnsDefaultContext`, + `Context_Create_VersionFlag_SetsVersionTrue`, `Context_Create_ShortVersionFlag_SetsVersionTrue`, + `Context_Create_HelpFlag_SetsHelpTrue`, `Context_Create_ShortHelpFlag_H_SetsHelpTrue`, + `Context_Create_ShortHelpFlag_Question_SetsHelpTrue`, `Context_Create_SilentFlag_SetsSilentTrue`, + `Context_Create_ValidateFlag_SetsValidateTrue`, `Context_Create_ResultsFlag_SetsResultsFile`, + `Context_Create_ResultFlag_SetsResultsFile`, `Context_Create_LogFlag_OpensLogFile`, + `Context_Create_UnknownArgument_ThrowsArgumentException`, + `Context_Create_LogFlag_WithoutValue_ThrowsArgumentException`, + `Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException`, + `Context_Create_PublishFlag_SetsPublishTrue`, `Context_Create_ReportParameter_SetsReportFile`, + `Context_Create_ReportDepthParameter_SetsReportDepth`, + `Context_Create_NoReportDepth_DefaultsToDepthOne`, + `Context_Create_GlobPatternsAfterSeparator_CapturesPatterns`, + `Context_Create_PublishWithoutReport_ParsesSuccessfully`, + `Context_Create_NoGlobPatterns_EmptyArray`, `Context_Create_LintFlag_SetsLintTrue`, + `Context_Create_LintFlag_WithFile_SetsLintFile`, + `Context_Create_LintFlag_FollowedByFlag_DoesNotConsumeFlagAsFile`, + `Context_Create_DepthParameter_SetsDepth`, `Context_Create_NoDepth_DefaultsToOne`, + `Context_Create_DepthParameter_SetsDefaultReportDepth`, + `Context_Create_ExplicitReportDepthOverridesDepth`, + `Context_Create_DepthZero_ThrowsArgumentException`, + `Context_Create_DepthNegative_ThrowsArgumentException`, + `Context_Create_DepthSeven_ThrowsArgumentException`, + `Context_Create_ReportDepthZero_ThrowsArgumentException`, + `Context_Create_ReportDepthNegative_ThrowsArgumentException`, + `Context_Create_ReportDepthSeven_ThrowsArgumentException`, + `Context_Create_CaptureFlag_SetsCaptureTrue`, `Context_Create_JobIdFlag_SetsJobId`, + `Context_Create_OutputFlag_SetsOutputFile` +- **`VersionMark-Context-WriteLine`**: `Context_WriteLine_NotSilent_WritesToConsole`, + `Context_WriteLine_Silent_DoesNotWriteToConsole` +- **`VersionMark-Context-WriteError`**: `Context_WriteError_NotSilent_WritesToConsole`, + `Context_WriteError_Silent_DoesNotWriteToConsole`, `Context_WriteError_WritesToLogFile` +- **`VersionMark-Context-WriteErrorExitCode`**: `Context_WriteError_SetsErrorExitCode` diff --git a/docs/verification/version-mark/cli/program.md b/docs/verification/version-mark/cli/program.md new file mode 100644 index 0000000..842fc05 --- /dev/null +++ b/docs/verification/version-mark/cli/program.md @@ -0,0 +1,66 @@ +### Program Unit Verification + +#### Overview + +The `Program` unit is the entry point for the VersionMark tool. It dispatches to the +appropriate subsystem based on parsed context flags. Unit tests are in `ProgramTests.cs`. +Each test constructs a `Context` with specific arguments and calls `Program.Run`, then +asserts on console output and exit code. `StringWriter` captures console output. + +#### Test Scenarios + +The following test scenarios verify `Program` unit requirements: + +- **`Program_Version_ReturnsNonEmptyString`**: `Program.Version` returns a non-empty version string. +- **`Program_Run_WithVersionFlag_DisplaysVersionOnly`**: `--version` writes version only (no copyright). +- **`Program_Run_WithHelpFlag_DisplaysUsageInformation`**: `--help` writes usage information. +- **`Program_Run_WithValidateFlag_RunsValidation`**: `--validate` writes output containing "Total Tests:". +- **`Program_Run_NoArguments_DisplaysDefaultBehavior`**: No arguments writes banner with version and copyright. +- **`Program_Run_WithCaptureCommand_CapturesToolVersions`**: `--capture --job-id --output -- dotnet` creates output file. +- **`Program_Run_WithCaptureCommandNoToolFilter_CapturesAllConfiguredTools`**: + Capture without tool filter captures all configured tools. +- **`Program_Run_WithCaptureCommandWithoutJobId_ReturnsError`**: + Missing `--job-id` returns a non-zero exit code. +- **`Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError`**: + Missing `.versionmark.yaml` returns a non-zero exit code. +- **`Program_Run_WithPublishCommandWithoutReport_ReturnsError`**: + `--publish` without `--report` returns a non-zero exit code. +- **`Program_Run_WithPublishCommandNoMatchingFiles_ReturnsError`**: Glob matching no files returns a non-zero exit code. +- **`Program_Run_WithPublishCommandInvalidJson_ReturnsError`**: Invalid JSON file returns a non-zero exit code. +- **`Program_Run_WithPublishCommand_GeneratesMarkdownReport`**: Full publish generates a markdown report file. +- **`Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels`**: Custom depth adjusts heading + levels in the report. +- **`Program_Run_WithLintFlag_ValidConfig_ReturnsSuccess`**: Valid config lint returns exit code 0. +- **`Program_Run_WithLintFlag_ValidConfig_SuppressesBanner`**: Lint mode suppresses the banner. +- **`Program_Run_WithLintFlag_InvalidConfig_ReturnsError`**: Invalid config lint reports error + and returns non-zero exit code. +- **`Program_Run_WithLintFlag_NoFile_UsesDefaultConfigFile`**: No file argument uses `.versionmark.yaml` as default. +- **`Program_Run_WithHelpFlag_IncludesLintInformation`**: Help output includes lint command information. + +#### Dependencies + +Tests use `StringWriter` to capture console output. Capture and publish tests use +temporary directories and configuration files created during test setup. + +#### Requirements Coverage + +The following list maps `Program` unit requirements to test scenarios: + +- **`VersionMark-Program-Version`**: `Program_Version_ReturnsNonEmptyString` +- **`VersionMark-Program-Dispatch`**: `Program_Run_WithVersionFlag_DisplaysVersionOnly`, + `Program_Run_WithHelpFlag_DisplaysUsageInformation`, + `Program_Run_WithValidateFlag_RunsValidation`, `Program_Run_NoArguments_DisplaysDefaultBehavior` +- **`VersionMark-Program-RunCapture`**: `Program_Run_WithCaptureCommand_CapturesToolVersions`, + `Program_Run_WithCaptureCommandNoToolFilter_CapturesAllConfiguredTools`, + `Program_Run_WithCaptureCommandWithoutJobId_ReturnsError`, + `Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError` +- **`VersionMark-Program-RunPublish`**: `Program_Run_WithPublishCommandWithoutReport_ReturnsError`, + `Program_Run_WithPublishCommandNoMatchingFiles_ReturnsError`, + `Program_Run_WithPublishCommandInvalidJson_ReturnsError`, + `Program_Run_WithPublishCommand_GeneratesMarkdownReport`, + `Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels` +- **`VersionMark-Program-RunLint`**: `Program_Run_WithLintFlag_ValidConfig_ReturnsSuccess`, + `Program_Run_WithLintFlag_ValidConfig_SuppressesBanner`, + `Program_Run_WithLintFlag_InvalidConfig_ReturnsError`, + `Program_Run_WithLintFlag_NoFile_UsesDefaultConfigFile`, + `Program_Run_WithHelpFlag_IncludesLintInformation` diff --git a/docs/verification/version-mark/configuration.md b/docs/verification/version-mark/configuration.md new file mode 100644 index 0000000..8b2c8e0 --- /dev/null +++ b/docs/verification/version-mark/configuration.md @@ -0,0 +1,47 @@ +## Configuration Subsystem Verification + +### Overview + +The Configuration subsystem is responsible for loading and validating the +`.versionmark.yaml` configuration file. It consists of three units: `VersionMarkConfig` +(the top-level configuration container), `ToolConfig` (per-tool configuration record), and +`LintIssue` (lint issue record and load result). + +Subsystem-level integration tests are in `Configuration/ConfigurationTests.cs` and cover +the full configuration loading workflow via `VersionMarkConfig.ReadFromFile`. Unit-level +verification for each unit is in the chapters that follow. + +### Verification Approach + +Integration tests write temporary YAML files to disk and call `VersionMarkConfig.ReadFromFile`, +then assert on the returned configuration object. No external mocks or stubs are required. + +### Test Scenarios + +The following integration test scenarios verify Configuration subsystem requirements: + +- **`Configuration_ReadFromFile_MultipleTools_AllToolsAccessible`**: Multiple tools all accessible after load. +- **`Configuration_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand`**: OS overrides select correct command. +- **`Configuration_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex`**: OS regex overrides select correct regex. +- **`Configuration_ReadFromFile_EmptyTools_ThrowsArgumentException`**: Empty tools section throws ArgumentException. +- **`Configuration_ReadFromFile_MissingFile_ThrowsArgumentException`**: Missing config file throws ArgumentException. +- **`Configuration_ReadFromFile_InvalidYaml_ThrowsArgumentException`**: Invalid YAML throws ArgumentException. + +### Dependencies + +No external mocks are required. Tests read from temporary YAML files written during the +test setup. + +### Requirements Coverage + +The following list maps Configuration subsystem requirements to test scenarios: + +- **`VersionMark-Configuration-YamlConfig`**: `Configuration_ReadFromFile_MultipleTools_AllToolsAccessible` +- **`VersionMark-Configuration-ToolDefinition`**: `Configuration_ReadFromFile_MultipleTools_AllToolsAccessible` +- **`VersionMark-Configuration-OsCommandOverride`**: + `Configuration_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand` +- **`VersionMark-Configuration-OsRegexOverride`**: + `Configuration_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex` +- **`VersionMark-Configuration-ValidateTools`**: `Configuration_ReadFromFile_EmptyTools_ThrowsArgumentException` +- **`VersionMark-Configuration-ReadError`**: `Configuration_ReadFromFile_MissingFile_ThrowsArgumentException` +- **`VersionMark-Configuration-ParseError`**: `Configuration_ReadFromFile_InvalidYaml_ThrowsArgumentException` diff --git a/docs/verification/version-mark/configuration/lint-issue.md b/docs/verification/version-mark/configuration/lint-issue.md new file mode 100644 index 0000000..bc9a244 --- /dev/null +++ b/docs/verification/version-mark/configuration/lint-issue.md @@ -0,0 +1,35 @@ +### LintIssue Unit Verification + +#### Overview + +The `LintIssue` unit represents a single lint issue produced during configuration +validation. The related `VersionMarkLoadResult` record holds the loaded configuration (or +null on failure) together with all discovered issues. Tests are in +`Configuration/LintIssueTests.cs`. + +#### Test Scenarios + +The following test scenarios verify `LintIssue` and `VersionMarkLoadResult`: + +- **`LintIssue_Constructor_AllFields_AreStoredCorrectly`**: Constructor stores severity, file path, line, column, and message. +- **`LintIssue_ToString_Error_ProducesLowercaseSeverity`**: `ToString` for an error issue includes lowercase "error". +- **`LintIssue_ToString_Warning_ProducesLowercaseSeverity`**: `ToString` for a warning issue includes lowercase "warning". +- **`VersionMarkLoadResult_Constructor_AllFields_AreStoredCorrectly`**: Constructor stores config and issue list correctly. +- **`VersionMarkLoadResult_ReportIssues_Error_WritesToErrorStream`**: Error-severity issues are written to the error stream. +- **`VersionMarkLoadResult_ReportIssues_Warning_WritesToStdOut`**: Warning-severity issues are written to standard output. + +#### Dependencies + +Tests use `StringWriter` to capture console and error stream output. + +#### Requirements Coverage + +The following list maps `LintIssue` and `VersionMarkLoadResult` unit requirements to +test scenarios: + +- **`VersionMark-Load-LintIssue-Fields`**: `LintIssue_Constructor_AllFields_AreStoredCorrectly` +- **`VersionMark-Load-LintIssue-Format`**: `LintIssue_ToString_Error_ProducesLowercaseSeverity`, + `LintIssue_ToString_Warning_ProducesLowercaseSeverity` +- **`VersionMark-Load-VersionMarkLoadResult`**: `VersionMarkLoadResult_Constructor_AllFields_AreStoredCorrectly`, + `VersionMarkLoadResult_ReportIssues_Error_WritesToErrorStream`, + `VersionMarkLoadResult_ReportIssues_Warning_WritesToStdOut` diff --git a/docs/verification/version-mark/configuration/tool-config.md b/docs/verification/version-mark/configuration/tool-config.md new file mode 100644 index 0000000..a60ead7 --- /dev/null +++ b/docs/verification/version-mark/configuration/tool-config.md @@ -0,0 +1,54 @@ +### ToolConfig Unit Verification + +#### Overview + +The `ToolConfig` unit holds the per-tool configuration record, including default command, +default regex, and OS-specific overrides. It provides `GetEffectiveCommand` and +`GetEffectiveRegex` methods that select the appropriate value for the current (or a +specified) operating system. Tests are in `Configuration/VersionMarkConfigTests.cs`. + +#### Test Scenarios — GetEffectiveCommand + +The following test scenarios verify `ToolConfig.GetEffectiveCommand`: + +- **`ToolConfig_GetEffectiveCommand_NoOverride_ReturnsDefaultCommand`**: No OS override returns the default command. +- **`ToolConfig_GetEffectiveCommand_WithExplicitOs_ReturnsCorrectCommand`**: Explicit OS argument returns the matching override. +- **`ToolConfig_GetEffectiveCommand_WindowsOverride_ReturnsWindowsCommand`**: Windows override selected on Windows. +- **`ToolConfig_GetEffectiveCommand_LinuxOverride_ReturnsLinuxCommand`**: Linux override selected on Linux. +- **`ToolConfig_GetEffectiveCommand_MacOsOverride_ReturnsMacOsCommand`**: macOS override selected on macOS. +- **`ToolConfig_GetEffectiveCommand_NoDefaultKey_ThrowsInvalidOperationException`**: + No default key and no matching override throws InvalidOperationException. + +#### Test Scenarios — GetEffectiveRegex + +The following test scenarios verify `ToolConfig.GetEffectiveRegex`: + +- **`ToolConfig_GetEffectiveRegex_NoOverride_ReturnsDefaultRegex`**: No OS override returns the default regex. +- **`ToolConfig_GetEffectiveRegex_WithExplicitOs_ReturnsCorrectRegex`**: Explicit OS argument returns the matching override. +- **`ToolConfig_GetEffectiveRegex_WindowsOverride_ReturnsWindowsRegex`**: Windows override selected on Windows. +- **`ToolConfig_GetEffectiveRegex_LinuxOverride_ReturnsLinuxRegex`**: Linux override selected on Linux. +- **`ToolConfig_GetEffectiveRegex_MacOsOverride_ReturnsMacOsRegex`**: macOS override selected on macOS. +- **`ToolConfig_GetEffectiveRegex_NoDefaultKey_ThrowsInvalidOperationException`**: + No default key and no matching override throws InvalidOperationException. + +#### Dependencies + +No external mocks or file system access is required. Tests call methods directly on +`ToolConfig` instances. + +#### Requirements Coverage + +The following list maps `ToolConfig` unit requirements to test scenarios: + +- **`VersionMark-ToolConfig-EffectiveCommand`**: `ToolConfig_GetEffectiveCommand_NoOverride_ReturnsDefaultCommand`, + `ToolConfig_GetEffectiveCommand_WithExplicitOs_ReturnsCorrectCommand`, + `ToolConfig_GetEffectiveCommand_WindowsOverride_ReturnsWindowsCommand`, + `ToolConfig_GetEffectiveCommand_LinuxOverride_ReturnsLinuxCommand`, + `ToolConfig_GetEffectiveCommand_MacOsOverride_ReturnsMacOsCommand`, + `ToolConfig_GetEffectiveCommand_NoDefaultKey_ThrowsInvalidOperationException` +- **`VersionMark-ToolConfig-EffectiveRegex`**: `ToolConfig_GetEffectiveRegex_NoOverride_ReturnsDefaultRegex`, + `ToolConfig_GetEffectiveRegex_WithExplicitOs_ReturnsCorrectRegex`, + `ToolConfig_GetEffectiveRegex_WindowsOverride_ReturnsWindowsRegex`, + `ToolConfig_GetEffectiveRegex_LinuxOverride_ReturnsLinuxRegex`, + `ToolConfig_GetEffectiveRegex_MacOsOverride_ReturnsMacOsRegex`, + `ToolConfig_GetEffectiveRegex_NoDefaultKey_ThrowsInvalidOperationException` diff --git a/docs/verification/version-mark/configuration/version-mark-config.md b/docs/verification/version-mark/configuration/version-mark-config.md new file mode 100644 index 0000000..7e3ed52 --- /dev/null +++ b/docs/verification/version-mark/configuration/version-mark-config.md @@ -0,0 +1,98 @@ +### VersionMarkConfig Unit Verification + +#### Overview + +The `VersionMarkConfig` unit is the top-level configuration container. It reads and +validates `.versionmark.yaml` files and provides `FindVersions` to execute tool commands +and extract version strings. Tests are split across two files: +`Configuration/VersionMarkConfigTests.cs` (covering `ReadFromFile` and `FindVersions`) +and `Configuration/VersionMarkConfigLoadTests.cs` (covering the `Load` method). + +#### Test Scenarios — ReadFromFile + +The following test scenarios verify `VersionMarkConfig.ReadFromFile`: + +- **`VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig`**: Valid file returns a populated config object. +- **`VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig`**: File with all OS overrides returns config. +- **`VersionMarkConfig_ReadFromFile_NonExistentFile_ThrowsArgumentException`**: Non-existent file throws ArgumentException. +- **`VersionMarkConfig_ReadFromFile_InvalidYaml_ThrowsArgumentException`**: Invalid YAML throws ArgumentException. +- **`VersionMarkConfig_ReadFromFile_NoTools_ThrowsArgumentException`**: No tools section throws ArgumentException. + +#### Test Scenarios — FindVersions + +The following test scenarios verify `VersionMarkConfig.FindVersions`: + +- **`VersionMarkConfig_FindVersions_DotnetCommand_ReturnsVersionInfo`**: `dotnet --version` executes and returns version. +- **`VersionMarkConfig_FindVersions_MultipleTools_ReturnsAllVersions`**: Multiple tools returns all version entries. +- **`VersionMarkConfig_FindVersions_NonExistentTool_ThrowsArgumentException`**: Tool not in config throws ArgumentException. +- **`VersionMarkConfig_FindVersions_InvalidCommand_ThrowsInvalidOperationException`**: + Invalid command throws InvalidOperationException. +- **`VersionMarkConfig_FindVersions_RegexNoMatch_ThrowsInvalidOperationException`**: + No regex match throws InvalidOperationException. +- **`VersionMarkConfig_FindVersions_RegexNoVersionGroup_ThrowsInvalidOperationException`**: + Missing version group throws InvalidOperationException. + +#### Test Scenarios — Load + +The following test scenarios verify `VersionMarkConfig.Load`: + +- **`VersionMarkConfig_Load_ValidConfig_ReturnsConfig`**: Valid config returns a config with no issues. +- **`VersionMarkConfig_Load_MissingFile_ReturnsNullConfig`**: Missing file returns a null config with an issue. +- **`VersionMarkConfig_Load_InvalidYaml_ReturnsNullConfig`**: Invalid YAML returns a null config with an issue. +- **`VersionMarkConfig_Load_MissingToolsSection_ReturnsNullConfig`**: Missing tools section returns a null config. +- **`VersionMarkConfig_Load_EmptyToolsSection_ReturnsNullConfig`**: Empty tools section returns a null config. +- **`VersionMarkConfig_Load_MissingCommand_ReturnsNullConfig`**: Missing command returns a null config with an issue. +- **`VersionMarkConfig_Load_EmptyCommand_ReturnsNullConfig`**: Empty command returns a null config with an issue. +- **`VersionMarkConfig_Load_MissingRegex_ReturnsNullConfig`**: Missing regex returns a null config with an issue. +- **`VersionMarkConfig_Load_EmptyRegex_ReturnsNullConfig`**: Empty regex returns a null config with an issue. +- **`VersionMarkConfig_Load_InvalidRegex_ReturnsNullConfig`**: Invalid regex returns a null config with an issue. +- **`VersionMarkConfig_Load_RegexMissingVersionGroup_ReturnsNullConfig`**: Regex without version group returns a null config. +- **`VersionMarkConfig_Load_UnknownTopLevelKey_ReturnsConfig`**: Unknown top-level key is tolerated; config is returned. +- **`VersionMarkConfig_Load_UnknownToolKey_ReturnsConfig`**: Unknown tool-level key is tolerated; config is returned. +- **`VersionMarkConfig_Load_OsSpecificEmptyCommand_ReturnsNullConfig`**: Empty OS-specific command returns a null config. +- **`VersionMarkConfig_Load_OsSpecificEmptyRegex_ReturnsNullConfig`**: Empty OS-specific regex returns a null config. +- **`VersionMarkConfig_Load_OsSpecificRegexMissingVersionGroup_ReturnsNullConfig`**: + OS-specific regex without version group returns null. +- **`VersionMarkConfig_Load_OsSpecificInvalidRegex_ReturnsNullConfig`**: OS-specific invalid regex returns a null config. +- **`VersionMarkConfig_Load_MultipleErrors_ReportsAll`**: Multiple errors are all reported in the issue list. +- **`VersionMarkConfig_Load_IssuesContainFilePath`**: Issue records include the config file path. + +#### Dependencies + +No external mocks are required. Tests write temporary YAML files to disk. + +#### Requirements Coverage + +The following list maps `VersionMarkConfig` unit requirements to test scenarios: + +- **`VersionMark-VersionMarkConfig-ReadFromFile`**: `VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig`, + `VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig`, + `VersionMarkConfig_ReadFromFile_NonExistentFile_ThrowsArgumentException`, + `VersionMarkConfig_ReadFromFile_InvalidYaml_ThrowsArgumentException`, + `VersionMarkConfig_ReadFromFile_NoTools_ThrowsArgumentException` +- **`VersionMark-VersionMarkConfig-FindVersions`**: `VersionMarkConfig_FindVersions_DotnetCommand_ReturnsVersionInfo`, + `VersionMarkConfig_FindVersions_MultipleTools_ReturnsAllVersions`, + `VersionMarkConfig_FindVersions_NonExistentTool_ThrowsArgumentException`, + `VersionMarkConfig_FindVersions_InvalidCommand_ThrowsInvalidOperationException`, + `VersionMarkConfig_FindVersions_RegexNoMatch_ThrowsInvalidOperationException`, + `VersionMarkConfig_FindVersions_RegexNoVersionGroup_ThrowsInvalidOperationException` +- **`VersionMark-VersionMarkConfig-Load`**: All `VersionMarkConfig_Load_*` test scenarios above +- **`VersionMark-Load-Method`**: `VersionMarkConfig_Load_ValidConfig_ReturnsConfig` +- **`VersionMark-Load-FileExistence`**: `VersionMarkConfig_Load_MissingFile_ReturnsNullConfig` +- **`VersionMark-Load-YamlParsing`**: `VersionMarkConfig_Load_InvalidYaml_ReturnsNullConfig` +- **`VersionMark-Load-ToolsSection`**: `VersionMarkConfig_Load_MissingToolsSection_ReturnsNullConfig`, + `VersionMarkConfig_Load_EmptyToolsSection_ReturnsNullConfig` +- **`VersionMark-Load-ToolCommand`**: `VersionMarkConfig_Load_MissingCommand_ReturnsNullConfig`, + `VersionMarkConfig_Load_EmptyCommand_ReturnsNullConfig` +- **`VersionMark-Load-ToolRegex`**: `VersionMarkConfig_Load_MissingRegex_ReturnsNullConfig`, + `VersionMarkConfig_Load_EmptyRegex_ReturnsNullConfig` +- **`VersionMark-Load-RegexValid`**: `VersionMarkConfig_Load_InvalidRegex_ReturnsNullConfig` +- **`VersionMark-Load-RegexVersion`**: `VersionMarkConfig_Load_RegexMissingVersionGroup_ReturnsNullConfig` +- **`VersionMark-Load-OsOverrides`**: `VersionMarkConfig_Load_OsSpecificEmptyCommand_ReturnsNullConfig`, + `VersionMarkConfig_Load_OsSpecificEmptyRegex_ReturnsNullConfig`, + `VersionMarkConfig_Load_OsSpecificRegexMissingVersionGroup_ReturnsNullConfig`, + `VersionMarkConfig_Load_OsSpecificInvalidRegex_ReturnsNullConfig` +- **`VersionMark-Load-UnknownKeys`**: `VersionMarkConfig_Load_UnknownTopLevelKey_ReturnsConfig`, + `VersionMarkConfig_Load_UnknownToolKey_ReturnsConfig` +- **`VersionMark-Load-ErrorLocation`**: `VersionMarkConfig_Load_IssuesContainFilePath` +- **`VersionMark-Load-AllIssues`**: `VersionMarkConfig_Load_MultipleErrors_ReportsAll` diff --git a/docs/verification/version-mark/publishing.md b/docs/verification/version-mark/publishing.md new file mode 100644 index 0000000..9e9b817 --- /dev/null +++ b/docs/verification/version-mark/publishing.md @@ -0,0 +1,53 @@ +## Publishing Subsystem Verification + +### Overview + +The Publishing subsystem is responsible for reading captured version JSON files and +generating a consolidated markdown report. It consists of one unit: `MarkdownFormatter` +(the version report formatter). + +Subsystem-level integration tests are in `Publishing/PublishingTests.cs` and cover the +full publish workflow including glob pattern resolution, JSON file loading, report +generation, and error handling. Unit-level verification for `MarkdownFormatter` is in the +chapter that follows. + +### Verification Approach + +Integration tests use temporary directories containing pre-built JSON capture files. +Tests invoke publish operations via `Program.RunPublish` and assert on the contents of +the generated report or the error output. No external mocks are required. + +### Test Scenarios + +The following integration test scenarios verify Publishing subsystem requirements: + +- **`Publishing_Format_MultipleCaptureFiles_ProducesConsolidatedReport`**: Multiple files produce a consolidated report. +- **`Publishing_Format_IdenticalVersionsAcrossJobs_ConsolidatesVersions`**: Identical versions across jobs are consolidated. +- **`Publishing_Format_ConflictingVersions_ShowsJobIds`**: Conflicting versions show individual job IDs. +- **`Publishing_Format_WithCustomDepth_UsesCorrectHeadingLevel`**: Custom depth produces the correct heading level. +- **`Publishing_Run_WithoutReport_ReportsError`**: Missing `--report` flag reports an error. +- **`Publishing_Run_WithGlobPattern_ReadsMatchingFiles`**: Glob pattern reads all matching files. +- **`Publishing_Run_WithGlobPatternMatchingNoFiles_ReportsError`**: No matching files reports an error. +- **`Publishing_Run_WithMalformedJsonFile_ReportsError`**: Malformed JSON file reports an error. +- **`Publishing_Run_WithReportDepth_UsesCorrectDepth`**: Report depth flag is applied to heading levels. + +### Dependencies + +No external mocks are required. Tests use temporary directories and pre-built JSON +capture files created during test setup. + +### Requirements Coverage + +The following list maps Publishing subsystem requirements to test scenarios: + +- **`VersionMark-Publish-Publish`**: `Publishing_Format_MultipleCaptureFiles_ProducesConsolidatedReport` +- **`VersionMark-Publish-Report`**: `Publishing_Format_MultipleCaptureFiles_ProducesConsolidatedReport` +- **`VersionMark-Publish-ReportDepth`**: `Publishing_Format_WithCustomDepth_UsesCorrectHeadingLevel`, + `Publishing_Run_WithReportDepth_UsesCorrectDepth` +- **`VersionMark-Publish-RequireReport`**: `Publishing_Run_WithoutReport_ReportsError` +- **`VersionMark-Publish-GlobPattern`**: `Publishing_Run_WithGlobPattern_ReadsMatchingFiles` +- **`VersionMark-Publish-Consolidate`**: `Publishing_Format_MultipleCaptureFiles_ProducesConsolidatedReport`, + `Publishing_Format_IdenticalVersionsAcrossJobs_ConsolidatesVersions` +- **`VersionMark-Publish-ConflictReport`**: `Publishing_Run_WithGlobPatternMatchingNoFiles_ReportsError` +- **`VersionMark-Publish-ConflictDisplay`**: `Publishing_Format_ConflictingVersions_ShowsJobIds` +- **`VersionMark-Publish-FileError`**: `Publishing_Run_WithMalformedJsonFile_ReportsError` diff --git a/docs/verification/version-mark/publishing/markdown-formatter.md b/docs/verification/version-mark/publishing/markdown-formatter.md new file mode 100644 index 0000000..b00fd0c --- /dev/null +++ b/docs/verification/version-mark/publishing/markdown-formatter.md @@ -0,0 +1,60 @@ +### MarkdownFormatter Unit Verification + +#### Overview + +The `MarkdownFormatter` unit generates consolidated markdown version reports from a +collection of `VersionInfo` records. It sorts tools and job IDs alphabetically, collapses +uniform versions across jobs into a single line, and uses the configured heading depth for +section headers. Tests are in `Publishing/MarkdownFormatterTests.cs`. + +#### Test Scenarios + +The following test scenarios verify `MarkdownFormatter`: + +- **`MarkdownFormatter_FormatVersions_SortsToolsAlphabetically`**: Tools appear in alphabetical order in the report. +- **`MarkdownFormatter_FormatVersions_WithUniformVersions_ShowsVersionOnly`**: + Same version across all jobs shows version without job IDs. +- **`MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividualJobs`**: + Different versions across jobs show each job ID and version. +- **`MarkdownFormatter_FormatVersions_WithCustomDepth_UsesCorrectHeadingLevel`**: + Custom depth produces the correct Markdown heading level. +- **`MarkdownFormatter_FormatVersions_EmptyList_ProducesHeaderOnly`**: Empty input list produces a header with no tool entries. +- **`MarkdownFormatter_FormatVersions_SingleJob_ShowsAllJobs`**: Single job shows all version entries. +- **`MarkdownFormatter_FormatVersions_MixedVersions_HandlesCorrectly`**: + Mix of uniform and differing versions is handled correctly. +- **`MarkdownFormatter_FormatVersions_SortsJobIdsAlphabetically`**: Job IDs appear in alphabetical order within a tool entry. +- **`MarkdownFormatter_FormatVersions_WithSpecialCharacters_PreservesVersions`**: + Version strings with special characters are preserved. +- **`MarkdownFormatter_FormatVersions_CaseInsensitiveSorting`**: Alphabetical sorting is case-insensitive. +- **`MarkdownFormatter_FormatVersions_SortsVersionsAlphabetically`**: + Version strings are sorted alphabetically within a tool entry. +- **`MarkdownFormatter_Format_WithZeroDepth_ThrowsArgumentOutOfRangeException`**: + A depth of zero throws ArgumentOutOfRangeException. +- **`MarkdownFormatter_Format_WithPartialToolCoverage_ShowsAllContributingTools`**: + Partial tool coverage across jobs shows all contributing tools. + +#### Dependencies + +No external mocks or file system access is required. Tests call `MarkdownFormatter.Format` +directly with constructed lists of `VersionInfo` objects. + +#### Requirements Coverage + +The following list maps `MarkdownFormatter` unit requirements to test scenarios: + +- **`VersionMark-Formatter-Structure`**: `MarkdownFormatter_FormatVersions_SortsToolsAlphabetically`, + `MarkdownFormatter_FormatVersions_EmptyList_ProducesHeaderOnly`, + `MarkdownFormatter_FormatVersions_SortsJobIdsAlphabetically`, + `MarkdownFormatter_FormatVersions_CaseInsensitiveSorting`, + `MarkdownFormatter_FormatVersions_SortsVersionsAlphabetically`, + `MarkdownFormatter_Format_WithPartialToolCoverage_ShowsAllContributingTools` +- **`VersionMark-Formatter-JobId`**: `MarkdownFormatter_FormatVersions_WithUniformVersions_ShowsVersionOnly`, + `MarkdownFormatter_FormatVersions_MixedVersions_HandlesCorrectly` +- **`VersionMark-Formatter-Versions`**: `MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividualJobs`, + `MarkdownFormatter_FormatVersions_SingleJob_ShowsAllJobs`, + `MarkdownFormatter_FormatVersions_MixedVersions_HandlesCorrectly`, + `MarkdownFormatter_FormatVersions_WithSpecialCharacters_PreservesVersions` +- **`VersionMark-Formatter-MarkdownList`**: `MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividualJobs` +- **`VersionMark-Formatter-MarkdownConsolidation`**: + `MarkdownFormatter_FormatVersions_WithCustomDepth_UsesCorrectHeadingLevel`, + `MarkdownFormatter_Format_WithZeroDepth_ThrowsArgumentOutOfRangeException` diff --git a/docs/verification/version-mark/self-test.md b/docs/verification/version-mark/self-test.md new file mode 100644 index 0000000..b44effd --- /dev/null +++ b/docs/verification/version-mark/self-test.md @@ -0,0 +1,47 @@ +## SelfTest Subsystem Verification + +### Overview + +The SelfTest subsystem provides built-in self-validation for the VersionMark tool. It +consists of two units: `Validation` (the self-validation test runner) and `PathHelpers` +(the safe path combination utility). + +Subsystem-level integration tests are in `SelfTest/SelfTestTests.cs` and cover the full +self-validation workflow including TRX/JUnit results file writing, heading depth handling, +and path safety verification. Unit-level verification for `Validation` and `PathHelpers` +is in the chapters that follow. + +### Verification Approach + +Integration tests invoke the SelfTest subsystem with various flag combinations and assert +on the written results files and output. Tests use a temporary directory for results +files. No external mocks are required. + +### Test Scenarios + +The following integration test scenarios verify SelfTest subsystem requirements: + +- **`SelfTest_PathHelpers_PathTraversal_ThrowsArgumentException`**: Path traversal attempt is rejected with ArgumentException. +- **`SelfTest_PathHelpers_ValidRelativePath_ProducesExpectedPath`**: Valid relative path combines correctly. +- **`SelfTest_PathHelpers_FindsDllInBaseDirectory_FileExists`**: Tool DLL is found in the base directory. +- **`SelfTest_Run_WithResultsFlag_WritesResultsFile`**: `--results` flag writes a TRX results file. +- **`SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile`**: `--results-xml` flag writes a JUnit results file. +- **`SelfTest_Run_WithDepthTwo_WritesHashHashHeader`**: Depth 2 produces a `##` heading in the output. + +### Dependencies + +Tests use a temporary directory for results files. No external mocks are required. + +### Requirements Coverage + +The following list maps SelfTest subsystem requirements to test scenarios: + +- **`VersionMark-Validate-Capture`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile` +- **`VersionMark-Validate-Publish`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile` +- **`VersionMark-Validate-Lint`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile` +- **`VersionMark-Validate-Results`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile`, + `SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile` +- **`VersionMark-PathHelpers-SafeCombine`**: `SelfTest_PathHelpers_PathTraversal_ThrowsArgumentException`, + `SelfTest_PathHelpers_ValidRelativePath_ProducesExpectedPath`, + `SelfTest_PathHelpers_FindsDllInBaseDirectory_FileExists` +- **`VersionMark-Validation-HeaderDepth`**: `SelfTest_Run_WithDepthTwo_WritesHashHashHeader` diff --git a/docs/verification/version-mark/self-test/path-helpers.md b/docs/verification/version-mark/self-test/path-helpers.md new file mode 100644 index 0000000..80b821d --- /dev/null +++ b/docs/verification/version-mark/self-test/path-helpers.md @@ -0,0 +1,51 @@ +### PathHelpers Unit Verification + +#### Overview + +The `PathHelpers` unit provides a `SafePathCombine` method that combines a base path and +a relative path while preventing path traversal attacks. It rejects relative paths that +contain `..` components that would escape the base directory, as well as absolute paths. +Tests are in `SelfTest/PathHelpersTests.cs`. + +#### Test Scenarios + +The following test scenarios verify `PathHelpers`: + +- **`PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly`**: A simple relative path is combined with the base path. +- **`PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException`**: + A path beginning with `../` throws ArgumentException. +- **`PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException`**: + A path containing `..` in the middle throws ArgumentException. +- **`PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException`**: + A rooted absolute path throws ArgumentException. +- **`PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrectly`**: + A path containing `.` (current directory) combines correctly. +- **`PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly`**: A nested relative path combines correctly. +- **`PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath`**: + An empty relative path returns the base path unchanged. +- **`PathHelpers_SafePathCombine_DotDotAsNamePrefix_CombinesCorrectly`**: + A filename that starts with `..` but is not a traversal combines correctly. +- **`PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException`**: + A null base path throws ArgumentNullException. +- **`PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException`**: + A null relative path throws ArgumentNullException. + +#### Dependencies + +No external mocks or file system access is required. Tests call `PathHelpers.SafePathCombine` +directly. + +#### Requirements Coverage + +The following list maps `PathHelpers` unit requirements to test scenarios: + +- **`VersionMark-PathHelpers-SafeCombine`**: `PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly`, + `PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException`, + `PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException`, + `PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException`, + `PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrectly`, + `PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly`, + `PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath`, + `PathHelpers_SafePathCombine_DotDotAsNamePrefix_CombinesCorrectly`, + `PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException`, + `PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException` diff --git a/docs/verification/version-mark/self-test/validation.md b/docs/verification/version-mark/self-test/validation.md new file mode 100644 index 0000000..45a1da4 --- /dev/null +++ b/docs/verification/version-mark/self-test/validation.md @@ -0,0 +1,43 @@ +### Validation Unit Verification + +#### Overview + +The `Validation` unit implements the self-test suite that runs during `--validate`. It +exercises capture, publish, and lint workflows end-to-end inside a temporary directory and +writes results to a TRX or JUnit XML file. Tests are in `SelfTest/SelfTestTests.cs`. + +The internal test names used by `Validation` are `VersionMark_CapturesVersions`, +`VersionMark_GeneratesMarkdownReport`, `VersionMark_LintPassesForValidConfig`, and +`VersionMark_LintReportsErrorsForInvalidConfig`. These names appear in TRX results files +and serve as platform-level traceability evidence. + +#### Test Scenarios + +The following test scenarios verify the `Validation` unit: + +- **`SelfTest_Run_WithResultsFlag_WritesResultsFile`**: Full validation run with `--results` + writes a TRX file containing all internal test results. +- **`SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile`**: Full validation run with a + JUnit results flag writes a JUnit XML file. +- **`SelfTest_Run_WithDepthTwo_WritesHashHashHeader`**: Running with depth 2 produces a `##` heading in the validation output. + +Note: `SelfTest_Run_WithResultsFlag_WritesResultsFile` verifies that the `Validation` +class internally exercises all four self-test scenarios: `VersionMark_CapturesVersions`, +`VersionMark_GeneratesMarkdownReport`, `VersionMark_LintPassesForValidConfig`, and +`VersionMark_LintReportsErrorsForInvalidConfig`. + +#### Dependencies + +Tests use a temporary directory for results files and intermediate capture and publish +artifacts. No external mocks are required. + +#### Requirements Coverage + +The following list maps `Validation` unit requirements to test scenarios: + +- **`VersionMark-Validation-Capture`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile` +- **`VersionMark-Validation-Publish`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile` +- **`VersionMark-Validation-Lint`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile` +- **`VersionMark-Validation-WriteResults`**: `SelfTest_Run_WithResultsFlag_WritesResultsFile`, + `SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile` +- **`VersionMark-Validation-HeaderDepth`**: `SelfTest_Run_WithDepthTwo_WritesHashHashHeader` diff --git a/requirements.yaml b/requirements.yaml index a4797bc..f932540 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -5,23 +5,23 @@ # Requirements are verified through tests and documented in docs/requirements_doc/ and docs/requirements_report/. includes: - - docs/reqstream/version-mark/version-mark.yaml + - docs/reqstream/version-mark.yaml - docs/reqstream/version-mark/platform-requirements.yaml - - docs/reqstream/version-mark/cli/cli.yaml + - docs/reqstream/version-mark/cli.yaml - docs/reqstream/version-mark/cli/program.yaml - docs/reqstream/version-mark/cli/context.yaml - - docs/reqstream/version-mark/capture/capture.yaml + - docs/reqstream/version-mark/capture.yaml - docs/reqstream/version-mark/capture/version-info.yaml - - docs/reqstream/version-mark/publishing/publishing.yaml + - docs/reqstream/version-mark/publishing.yaml - docs/reqstream/version-mark/publishing/markdown-formatter.yaml - - docs/reqstream/version-mark/configuration/configuration.yaml + - docs/reqstream/version-mark/configuration.yaml - docs/reqstream/version-mark/configuration/version-mark-config.yaml - docs/reqstream/version-mark/configuration/tool-config.yaml - docs/reqstream/version-mark/configuration/load.yaml - - docs/reqstream/version-mark/self-test/self-test.yaml + - docs/reqstream/version-mark/self-test.yaml - docs/reqstream/version-mark/self-test/validation.yaml - docs/reqstream/version-mark/self-test/path-helpers.yaml - - docs/reqstream/ots/mstest.yaml + - docs/reqstream/ots/xunit.yaml - docs/reqstream/ots/reqstream.yaml - docs/reqstream/ots/buildmark.yaml - docs/reqstream/ots/sarifmark.yaml diff --git a/test/DemaConsulting.VersionMark.Tests/AssemblyInfo.cs b/test/DemaConsulting.VersionMark.Tests/AssemblyInfo.cs index 495b4f8..4273839 100644 --- a/test/DemaConsulting.VersionMark.Tests/AssemblyInfo.cs +++ b/test/DemaConsulting.VersionMark.Tests/AssemblyInfo.cs @@ -18,4 +18,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -[assembly: DoNotParallelize] +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/test/DemaConsulting.VersionMark.Tests/Capture/CaptureTests.cs b/test/DemaConsulting.VersionMark.Tests/Capture/CaptureTests.cs index 759ca47..b5164c2 100644 --- a/test/DemaConsulting.VersionMark.Tests/Capture/CaptureTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Capture/CaptureTests.cs @@ -28,13 +28,12 @@ namespace DemaConsulting.VersionMark.Tests.Capture; /// /// Subsystem tests for the Capture subsystem (version capture and persistence pipeline). /// -[TestClass] public class CaptureTests { /// /// Test that the full capture pipeline saves and loads version data without data loss. /// - [TestMethod] + [Fact] public void Capture_SaveAndLoad_PreservesAllVersionData() { // Arrange - Create version info representing a complete capture result @@ -55,13 +54,12 @@ public void Capture_SaveAndLoad_PreservesAllVersionData() 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"]); + Assert.NotNull(loadedVersionInfo); + Assert.Equal(originalVersionInfo.JobId, loadedVersionInfo.JobId); + Assert.Equal(3, loadedVersionInfo.Versions.Count); + Assert.Equal("8.0.100", loadedVersionInfo.Versions["dotnet"]); + Assert.Equal("2.43.0", loadedVersionInfo.Versions["git"]); + Assert.Equal("20.11.0", loadedVersionInfo.Versions["node"]); } finally { @@ -72,7 +70,7 @@ public void Capture_SaveAndLoad_PreservesAllVersionData() /// /// Test that the capture subsystem correctly handles multiple capture files from the same job. /// - [TestMethod] + [Fact] public void Capture_MultipleCaptures_EachFileHasDistinctJobId() { // Arrange - Create two capture files representing different CI jobs @@ -92,10 +90,9 @@ public void Capture_MultipleCaptures_EachFileHasDistinctJobId() 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"); + Assert.Equal("job-build-linux", loaded1.JobId); + Assert.Equal("job-build-windows", loaded2.JobId); + Assert.NotEqual(loaded1.JobId, loaded2.JobId); } finally { @@ -107,48 +104,47 @@ public void Capture_MultipleCaptures_EachFileHasDistinctJobId() /// /// Test that loading a version info file that does not exist throws an ArgumentException. /// - [TestMethod] + [Fact] public void Capture_LoadFromFile_NonExistentFile_ThrowsArgumentException() { // Arrange var nonExistentPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".json"); // Act & Assert - Assert.ThrowsExactly(() => VersionInfo.LoadFromFile(nonExistentPath)); + Assert.Throws(() => VersionInfo.LoadFromFile(nonExistentPath)); } /// /// Test that Context correctly sets the capture mode flag when --capture is specified. /// - [TestMethod] + [Fact] public void Capture_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, + Assert.True(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] + [Fact] public void Capture_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"); + Assert.Equal("my-build-job", context.JobId); } /// /// Test that when --output is not specified, the default filename includes the job ID. /// - [TestMethod] + [Fact] public void Capture_Run_NoOutputFlagSpecified_UsesDefaultFilename() { // Arrange - Set up temp directory with config; run without --output so default filename is used @@ -174,7 +170,7 @@ public void Capture_Run_NoOutputFlagSpecified_UsesDefaultFilename() // Assert - The default output file versionmark-.json should exist var defaultFile = PathHelpers.SafePathCombine(tempDir, "versionmark-default-job.json"); - Assert.IsTrue(File.Exists(defaultFile), + Assert.True(File.Exists(defaultFile), "Default output file 'versionmark-.json' should be created when --output is not specified"); } finally @@ -190,23 +186,22 @@ public void Capture_Run_NoOutputFlagSpecified_UsesDefaultFilename() /// /// Test that Context correctly stores tool names from the -- separator. /// - [TestMethod] + [Fact] public void Capture_Context_WithToolFilter_SetsToolNames() { // Arrange & Act - Create a context with --capture and tool names after -- using var context = Context.Create(["--capture", "--job-id", "test-job", "--", "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")); + Assert.Equal(2, context.ToolNames.Length); + Assert.True(context.ToolNames.Contains("dotnet")); + Assert.True(context.ToolNames.Contains("git")); } /// /// Test that capture without a tool filter captures all tools defined in configuration. /// - [TestMethod] + [Fact] public void Capture_Run_NoToolFilter_CapturesAllConfiguredTools() { // Arrange - Set up temp directory with a two-tool config; no tool filter specified @@ -237,11 +232,11 @@ public void Capture_Run_NoToolFilter_CapturesAllConfiguredTools() Program.Run(context); // Assert - Both tools should appear in the saved output - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet"), + Assert.True(versionInfo.Versions.ContainsKey("dotnet"), "All configured tools should be captured when no tool filter is specified"); - Assert.IsTrue(versionInfo.Versions.ContainsKey("git"), + Assert.True(versionInfo.Versions.ContainsKey("git"), "All configured tools should be captured when no tool filter is specified"); } finally @@ -257,7 +252,7 @@ public void Capture_Run_NoToolFilter_CapturesAllConfiguredTools() /// /// Test that VersionMarkConfig.ReadFromFile correctly loads tool definitions from a YAML file. /// - [TestMethod] + [Fact] public void Capture_Config_ReadFromFile_LoadsToolDefinitions() { // Arrange - Write a .versionmark.yaml file to a temp path @@ -278,10 +273,10 @@ public void Capture_Config_ReadFromFile_LoadsToolDefinitions() var config = VersionMarkConfig.ReadFromFile(tempFile); // Assert - All tool definitions should be loaded - Assert.IsNotNull(config); - Assert.IsTrue(config.Tools.ContainsKey("dotnet"), + Assert.NotNull(config); + Assert.True(config.Tools.ContainsKey("dotnet"), "ReadFromFile should load all tools from the configuration file"); - Assert.IsTrue(config.Tools.ContainsKey("git"), + Assert.True(config.Tools.ContainsKey("git"), "ReadFromFile should load all tools from the configuration file"); } finally @@ -293,7 +288,7 @@ public void Capture_Config_ReadFromFile_LoadsToolDefinitions() /// /// Test that FindVersions executes the configured command and extracts the version via regex. /// - [TestMethod] + [Fact] public void Capture_FindVersions_ExecutesCommandAndExtractsVersion() { // Arrange - Create a configuration for dotnet (always available in the build environment) @@ -312,10 +307,10 @@ public void Capture_FindVersions_ExecutesCommandAndExtractsVersion() 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"), + Assert.NotNull(versionInfo); + Assert.True(versionInfo.Versions.ContainsKey("dotnet"), "FindVersions should capture the dotnet version"); - Assert.IsFalse(string.IsNullOrEmpty(versionInfo.Versions["dotnet"]), + Assert.False(string.IsNullOrEmpty(versionInfo.Versions["dotnet"]), "Captured version should be a non-empty string"); } finally @@ -327,7 +322,7 @@ public void Capture_FindVersions_ExecutesCommandAndExtractsVersion() /// /// Test that the capture pipeline displays captured tool versions to the user. /// - [TestMethod] + [Fact] public void Capture_Run_DisplaysCapturedVersionsAfterCapture() { // Arrange - Set up temp directory with config and redirect console output to capture it @@ -361,8 +356,8 @@ public void Capture_Run_DisplaysCapturedVersionsAfterCapture() // Assert - Tool names and versions should appear in the output var output = outWriter.ToString(); - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(output.Contains("dotnet"), + Assert.Equal(0, context.ExitCode); + Assert.True(output.Contains("dotnet"), "Capture output should display captured tool names to the user"); } finally @@ -383,7 +378,7 @@ public void Capture_Run_DisplaysCapturedVersionsAfterCapture() /// /// Test that the capture pipeline reports an error when .versionmark.yaml does not exist. /// - [TestMethod] + [Fact] public void Capture_Run_MissingConfig_ReportsError() { // Arrange - Create a temp directory with no .versionmark.yaml configuration file @@ -405,9 +400,8 @@ public void Capture_Run_MissingConfig_ReportsError() Program.Run(context); // Assert - Non-zero exit code and an error message should be reported - Assert.AreEqual(1, context.ExitCode, - "Capture without .versionmark.yaml should result in a non-zero exit code"); - Assert.IsTrue( + Assert.Equal(1, context.ExitCode); + Assert.True( errWriter.ToString().Length > 0, "An error message should be written when the config file is missing"); } diff --git a/test/DemaConsulting.VersionMark.Tests/Capture/VersionInfoTests.cs b/test/DemaConsulting.VersionMark.Tests/Capture/VersionInfoTests.cs index 5409b48..4ef8884 100644 --- a/test/DemaConsulting.VersionMark.Tests/Capture/VersionInfoTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Capture/VersionInfoTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.VersionMark.Tests.Capture; /// /// Unit tests for the VersionInfo class. /// -[TestClass] public class VersionInfoTests { /// /// Test creating a VersionInfo with constructor. /// - [TestMethod] + [Fact] public void VersionInfo_Constructor_CreatesVersionInfo() { // Arrange @@ -46,17 +45,17 @@ public void VersionInfo_Constructor_CreatesVersionInfo() var versionInfo = new VersionInfo(jobId, versions); // Assert - Assert.IsNotNull(versionInfo); - Assert.AreEqual(jobId, versionInfo.JobId); - Assert.HasCount(2, versionInfo.Versions); - Assert.AreEqual("8.0.0", versionInfo.Versions["dotnet"]); - Assert.AreEqual("2.40.0", versionInfo.Versions["git"]); + Assert.NotNull(versionInfo); + Assert.Equal(jobId, versionInfo.JobId); + Assert.Equal(2, versionInfo.Versions.Count); + Assert.Equal("8.0.0", versionInfo.Versions["dotnet"]); + Assert.Equal("2.40.0", versionInfo.Versions["git"]); } /// /// Test SaveToFile creates a JSON file with expected content. /// - [TestMethod] + [Fact] public void VersionInfo_SaveToFile_CreatesJsonFile() { // Arrange @@ -75,14 +74,14 @@ public void VersionInfo_SaveToFile_CreatesJsonFile() versionInfo.SaveToFile(tempFile); // Assert - Assert.IsTrue(File.Exists(tempFile)); + Assert.True(File.Exists(tempFile)); // Verify by loading and comparing var loaded = VersionInfo.LoadFromFile(tempFile); - Assert.AreEqual("job-456", loaded.JobId); - Assert.HasCount(2, loaded.Versions); - Assert.AreEqual("18.0.0", loaded.Versions["node"]); - Assert.AreEqual("9.0.0", loaded.Versions["npm"]); + Assert.Equal("job-456", loaded.JobId); + Assert.Equal(2, loaded.Versions.Count); + Assert.Equal("18.0.0", loaded.Versions["node"]); + Assert.Equal("9.0.0", loaded.Versions["npm"]); } finally { @@ -96,7 +95,7 @@ public void VersionInfo_SaveToFile_CreatesJsonFile() /// /// Test LoadFromFile reads a JSON file and creates VersionInfo. /// - [TestMethod] + [Fact] public void VersionInfo_LoadFromFile_ReadsJsonFile() { // Arrange @@ -116,11 +115,11 @@ public void VersionInfo_LoadFromFile_ReadsJsonFile() var versionInfo = VersionInfo.LoadFromFile(tempFile); // Assert - Assert.IsNotNull(versionInfo); - Assert.AreEqual("job-789", versionInfo.JobId); - Assert.HasCount(2, versionInfo.Versions); - Assert.AreEqual("3.11.0", versionInfo.Versions["python"]); - Assert.AreEqual("23.0.0", versionInfo.Versions["pip"]); + Assert.NotNull(versionInfo); + Assert.Equal("job-789", versionInfo.JobId); + Assert.Equal(2, versionInfo.Versions.Count); + Assert.Equal("3.11.0", versionInfo.Versions["python"]); + Assert.Equal("23.0.0", versionInfo.Versions["pip"]); } finally { @@ -134,7 +133,7 @@ public void VersionInfo_LoadFromFile_ReadsJsonFile() /// /// Test SaveToFile and LoadFromFile round-trip preserves data. /// - [TestMethod] + [Fact] public void VersionInfo_SaveAndLoad_RoundTripPreservesData() { // Arrange @@ -155,12 +154,12 @@ public void VersionInfo_SaveAndLoad_RoundTripPreservesData() var loaded = VersionInfo.LoadFromFile(tempFile); // Assert - Assert.AreEqual(original.JobId, loaded.JobId); - Assert.HasCount(original.Versions.Count, loaded.Versions); + Assert.Equal(original.JobId, loaded.JobId); + Assert.Equal(original.Versions.Count, loaded.Versions.Count); foreach (var kvp in original.Versions) { - Assert.IsTrue(loaded.Versions.TryGetValue(kvp.Key, out var value)); - Assert.AreEqual(kvp.Value, value); + Assert.True(loaded.Versions.TryGetValue(kvp.Key, out var value)); + Assert.Equal(kvp.Value, value); } } finally @@ -175,21 +174,21 @@ public void VersionInfo_SaveAndLoad_RoundTripPreservesData() /// /// Test LoadFromFile throws ArgumentException when file does not exist. /// - [TestMethod] + [Fact] public void VersionInfo_LoadFromFile_NonExistentFile_ThrowsArgumentException() { // Arrange var nonExistentFile = Path.Combine(Path.GetTempPath(), "non-existent-file.json"); // Act & Assert - var exception = Assert.ThrowsExactly(() => VersionInfo.LoadFromFile(nonExistentFile)); + var exception = Assert.Throws(() => VersionInfo.LoadFromFile(nonExistentFile)); Assert.Contains("not found", exception.Message, StringComparison.OrdinalIgnoreCase); } /// /// Test LoadFromFile throws ArgumentException for invalid JSON. /// - [TestMethod] + [Fact] public void VersionInfo_LoadFromFile_InvalidJson_ThrowsArgumentException() { // Arrange @@ -199,7 +198,7 @@ public void VersionInfo_LoadFromFile_InvalidJson_ThrowsArgumentException() File.WriteAllText(tempFile, "{ invalid json }"); // Act & Assert - var exception = Assert.ThrowsExactly(() => VersionInfo.LoadFromFile(tempFile)); + var exception = Assert.Throws(() => VersionInfo.LoadFromFile(tempFile)); Assert.Contains("parse", exception.Message, StringComparison.OrdinalIgnoreCase); } finally @@ -214,7 +213,7 @@ public void VersionInfo_LoadFromFile_InvalidJson_ThrowsArgumentException() /// /// Test LoadFromFile throws ArgumentException for empty JSON file. /// - [TestMethod] + [Fact] public void VersionInfo_LoadFromFile_EmptyJson_ThrowsArgumentException() { // Arrange @@ -224,7 +223,7 @@ public void VersionInfo_LoadFromFile_EmptyJson_ThrowsArgumentException() File.WriteAllText(tempFile, ""); // Act & Assert - var exception = Assert.ThrowsExactly(() => VersionInfo.LoadFromFile(tempFile)); + var exception = Assert.Throws(() => VersionInfo.LoadFromFile(tempFile)); Assert.Contains("parse", exception.Message, StringComparison.OrdinalIgnoreCase); } finally @@ -239,7 +238,7 @@ public void VersionInfo_LoadFromFile_EmptyJson_ThrowsArgumentException() /// /// Test SaveToFile to invalid path throws InvalidOperationException. /// - [TestMethod] + [Fact] public void VersionInfo_SaveToFile_InvalidPath_ThrowsInvalidOperationException() { // Arrange @@ -247,14 +246,14 @@ public void VersionInfo_SaveToFile_InvalidPath_ThrowsInvalidOperationException() var invalidPath = Path.Combine(Path.GetTempPath(), "non-existent-directory", "file.json"); // Act & Assert - var exception = Assert.ThrowsExactly(() => versionInfo.SaveToFile(invalidPath)); + var exception = Assert.Throws(() => versionInfo.SaveToFile(invalidPath)); Assert.Contains("Failed to save", exception.Message); } /// /// Test VersionInfo with empty versions dictionary. /// - [TestMethod] + [Fact] public void VersionInfo_EmptyVersions_SavesAndLoadsCorrectly() { // Arrange @@ -268,8 +267,8 @@ public void VersionInfo_EmptyVersions_SavesAndLoadsCorrectly() var loaded = VersionInfo.LoadFromFile(tempFile); // Assert - Assert.AreEqual(original.JobId, loaded.JobId); - Assert.IsEmpty(loaded.Versions); + Assert.Equal(original.JobId, loaded.JobId); + Assert.Empty(loaded.Versions); } finally { @@ -283,7 +282,7 @@ public void VersionInfo_EmptyVersions_SavesAndLoadsCorrectly() /// /// Test LoadFromFile throws ArgumentException when JSON deserializes to null (e.g., literal "null"). /// - [TestMethod] + [Fact] public void VersionInfo_LoadFromFile_NullJson_ThrowsArgumentException() { // Arrange @@ -293,7 +292,7 @@ public void VersionInfo_LoadFromFile_NullJson_ThrowsArgumentException() File.WriteAllText(tempFile, "null"); // Act & Assert - var exception = Assert.ThrowsExactly(() => VersionInfo.LoadFromFile(tempFile)); + var exception = Assert.Throws(() => VersionInfo.LoadFromFile(tempFile)); Assert.Contains("deserialize", exception.Message, StringComparison.OrdinalIgnoreCase); } finally @@ -308,7 +307,7 @@ public void VersionInfo_LoadFromFile_NullJson_ThrowsArgumentException() /// /// Test VersionInfo with special characters in values. /// - [TestMethod] + [Fact] public void VersionInfo_SpecialCharacters_SavesAndLoadsCorrectly() { // Arrange @@ -329,11 +328,11 @@ public void VersionInfo_SpecialCharacters_SavesAndLoadsCorrectly() var loaded = VersionInfo.LoadFromFile(tempFile); // Assert - Assert.AreEqual(original.JobId, loaded.JobId); - Assert.HasCount(original.Versions.Count, loaded.Versions); + Assert.Equal(original.JobId, loaded.JobId); + Assert.Equal(original.Versions.Count, loaded.Versions.Count); foreach (var kvp in original.Versions) { - Assert.AreEqual(kvp.Value, loaded.Versions[kvp.Key]); + Assert.Equal(kvp.Value, loaded.Versions[kvp.Key]); } } finally @@ -348,7 +347,7 @@ public void VersionInfo_SpecialCharacters_SavesAndLoadsCorrectly() /// /// Test VersionInfo record creation with specific job-id and version values. /// - [TestMethod] + [Fact] public void VersionInfo_Constructor_CreatesRecord() { // Arrange @@ -363,10 +362,10 @@ public void VersionInfo_Constructor_CreatesRecord() var versionInfo = new VersionInfo(jobId, versions); // Assert - Assert.IsNotNull(versionInfo); - Assert.AreEqual("job-123", versionInfo.JobId); - Assert.HasCount(2, versionInfo.Versions); - Assert.AreEqual("8.0.100", versionInfo.Versions["dotnet"]); - Assert.AreEqual("2.43.0", versionInfo.Versions["git"]); + Assert.NotNull(versionInfo); + Assert.Equal("job-123", versionInfo.JobId); + Assert.Equal(2, versionInfo.Versions.Count); + Assert.Equal("8.0.100", versionInfo.Versions["dotnet"]); + Assert.Equal("2.43.0", versionInfo.Versions["git"]); } } diff --git a/test/DemaConsulting.VersionMark.Tests/Cli/CliTests.cs b/test/DemaConsulting.VersionMark.Tests/Cli/CliTests.cs index d37cfa6..a961e02 100644 --- a/test/DemaConsulting.VersionMark.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Cli/CliTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.VersionMark.Tests.Cli; /// /// Subsystem tests for the Cli subsystem (Program and Context working together). /// -[TestClass] public class CliTests { /// /// Test that the full CLI pipeline with --version flag exits cleanly. /// - [TestMethod] + [Fact] public void Cli_Run_VersionFlag_ExitsCleanly() { // Arrange - Create a context with --version via the full CLI pipeline @@ -41,13 +40,13 @@ public void Cli_Run_VersionFlag_ExitsCleanly() Program.Run(context); // Assert - The CLI subsystem should exit with code 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test that the full CLI pipeline with --silent flag suppresses standard output. /// - [TestMethod] + [Fact] public void Cli_Run_SilentWithVersionFlag_SuppressesOutput() { // Arrange - Redirect console output to capture what the CLI writes @@ -62,8 +61,8 @@ public void Cli_Run_SilentWithVersionFlag_SuppressesOutput() Program.Run(context); // Assert - Silent mode should suppress all standard output - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(string.IsNullOrEmpty(outWriter.ToString()), + Assert.Equal(0, context.ExitCode); + Assert.True(string.IsNullOrEmpty(outWriter.ToString()), "Silent flag should suppress version output through the full CLI pipeline"); } finally @@ -75,7 +74,7 @@ public void Cli_Run_SilentWithVersionFlag_SuppressesOutput() /// /// Test that the full CLI pipeline with --help flag displays usage information. /// - [TestMethod] + [Fact] public void Cli_Run_HelpFlag_DisplaysUsageInformation() { // Arrange @@ -91,10 +90,10 @@ public void Cli_Run_HelpFlag_DisplaysUsageInformation() Program.Run(context); // Assert - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var text = output.ToString(); - Assert.IsTrue(text.Length > 0); - Assert.IsTrue(text.Contains("--capture")); + Assert.True(text.Length > 0); + Assert.Contains("--capture", text); } finally { @@ -105,7 +104,7 @@ public void Cli_Run_HelpFlag_DisplaysUsageInformation() /// /// Test that the full CLI pipeline with --validate flag runs self-validation. /// - [TestMethod] + [Fact] public void Cli_Run_ValidateFlag_RunsValidation() { // Arrange @@ -115,19 +114,19 @@ public void Cli_Run_ValidateFlag_RunsValidation() Program.Run(context); // Assert - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test that the full CLI pipeline rejects unknown arguments by throwing ArgumentException. /// - [TestMethod] + [Fact] public void Cli_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(() => + Assert.Throws(() => { using var context = Context.Create(["--unknown-flag-xyz"]); Program.Run(context); @@ -137,7 +136,7 @@ public void Cli_Run_InvalidArgs_ThrowsArgumentException() /// /// Test that the full CLI pipeline with --lint flag succeeds for a valid config file. /// - [TestMethod] + [Fact] public void Cli_Run_LintFlag_ValidConfig_Succeeds() { // Arrange @@ -157,7 +156,7 @@ public void Cli_Run_LintFlag_ValidConfig_Succeeds() Program.Run(context); // Assert - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } finally { @@ -168,7 +167,7 @@ public void Cli_Run_LintFlag_ValidConfig_Succeeds() /// /// Test that the full CLI pipeline with --results flag writes validation results to a file. /// - [TestMethod] + [Fact] public void Cli_Run_ResultsFlag_WritesResultsFile() { // Arrange - Set up a results file path that should be written during --validate @@ -181,11 +180,11 @@ public void Cli_Run_ResultsFlag_WritesResultsFile() Program.Run(context); // Assert - The results file should exist and contain TRX content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(resultsFile), + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(resultsFile), "Results file should be written when --results flag is specified"); var content = File.ReadAllText(resultsFile); - Assert.IsFalse(string.IsNullOrWhiteSpace(content), + Assert.False(string.IsNullOrWhiteSpace(content), "Results file should contain test result data"); } finally @@ -200,7 +199,7 @@ public void Cli_Run_ResultsFlag_WritesResultsFile() /// /// Test that the full CLI pipeline with --log flag writes output to a log file. /// - [TestMethod] + [Fact] public void Cli_Run_LogFlag_WritesOutputToLogFile() { // Arrange - Set up a log file that should be written with version output @@ -214,12 +213,12 @@ public void Cli_Run_LogFlag_WritesOutputToLogFile() Program.Run(context); // Assert - Exit code should be zero - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(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), + Assert.False(string.IsNullOrWhiteSpace(logContent), "Log file should contain output when --log flag is specified"); } finally diff --git a/test/DemaConsulting.VersionMark.Tests/Cli/ContextTests.cs b/test/DemaConsulting.VersionMark.Tests/Cli/ContextTests.cs index 1d33ffb..361cea8 100644 --- a/test/DemaConsulting.VersionMark.Tests/Cli/ContextTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Cli/ContextTests.cs @@ -25,161 +25,160 @@ namespace DemaConsulting.VersionMark.Tests.Cli; /// /// Unit tests for the Context class. /// -[TestClass] public class ContextTests { /// /// Test creating a context with no arguments. /// - [TestMethod] + [Fact] public void Context_Create_NoArguments_ReturnsDefaultContext() { // Arrange & Act - Create context with no arguments using var context = Context.Create([]); // Assert - Verify default context state - Assert.IsFalse(context.Version); - Assert.IsFalse(context.Help); - Assert.IsFalse(context.Silent); - Assert.IsFalse(context.Validate); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.False(context.Help); + Assert.False(context.Silent); + Assert.False(context.Validate); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the version flag. /// - [TestMethod] + [Fact] public void Context_Create_VersionFlag_SetsVersionTrue() { // Arrange & Act - Create context with --version flag using var context = Context.Create(["--version"]); // Assert - Verify version flag is set - Assert.IsTrue(context.Version); - Assert.IsFalse(context.Help); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Version); + Assert.False(context.Help); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the short version flag. /// - [TestMethod] + [Fact] public void Context_Create_ShortVersionFlag_SetsVersionTrue() { // Arrange & Act - Create context with -v flag using var context = Context.Create(["-v"]); // Assert - Verify version flag is set - Assert.IsTrue(context.Version); - Assert.IsFalse(context.Help); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Version); + Assert.False(context.Help); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the help flag. /// - [TestMethod] + [Fact] public void Context_Create_HelpFlag_SetsHelpTrue() { // Arrange & Act - Create context with --help flag using var context = Context.Create(["--help"]); // Assert - Verify help flag is set - Assert.IsFalse(context.Version); - Assert.IsTrue(context.Help); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.True(context.Help); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the short help flag -h. /// - [TestMethod] + [Fact] public void Context_Create_ShortHelpFlag_H_SetsHelpTrue() { // Arrange & Act - Create context with -h flag using var context = Context.Create(["-h"]); // Assert - Verify help flag is set - Assert.IsFalse(context.Version); - Assert.IsTrue(context.Help); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.True(context.Help); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the short help flag -?. /// - [TestMethod] + [Fact] public void Context_Create_ShortHelpFlag_Question_SetsHelpTrue() { // Arrange & Act - Create context with -? flag using var context = Context.Create(["-?"]); // Assert - Verify help flag is set - Assert.IsFalse(context.Version); - Assert.IsTrue(context.Help); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.True(context.Help); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the silent flag. /// - [TestMethod] + [Fact] public void Context_Create_SilentFlag_SetsSilentTrue() { // Arrange & Act - Create context with --silent flag using var context = Context.Create(["--silent"]); // Assert - Verify silent flag is set - Assert.IsTrue(context.Silent); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Silent); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the validate flag. /// - [TestMethod] + [Fact] public void Context_Create_ValidateFlag_SetsValidateTrue() { // Arrange & Act - Create context with --validate flag using var context = Context.Create(["--validate"]); // Assert - Verify validate flag is set - Assert.IsTrue(context.Validate); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Validate); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the results flag. /// - [TestMethod] + [Fact] public void Context_Create_ResultsFlag_SetsResultsFile() { // Arrange & Act - Create context with --results flag using var context = Context.Create(["--results", "test.trx"]); // Assert - Verify results file is set - Assert.AreEqual("test.trx", context.ResultsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("test.trx", context.ResultsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the legacy result flag (alias for --results). /// - [TestMethod] + [Fact] public void Context_Create_ResultFlag_SetsResultsFile() { // Arrange & Act - Create context with --result flag (legacy alias) using var context = Context.Create(["--result", "test.trx"]); // Assert - Verify results file is set - Assert.AreEqual("test.trx", context.ResultsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("test.trx", context.ResultsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the log flag. /// - [TestMethod] + [Fact] public void Context_Create_LogFlag_OpensLogFile() { var logFile = Path.GetTempFileName(); @@ -188,11 +187,11 @@ public void Context_Create_LogFlag_OpensLogFile() using (var context = Context.Create(["--log", logFile])) { context.WriteLine("Test message"); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } // Verify log file was written - Assert.IsTrue(File.Exists(logFile)); + Assert.True(File.Exists(logFile)); var logContent = File.ReadAllText(logFile); Assert.Contains("Test message", logContent); } @@ -208,11 +207,11 @@ public void Context_Create_LogFlag_OpensLogFile() /// /// Test creating a context with an unknown argument throws exception. /// - [TestMethod] + [Fact] public void Context_Create_UnknownArgument_ThrowsArgumentException() { // Arrange & Act - Create context with unknown argument - var exception = Assert.ThrowsExactly(() => Context.Create(["--unknown"])); + var exception = Assert.Throws(() => Context.Create(["--unknown"])); // Assert - Verify exception is thrown with correct message Assert.Contains("Unsupported argument", exception.Message); @@ -221,7 +220,7 @@ public void Context_Create_UnknownArgument_ThrowsArgumentException() /// /// Test WriteLine writes to console output when not silent. /// - [TestMethod] + [Fact] public void Context_WriteLine_NotSilent_WritesToConsole() { // Arrange - Redirect console output @@ -248,7 +247,7 @@ public void Context_WriteLine_NotSilent_WritesToConsole() /// /// Test WriteLine does not write to console when silent. /// - [TestMethod] + [Fact] public void Context_WriteLine_Silent_DoesNotWriteToConsole() { // Arrange - Redirect console output @@ -277,7 +276,7 @@ public void Context_WriteLine_Silent_DoesNotWriteToConsole() /// What is tested: WriteError is suppressed when Silent flag is set /// What the assertions prove: WriteError respects the Silent flag /// - [TestMethod] + [Fact] public void Context_WriteError_Silent_DoesNotWriteToConsole() { // Arrange - Redirect console error output @@ -306,7 +305,7 @@ public void Context_WriteError_Silent_DoesNotWriteToConsole() /// What is tested: WriteError marks the context as having errors /// What the assertions prove: ExitCode is 1 after WriteError is called /// - [TestMethod] + [Fact] public void Context_WriteError_SetsErrorExitCode() { // Arrange - Create context with silent flag to avoid console output @@ -316,7 +315,7 @@ public void Context_WriteError_SetsErrorExitCode() context.WriteError("Error message"); // Assert - Verify exit code is 1 - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } /// @@ -324,7 +323,7 @@ public void Context_WriteError_SetsErrorExitCode() /// What is tested: WriteError writes to Console.Error (stderr) when not silent /// What the assertions prove: Error messages go to stderr, not stdout /// - [TestMethod] + [Fact] public void Context_WriteError_NotSilent_WritesToConsole() { // Arrange - Redirect console error output @@ -353,7 +352,7 @@ public void Context_WriteError_NotSilent_WritesToConsole() /// What is tested: WriteError writes to log file alongside stderr /// What the assertions prove: Error messages are recorded in the log file /// - [TestMethod] + [Fact] public void Context_WriteError_WritesToLogFile() { var logFile = Path.GetTempFileName(); @@ -370,7 +369,7 @@ public void Context_WriteError_WritesToLogFile() } // Assert - Verify error was written to log file - Assert.IsTrue(File.Exists(logFile)); + Assert.True(File.Exists(logFile)); var logContent = File.ReadAllText(logFile); Assert.Contains("Error message", logContent); } @@ -389,11 +388,11 @@ public void Context_WriteError_WritesToLogFile() /// What is tested: --log flag requires a filename argument /// What the assertions prove: Missing value for --log raises ArgumentException /// - [TestMethod] + [Fact] public void Context_Create_LogFlag_WithoutValue_ThrowsArgumentException() { // Arrange & Act - Create context with --log flag but no value - var exception = Assert.ThrowsExactly(() => Context.Create(["--log"])); + var exception = Assert.Throws(() => Context.Create(["--log"])); // Assert - Verify exception is thrown Assert.Contains("--log", exception.Message); @@ -404,11 +403,11 @@ public void Context_Create_LogFlag_WithoutValue_ThrowsArgumentException() /// What is tested: --results flag requires a filename argument /// What the assertions prove: Missing value for --results raises ArgumentException /// - [TestMethod] + [Fact] public void Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException() { // Arrange & Act - Create context with --results flag but no value - var exception = Assert.ThrowsExactly(() => Context.Create(["--results"])); + var exception = Assert.Throws(() => Context.Create(["--results"])); // Assert - Verify exception is thrown Assert.Contains("--results", exception.Message); @@ -419,7 +418,7 @@ public void Context_Create_ResultsFlag_WithoutValue_ThrowsArgumentException() /// What is tested: --publish flag parsing sets Publish property to true /// What the assertions prove: The Publish flag is correctly parsed and stored /// - [TestMethod] + [Fact] public void Context_Create_PublishFlag_SetsPublishTrue() { // Arrange & Act - Create context with --publish and --report flags @@ -427,8 +426,8 @@ public void Context_Create_PublishFlag_SetsPublishTrue() // Assert - Verify publish mode is enabled // What is proved: --publish flag is correctly recognized and sets Publish property - Assert.IsTrue(context.Publish); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Publish); + Assert.Equal(0, context.ExitCode); } /// @@ -436,7 +435,7 @@ public void Context_Create_PublishFlag_SetsPublishTrue() /// What is tested: --report parameter parsing captures the output file path /// What the assertions prove: The report file path is correctly parsed and stored /// - [TestMethod] + [Fact] public void Context_Create_ReportParameter_SetsReportFile() { // Arrange & Act - Create context with --publish and --report flags @@ -444,8 +443,8 @@ public void Context_Create_ReportParameter_SetsReportFile() // Assert - Verify report file path is captured // What is proved: --report parameter value is correctly captured - Assert.AreEqual("output.md", context.ReportFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("output.md", context.ReportFile); + Assert.Equal(0, context.ExitCode); } /// @@ -453,7 +452,7 @@ public void Context_Create_ReportParameter_SetsReportFile() /// What is tested: --report-depth parameter parsing captures the depth value /// What the assertions prove: The report depth is correctly parsed as an integer /// - [TestMethod] + [Fact] public void Context_Create_ReportDepthParameter_SetsReportDepth() { // Arrange & Act - Create context with --publish, --report, and --report-depth flags @@ -461,8 +460,8 @@ public void Context_Create_ReportDepthParameter_SetsReportDepth() // Assert - Verify report depth is captured // What is proved: --report-depth parameter value is correctly parsed as an integer - Assert.AreEqual(3, context.ReportDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(3, context.ReportDepth); + Assert.Equal(0, context.ExitCode); } /// @@ -470,7 +469,7 @@ public void Context_Create_ReportDepthParameter_SetsReportDepth() /// What is tested: Default report-depth value when not specified /// What the assertions prove: The default report depth is 1 (matching the --depth default) /// - [TestMethod] + [Fact] public void Context_Create_NoReportDepth_DefaultsToDepthOne() { // Arrange & Act - Create context without --report-depth @@ -478,7 +477,7 @@ public void Context_Create_NoReportDepth_DefaultsToDepthOne() // Assert - Verify default report depth is 1 (the default --depth value) // What is proved: Report depth defaults to the --depth value (1) when not specified - Assert.AreEqual(1, context.ReportDepth); + Assert.Equal(1, context.ReportDepth); } /// @@ -486,11 +485,11 @@ public void Context_Create_NoReportDepth_DefaultsToDepthOne() /// What is tested: Validation rejects a depth value less than 1 /// What the assertions prove: ArgumentException is thrown for report-depth values less than 1 /// - [TestMethod] + [Fact] public void Context_Create_ReportDepthZero_ThrowsArgumentException() { // Arrange & Act & Assert - Zero is not a valid heading depth - Assert.ThrowsExactly(() => + Assert.Throws(() => Context.Create(["--publish", "--report", "output.md", "--report-depth", "0"])); } @@ -499,11 +498,11 @@ public void Context_Create_ReportDepthZero_ThrowsArgumentException() /// What is tested: Validation rejects negative depth values /// What the assertions prove: ArgumentException is thrown for negative report-depth values /// - [TestMethod] + [Fact] public void Context_Create_ReportDepthNegative_ThrowsArgumentException() { // Arrange & Act & Assert - Negative values are not valid heading depths - Assert.ThrowsExactly(() => + Assert.Throws(() => Context.Create(["--publish", "--report", "output.md", "--report-depth", "-1"])); } @@ -512,11 +511,11 @@ public void Context_Create_ReportDepthNegative_ThrowsArgumentException() /// What is tested: Validation rejects a depth value greater than 6 /// What the assertions prove: ArgumentException is thrown for report-depth values greater than 6 /// - [TestMethod] + [Fact] public void Context_Create_ReportDepthSeven_ThrowsArgumentException() { // Arrange & Act & Assert - 7 exceeds the maximum Markdown heading level of 6 - Assert.ThrowsExactly(() => + Assert.Throws(() => Context.Create(["--publish", "--report", "output.md", "--report-depth", "7"])); } @@ -525,7 +524,7 @@ public void Context_Create_ReportDepthSeven_ThrowsArgumentException() /// What is tested: --depth parameter parsing captures the depth value /// What the assertions prove: The depth is correctly parsed as an integer /// - [TestMethod] + [Fact] public void Context_Create_DepthParameter_SetsDepth() { // Arrange & Act - Create context with --depth flag @@ -533,8 +532,8 @@ public void Context_Create_DepthParameter_SetsDepth() // Assert - Verify depth is captured // What is proved: --depth parameter value is correctly parsed as an integer - Assert.AreEqual(3, context.Depth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(3, context.Depth); + Assert.Equal(0, context.ExitCode); } /// @@ -542,7 +541,7 @@ public void Context_Create_DepthParameter_SetsDepth() /// What is tested: Default depth value when not specified /// What the assertions prove: The depth defaults to 1 /// - [TestMethod] + [Fact] public void Context_Create_NoDepth_DefaultsToOne() { // Arrange & Act - Create context without --depth @@ -550,7 +549,7 @@ public void Context_Create_NoDepth_DefaultsToOne() // Assert - Verify default depth is 1 // What is proved: Depth defaults to 1 when not specified - Assert.AreEqual(1, context.Depth); + Assert.Equal(1, context.Depth); } /// @@ -558,7 +557,7 @@ public void Context_Create_NoDepth_DefaultsToOne() /// What is tested: --depth value is used as default for ReportDepth when --report-depth not specified /// What the assertions prove: ReportDepth equals Depth when --report-depth is not given /// - [TestMethod] + [Fact] public void Context_Create_DepthParameter_SetsDefaultReportDepth() { // Arrange & Act - Create context with --depth but no --report-depth @@ -566,8 +565,8 @@ public void Context_Create_DepthParameter_SetsDefaultReportDepth() // Assert - Verify report depth defaults to the depth value // What is proved: --depth value is used as the default for ReportDepth - Assert.AreEqual(3, context.ReportDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(3, context.ReportDepth); + Assert.Equal(0, context.ExitCode); } /// @@ -575,7 +574,7 @@ public void Context_Create_DepthParameter_SetsDefaultReportDepth() /// What is tested: --report-depth takes precedence over --depth for ReportDepth /// What the assertions prove: ReportDepth equals the explicit --report-depth value, not --depth /// - [TestMethod] + [Fact] public void Context_Create_ExplicitReportDepthOverridesDepth() { // Arrange & Act - Create context with both --depth and --report-depth @@ -583,8 +582,8 @@ public void Context_Create_ExplicitReportDepthOverridesDepth() // Assert - Verify explicit --report-depth takes precedence over --depth // What is proved: --report-depth value (4) overrides --depth value (2) for ReportDepth - Assert.AreEqual(4, context.ReportDepth); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(4, context.ReportDepth); + Assert.Equal(0, context.ExitCode); } /// @@ -592,11 +591,11 @@ public void Context_Create_ExplicitReportDepthOverridesDepth() /// What is tested: Validation rejects a depth value less than 1 /// What the assertions prove: ArgumentException is thrown for depth values less than 1 /// - [TestMethod] + [Fact] public void Context_Create_DepthZero_ThrowsArgumentException() { // Arrange & Act & Assert - Zero is not a valid heading depth - Assert.ThrowsExactly(() => + Assert.Throws(() => Context.Create(["--depth", "0"])); } @@ -605,11 +604,11 @@ public void Context_Create_DepthZero_ThrowsArgumentException() /// What is tested: Validation rejects negative depth values /// What the assertions prove: ArgumentException is thrown for negative depth values /// - [TestMethod] + [Fact] public void Context_Create_DepthNegative_ThrowsArgumentException() { // Arrange & Act & Assert - Negative values are not valid heading depths - Assert.ThrowsExactly(() => + Assert.Throws(() => Context.Create(["--depth", "-1"])); } @@ -618,11 +617,11 @@ public void Context_Create_DepthNegative_ThrowsArgumentException() /// What is tested: Validation rejects a depth value greater than 6 /// What the assertions prove: ArgumentException is thrown for depth values greater than 6 /// - [TestMethod] + [Fact] public void Context_Create_DepthSeven_ThrowsArgumentException() { // Arrange & Act & Assert - 7 exceeds the maximum Markdown heading level of 6 - Assert.ThrowsExactly(() => + Assert.Throws(() => Context.Create(["--depth", "7"])); } @@ -631,7 +630,7 @@ public void Context_Create_DepthSeven_ThrowsArgumentException() /// What is tested: Glob patterns after -- are captured in GlobPatterns array /// What the assertions prove: Multiple glob patterns are correctly parsed and stored /// - [TestMethod] + [Fact] public void Context_Create_GlobPatternsAfterSeparator_CapturesPatterns() { // Arrange & Act - Create context with glob patterns after -- @@ -645,9 +644,9 @@ public void Context_Create_GlobPatternsAfterSeparator_CapturesPatterns() // Assert - Verify glob patterns are captured // What is proved: Arguments after -- are correctly captured in GlobPatterns array - Assert.HasCount(2, context.GlobPatterns); - Assert.AreEqual("versionmark-*.json", context.GlobPatterns[0]); - Assert.AreEqual("results/*.json", context.GlobPatterns[1]); + Assert.Equal(2, context.GlobPatterns.Length); + Assert.Equal("versionmark-*.json", context.GlobPatterns[0]); + Assert.Equal("results/*.json", context.GlobPatterns[1]); } /// @@ -655,7 +654,7 @@ public void Context_Create_GlobPatternsAfterSeparator_CapturesPatterns() /// What is tested: --publish flag can be parsed without --report in Context.Create /// What the assertions prove: Context parsing accepts --publish without --report (error checked later) /// - [TestMethod] + [Fact] public void Context_Create_PublishWithoutReport_ParsesSuccessfully() { // Arrange & Act - Create context with --publish but no --report @@ -663,9 +662,9 @@ public void Context_Create_PublishWithoutReport_ParsesSuccessfully() // Assert - Verify publish mode is enabled (validation happens in Program.Run) // What is proved: --publish flag is parsed successfully without --report - Assert.IsTrue(context.Publish); - Assert.IsNull(context.ReportFile); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Publish); + Assert.Null(context.ReportFile); + Assert.Equal(0, context.ExitCode); } /// @@ -673,7 +672,7 @@ public void Context_Create_PublishWithoutReport_ParsesSuccessfully() /// What is tested: GlobPatterns array when none provided after -- /// What the assertions prove: GlobPatterns is empty array when not specified /// - [TestMethod] + [Fact] public void Context_Create_NoGlobPatterns_EmptyArray() { // Arrange & Act - Create context without glob patterns @@ -681,94 +680,94 @@ public void Context_Create_NoGlobPatterns_EmptyArray() // Assert - Verify glob patterns array is empty (default applied in Program.RunPublish) // What is proved: When no glob patterns specified, GlobPatterns is an empty array - Assert.HasCount(0, context.GlobPatterns); + Assert.Empty(context.GlobPatterns); } /// /// Test creating a context with the lint flag. /// - [TestMethod] + [Fact] public void Context_Create_LintFlag_SetsLintTrue() { // Arrange & Act - Create context with --lint flag using var context = Context.Create(["--lint"]); // Assert - Verify lint flag is set - Assert.IsTrue(context.Lint); - Assert.IsNull(context.LintFile); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Lint); + Assert.Null(context.LintFile); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the lint flag and a config file argument. /// - [TestMethod] + [Fact] public void Context_Create_LintFlag_WithFile_SetsLintFile() { // Arrange & Act - Create context with --lint and a file argument using var context = Context.Create(["--lint", "custom.yaml"]); // Assert - Verify lint flag and file are set - Assert.IsTrue(context.Lint); - Assert.AreEqual("custom.yaml", context.LintFile); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Lint); + Assert.Equal("custom.yaml", context.LintFile); + Assert.Equal(0, context.ExitCode); } /// /// Test that lint flag without a file does not consume the next flag argument. /// - [TestMethod] + [Fact] public void Context_Create_LintFlag_FollowedByFlag_DoesNotConsumeFlagAsFile() { // Arrange & Act - Create context with --lint followed by another flag using var context = Context.Create(["--lint", "--silent"]); // Assert - Verify lint flag is set and silent is also set, LintFile is null - Assert.IsTrue(context.Lint); - Assert.IsNull(context.LintFile); - Assert.IsTrue(context.Silent); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Lint); + Assert.Null(context.LintFile); + Assert.True(context.Silent); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the capture flag. /// - [TestMethod] + [Fact] public void Context_Create_CaptureFlag_SetsCaptureTrue() { // Arrange & Act - Create context with --capture flag using var context = Context.Create(["--capture"]); // Assert - Verify capture flag is set - Assert.IsTrue(context.Capture); - Assert.AreEqual(0, context.ExitCode); + Assert.True(context.Capture); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the job-id parameter. /// - [TestMethod] + [Fact] public void Context_Create_JobIdFlag_SetsJobId() { // Arrange & Act - Create context with --capture and --job-id parameter using var context = Context.Create(["--capture", "--job-id", "test-job"]); // Assert - Verify job ID is set - Assert.AreEqual("test-job", context.JobId); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("test-job", context.JobId); + Assert.Equal(0, context.ExitCode); } /// /// Test creating a context with the output parameter. /// - [TestMethod] + [Fact] public void Context_Create_OutputFlag_SetsOutputFile() { // Arrange & Act - Create context with --capture and --output parameter using var context = Context.Create(["--capture", "--output", "output.json"]); // Assert - Verify output file is set - Assert.AreEqual("output.json", context.OutputFile); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal("output.json", context.OutputFile); + Assert.Equal(0, context.ExitCode); } } diff --git a/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationTests.cs b/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationTests.cs index dacfe4a..429e7a5 100644 --- a/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Configuration/ConfigurationTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.VersionMark.Tests.Configuration; /// /// Subsystem tests for the Configuration subsystem (VersionMarkConfig and ToolConfig working together). /// -[TestClass] public class ConfigurationTests { /// /// Test that reading a multi-tool configuration file produces all tools with usable commands and regexes. /// - [TestMethod] + [Fact] public void Configuration_ReadFromFile_MultipleTools_AllToolsAccessible() { // Arrange - Write a valid multi-tool config to a temp file @@ -54,13 +53,13 @@ public void Configuration_ReadFromFile_MultipleTools_AllToolsAccessible() 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()), + Assert.NotNull(config); + Assert.Equal(2, config.Tools.Count); + Assert.True(config.Tools.ContainsKey("dotnet"), "dotnet tool should be present"); + Assert.True(config.Tools.ContainsKey("git"), "git tool should be present"); + Assert.False(string.IsNullOrEmpty(config.Tools["dotnet"].GetEffectiveCommand()), "dotnet command should be accessible"); - Assert.IsFalse(string.IsNullOrEmpty(config.Tools["git"].GetEffectiveRegex()), + Assert.False(string.IsNullOrEmpty(config.Tools["git"].GetEffectiveRegex()), "git regex should be accessible"); } finally @@ -72,7 +71,7 @@ public void Configuration_ReadFromFile_MultipleTools_AllToolsAccessible() /// /// Test that reading a configuration file with OS-specific overrides selects the correct command. /// - [TestMethod] + [Fact] public void Configuration_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand() { // Arrange - Write a config with OS-specific overrides to a temp file @@ -97,18 +96,15 @@ public void Configuration_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand // 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"); + Assert.Equal("dotnet.exe --version", effectiveCommand); } else if (OperatingSystem.IsLinux()) { - Assert.AreEqual("dotnet-linux --version", effectiveCommand, - "On Linux the Linux override should be selected"); + Assert.Equal("dotnet-linux --version", effectiveCommand); } else { - Assert.AreEqual("dotnet --version", effectiveCommand, - "On other platforms the default command should be selected"); + Assert.Equal("dotnet --version", effectiveCommand); } } finally @@ -120,20 +116,20 @@ public void Configuration_ReadFromFile_WithOsOverrides_SelectsAppropriateCommand /// /// Test that reading a configuration from a missing file throws an ArgumentException. /// - [TestMethod] + [Fact] public void Configuration_ReadFromFile_MissingFile_ThrowsArgumentException() { // Arrange - Use a path that does not exist var nonExistentFile = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.yaml"); // Act & Assert - Assert.ThrowsExactly(() => VersionMarkConfig.ReadFromFile(nonExistentFile)); + Assert.Throws(() => VersionMarkConfig.ReadFromFile(nonExistentFile)); } /// /// Test that reading a configuration with an OS-specific regex override returns the appropriate regex. /// - [TestMethod] + [Fact] public void Configuration_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex() { // Arrange @@ -157,18 +153,15 @@ public void Configuration_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex() // 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"); + Assert.Equal(@"(?\d+\.\d+\.\d+)-win", effectiveRegex); } else if (OperatingSystem.IsLinux()) { - Assert.AreEqual(@"(?\d+\.\d+\.\d+)-linux", effectiveRegex, - "On Linux the Linux regex override should be selected"); + Assert.Equal(@"(?\d+\.\d+\.\d+)-linux", effectiveRegex); } else { - Assert.AreEqual(@"(?\d+\.\d+\.\d+)", effectiveRegex, - "On other platforms the default regex should be selected"); + Assert.Equal(@"(?\d+\.\d+\.\d+)", effectiveRegex); } } finally @@ -180,7 +173,7 @@ public void Configuration_ReadFromFile_OsRegexOverride_SelectsAppropriateRegex() /// /// Test that reading a configuration with an empty tools section throws an ArgumentException. /// - [TestMethod] + [Fact] public void Configuration_ReadFromFile_EmptyTools_ThrowsArgumentException() { // Arrange @@ -192,7 +185,7 @@ public void Configuration_ReadFromFile_EmptyTools_ThrowsArgumentException() try { // Act & Assert - Assert.ThrowsExactly(() => VersionMarkConfig.ReadFromFile(tempFile)); + Assert.Throws(() => VersionMarkConfig.ReadFromFile(tempFile)); } finally { @@ -203,7 +196,7 @@ public void Configuration_ReadFromFile_EmptyTools_ThrowsArgumentException() /// /// Test that reading a configuration with invalid YAML throws an ArgumentException. /// - [TestMethod] + [Fact] public void Configuration_ReadFromFile_InvalidYaml_ThrowsArgumentException() { // Arrange @@ -213,7 +206,7 @@ public void Configuration_ReadFromFile_InvalidYaml_ThrowsArgumentException() try { // Act & Assert - Assert.ThrowsExactly(() => VersionMarkConfig.ReadFromFile(tempFile)); + Assert.Throws(() => VersionMarkConfig.ReadFromFile(tempFile)); } finally { diff --git a/test/DemaConsulting.VersionMark.Tests/Configuration/LintIssueTests.cs b/test/DemaConsulting.VersionMark.Tests/Configuration/LintIssueTests.cs index bc4882d..3652d1c 100644 --- a/test/DemaConsulting.VersionMark.Tests/Configuration/LintIssueTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Configuration/LintIssueTests.cs @@ -26,13 +26,12 @@ namespace DemaConsulting.VersionMark.Tests.Configuration; /// /// Unit tests for the record and the record. /// -[TestClass] public class LintIssueTests { /// /// Test that properties are stored correctly. /// - [TestMethod] + [Fact] public void LintIssue_Constructor_AllFields_AreStoredCorrectly() { // Arrange & Act @@ -44,17 +43,17 @@ public void LintIssue_Constructor_AllFields_AreStoredCorrectly() Description: "Missing required field"); // Assert - Assert.AreEqual("config.yaml", issue.FilePath); - Assert.AreEqual(3L, issue.Line); - Assert.AreEqual(5L, issue.Column); - Assert.AreEqual(LintSeverity.Error, issue.Severity); - Assert.AreEqual("Missing required field", issue.Description); + Assert.Equal("config.yaml", issue.FilePath); + Assert.Equal(3L, issue.Line); + Assert.Equal(5L, issue.Column); + Assert.Equal(LintSeverity.Error, issue.Severity); + Assert.Equal("Missing required field", issue.Description); } /// /// Test that produces the expected format with lowercase severity for an error. /// - [TestMethod] + [Fact] public void LintIssue_ToString_Error_ProducesLowercaseSeverity() { // Arrange @@ -69,13 +68,13 @@ public void LintIssue_ToString_Error_ProducesLowercaseSeverity() var result = issue.ToString(); // Assert - severity must be lowercase 'error', not 'Error' - Assert.AreEqual("config.yaml(10,2): error: tool 'dotnet' is missing required field 'command'", result); + Assert.Equal("config.yaml(10,2): error: tool 'dotnet' is missing required field 'command'", result); } /// /// Test that produces the expected format with lowercase severity for a warning. /// - [TestMethod] + [Fact] public void LintIssue_ToString_Warning_ProducesLowercaseSeverity() { // Arrange @@ -90,13 +89,13 @@ public void LintIssue_ToString_Warning_ProducesLowercaseSeverity() var result = issue.ToString(); // Assert - severity must be lowercase 'warning', not 'Warning' - Assert.AreEqual("my.versionmark.yaml(4,1): warning: unknown key 'extra-field'", result); + Assert.Equal("my.versionmark.yaml(4,1): warning: unknown key 'extra-field'", result); } /// /// Test that properties are stored correctly. /// - [TestMethod] + [Fact] public void VersionMarkLoadResult_Constructor_AllFields_AreStoredCorrectly() { // Arrange @@ -107,15 +106,15 @@ public void VersionMarkLoadResult_Constructor_AllFields_AreStoredCorrectly() var loadResult = new VersionMarkLoadResult(null, issues); // Assert - Assert.IsNull(loadResult.Config); - Assert.HasCount(1, loadResult.Issues); - Assert.AreSame(issue, loadResult.Issues[0]); + Assert.Null(loadResult.Config); + Assert.Single(loadResult.Issues); + Assert.Same(issue, loadResult.Issues[0]); } /// /// Test that routes errors to context.WriteError. /// - [TestMethod] + [Fact] public void VersionMarkLoadResult_ReportIssues_Error_WritesToErrorStream() { // Arrange @@ -134,9 +133,9 @@ public void VersionMarkLoadResult_ReportIssues_Error_WritesToErrorStream() // Assert - errors must be routed to the error stream var errorOutput = errWriter.ToString(); - StringAssert.Contains(errorOutput, "error:"); - StringAssert.Contains(errorOutput, "missing command"); - Assert.AreEqual(1, context.ExitCode, "ExitCode should be non-zero after reporting an error"); + Assert.Contains("error:", errorOutput); + Assert.Contains("missing command", errorOutput); + Assert.Equal(1, context.ExitCode); } finally { @@ -147,7 +146,7 @@ public void VersionMarkLoadResult_ReportIssues_Error_WritesToErrorStream() /// /// Test that routes warnings to standard output (not errors). /// - [TestMethod] + [Fact] public void VersionMarkLoadResult_ReportIssues_Warning_WritesToStdOut() { // Arrange @@ -168,9 +167,9 @@ public void VersionMarkLoadResult_ReportIssues_Warning_WritesToStdOut() loadResult.ReportIssues(context); // Assert - warnings should go to stdout only and must not set the error exit code - Assert.AreEqual(0, context.ExitCode, "ExitCode should remain zero for warnings only"); - StringAssert.Contains(stdout.ToString(), "config.yaml(5,3): warning: unknown key 'x'"); - Assert.AreEqual(string.Empty, stderr.ToString(), "Warnings should not be written to stderr"); + Assert.Equal(0, context.ExitCode); + Assert.Contains("config.yaml(5,3): warning: unknown key 'x'", stdout.ToString()); + Assert.Equal(string.Empty, stderr.ToString()); } finally { diff --git a/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigLoadTests.cs b/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigLoadTests.cs index 9630c7d..2782646 100644 --- a/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigLoadTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigLoadTests.cs @@ -26,13 +26,12 @@ namespace DemaConsulting.VersionMark.Tests.Configuration; /// /// Unit tests for the method. /// -[TestClass] public class VersionMarkConfigLoadTests { /// /// Test that a valid configuration file returns a non-null config with no errors. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_ValidConfig_ReturnsConfig() { // Arrange - Create a well-formed .versionmark.yaml config file @@ -52,8 +51,8 @@ public void VersionMarkConfig_Load_ValidConfig_ReturnsConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be returned with no error issues - Assert.IsNotNull(config); - Assert.IsFalse(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.NotNull(config); + Assert.DoesNotContain(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -64,7 +63,7 @@ public void VersionMarkConfig_Load_ValidConfig_ReturnsConfig() /// /// Test that a non-existent file returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_MissingFile_ReturnsNullConfig() { // Arrange - Use a path that does not exist @@ -74,14 +73,14 @@ public void VersionMarkConfig_Load_MissingFile_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(nonExistentFile); // Assert - Config should be null and issues should contain an error - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } /// /// Test that a file containing invalid YAML returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_InvalidYaml_ReturnsNullConfig() { // Arrange - Write syntactically broken YAML to a temp file @@ -99,8 +98,8 @@ public void VersionMarkConfig_Load_InvalidYaml_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null and issues should contain a parse error - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -111,7 +110,7 @@ public void VersionMarkConfig_Load_InvalidYaml_ReturnsNullConfig() /// /// Test that a config without a 'tools' section returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_MissingToolsSection_ReturnsNullConfig() { // Arrange - Write a YAML file that has no 'tools' key at the root @@ -128,8 +127,8 @@ public void VersionMarkConfig_Load_MissingToolsSection_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because 'tools' is required - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -140,7 +139,7 @@ public void VersionMarkConfig_Load_MissingToolsSection_ReturnsNullConfig() /// /// Test that a config with an empty 'tools' section returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_EmptyToolsSection_ReturnsNullConfig() { // Arrange - Write a YAML file that has a 'tools' mapping with no entries @@ -157,8 +156,8 @@ public void VersionMarkConfig_Load_EmptyToolsSection_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because at least one tool is required - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -169,7 +168,7 @@ public void VersionMarkConfig_Load_EmptyToolsSection_ReturnsNullConfig() /// /// Test that a tool without a 'command' field returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_MissingCommand_ReturnsNullConfig() { // Arrange - Write a YAML file where the tool entry has no 'command' key @@ -188,8 +187,8 @@ public void VersionMarkConfig_Load_MissingCommand_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because 'command' is required - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -200,7 +199,7 @@ public void VersionMarkConfig_Load_MissingCommand_ReturnsNullConfig() /// /// Test that a tool with an empty 'command' field returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_EmptyCommand_ReturnsNullConfig() { // Arrange - Write a YAML file where the tool has an empty 'command' value @@ -220,8 +219,8 @@ public void VersionMarkConfig_Load_EmptyCommand_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because an empty command is invalid - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -232,7 +231,7 @@ public void VersionMarkConfig_Load_EmptyCommand_ReturnsNullConfig() /// /// Test that a tool without a 'regex' field returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_MissingRegex_ReturnsNullConfig() { // Arrange - Write a YAML file where the tool entry has no 'regex' key @@ -251,8 +250,8 @@ public void VersionMarkConfig_Load_MissingRegex_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because 'regex' is required - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -263,7 +262,7 @@ public void VersionMarkConfig_Load_MissingRegex_ReturnsNullConfig() /// /// Test that a tool with an empty 'regex' field returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_EmptyRegex_ReturnsNullConfig() { // Arrange - Write a YAML file where the tool has an empty 'regex' value @@ -283,8 +282,8 @@ public void VersionMarkConfig_Load_EmptyRegex_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because an empty regex is invalid - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -295,7 +294,7 @@ public void VersionMarkConfig_Load_EmptyRegex_ReturnsNullConfig() /// /// Test that a tool with an invalid 'regex' value returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_InvalidRegex_ReturnsNullConfig() { // Arrange - Write a YAML file with a syntactically broken regex (unclosed group) @@ -315,8 +314,8 @@ public void VersionMarkConfig_Load_InvalidRegex_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because the regex is invalid - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -327,7 +326,7 @@ public void VersionMarkConfig_Load_InvalidRegex_ReturnsNullConfig() /// /// Test that a tool with a regex missing the 'version' group returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_RegexMissingVersionGroup_ReturnsNullConfig() { // Arrange - Write a YAML file with a valid regex that lacks the required 'version' named group @@ -347,8 +346,8 @@ public void VersionMarkConfig_Load_RegexMissingVersionGroup_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because the 'version' capture group is required - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -359,7 +358,7 @@ public void VersionMarkConfig_Load_RegexMissingVersionGroup_ReturnsNullConfig() /// /// Test that an unknown top-level key produces a warning but config is still returned. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_UnknownTopLevelKey_ReturnsConfig() { // Arrange - Write a YAML file with a valid tool plus an unknown top-level key @@ -380,9 +379,9 @@ public void VersionMarkConfig_Load_UnknownTopLevelKey_ReturnsConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be returned; unknown keys produce warnings, not errors - Assert.IsNotNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Warning)); - Assert.IsFalse(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.NotNull(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Warning); + Assert.DoesNotContain(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -393,7 +392,7 @@ public void VersionMarkConfig_Load_UnknownTopLevelKey_ReturnsConfig() /// /// Test that an unknown tool key produces a warning but config is still returned. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_UnknownToolKey_ReturnsConfig() { // Arrange - Write a YAML file with a valid tool plus an unknown key inside the tool @@ -414,9 +413,9 @@ public void VersionMarkConfig_Load_UnknownToolKey_ReturnsConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be returned; unknown tool keys produce warnings, not errors - Assert.IsNotNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Warning)); - Assert.IsFalse(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.NotNull(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Warning); + Assert.DoesNotContain(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -427,7 +426,7 @@ public void VersionMarkConfig_Load_UnknownToolKey_ReturnsConfig() /// /// Test that an empty OS-specific command override returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_OsSpecificEmptyCommand_ReturnsNullConfig() { // Arrange - Write a YAML file with an empty command-win override @@ -448,8 +447,8 @@ public void VersionMarkConfig_Load_OsSpecificEmptyCommand_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because empty OS-specific overrides are not allowed - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -460,7 +459,7 @@ public void VersionMarkConfig_Load_OsSpecificEmptyCommand_ReturnsNullConfig() /// /// Test that an empty OS-specific regex override returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_OsSpecificEmptyRegex_ReturnsNullConfig() { // Arrange - Write a YAML file with an empty regex-linux override @@ -481,8 +480,8 @@ public void VersionMarkConfig_Load_OsSpecificEmptyRegex_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because empty OS-specific regex overrides are not allowed - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -493,7 +492,7 @@ public void VersionMarkConfig_Load_OsSpecificEmptyRegex_ReturnsNullConfig() /// /// Test that an OS-specific regex missing the 'version' group returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_OsSpecificRegexMissingVersionGroup_ReturnsNullConfig() { // Arrange - Write a YAML file with an OS-specific regex that has no 'version' named group @@ -514,8 +513,8 @@ public void VersionMarkConfig_Load_OsSpecificRegexMissingVersionGroup_ReturnsNul var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because the 'version' group is required in all regexes - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -526,7 +525,7 @@ public void VersionMarkConfig_Load_OsSpecificRegexMissingVersionGroup_ReturnsNul /// /// Test that an OS-specific regex that is invalid returns null config with an error issue. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_OsSpecificInvalidRegex_ReturnsNullConfig() { // Arrange - Write a YAML file with a broken OS-specific regex (unclosed group) @@ -547,8 +546,8 @@ public void VersionMarkConfig_Load_OsSpecificInvalidRegex_ReturnsNullConfig() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null because the OS-specific regex cannot be compiled - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { @@ -559,7 +558,7 @@ public void VersionMarkConfig_Load_OsSpecificInvalidRegex_ReturnsNullConfig() /// /// Test that multiple errors in different tools are all reported in a single Load call. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_MultipleErrors_ReportsAll() { // Arrange - Write a config where tool1 is missing 'regex' and tool2 is missing 'command' @@ -580,11 +579,11 @@ public void VersionMarkConfig_Load_MultipleErrors_ReportsAll() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null and issues should reference both tool1 and tool2 - Assert.IsNull(config); - Assert.IsTrue( + Assert.Null(config); + Assert.True( issues.Any(i => i.Severity == LintSeverity.Error && i.Description.Contains("tool1")), "Issues should contain an error mentioning tool1 (missing regex)"); - Assert.IsTrue( + Assert.True( issues.Any(i => i.Severity == LintSeverity.Error && i.Description.Contains("tool2")), "Issues should contain an error mentioning tool2 (missing command)"); } @@ -597,7 +596,7 @@ public void VersionMarkConfig_Load_MultipleErrors_ReportsAll() /// /// Test that all issues include the file path of the configuration file. /// - [TestMethod] + [Fact] public void VersionMarkConfig_Load_IssuesContainFilePath() { // Arrange - Write a config with a missing required field to force an error issue @@ -616,8 +615,8 @@ public void VersionMarkConfig_Load_IssuesContainFilePath() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - All issues should reference the path of the config file that was loaded - Assert.IsNull(config); - Assert.IsTrue( + Assert.Null(config); + Assert.True( issues.Any(i => i.FilePath == tempFile), "At least one issue should contain the config file path"); } @@ -630,17 +629,13 @@ public void VersionMarkConfig_Load_IssuesContainFilePath() /// /// Test that an unreadable file (permission denied) returns null config with an error issue. /// - [TestMethod] + [Fact] [SupportedOSPlatform("linux")] [SupportedOSPlatform("osx")] public void VersionMarkConfig_Load_UnreadableFile_ReturnsError() { // Skip on non-Unix platforms where Unix file permissions are not supported - if (!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) - { - Assert.Inconclusive("Unix file permissions not supported on this platform"); - return; - } + Assert.SkipUnless(OperatingSystem.IsLinux() || OperatingSystem.IsMacOS(), "Requires Unix file permissions"); // Arrange - Create a temp file and remove all read permissions var tempFile = Path.GetTempFileName(); @@ -653,8 +648,8 @@ public void VersionMarkConfig_Load_UnreadableFile_ReturnsError() var (config, issues) = VersionMarkConfig.Load(tempFile); // Assert - Config should be null and issues should contain an I/O error - Assert.IsNull(config); - Assert.IsTrue(issues.Any(i => i.Severity == LintSeverity.Error)); + Assert.Null(config); + Assert.Contains(issues, i => i.Severity == LintSeverity.Error); } finally { diff --git a/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigTests.cs b/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigTests.cs index 9e8f156..82246cc 100644 --- a/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Configuration/VersionMarkConfigTests.cs @@ -27,7 +27,6 @@ namespace DemaConsulting.VersionMark.Tests.Configuration; /// /// Unit tests for the VersionMarkConfig class. /// -[TestClass] public partial class VersionMarkConfigTests { private static readonly string[] s_dotnetToolArray = ["dotnet"]; @@ -41,7 +40,7 @@ public partial class VersionMarkConfigTests /// /// Test internal constructor creates config with tools. /// - [TestMethod] + [Fact] public void VersionMarkConfig_InternalConstructor_CreatesConfig() { // Arrange @@ -57,15 +56,15 @@ public void VersionMarkConfig_InternalConstructor_CreatesConfig() var config = new VersionMarkConfig(tools); // Assert - Assert.IsNotNull(config); - Assert.HasCount(1, config.Tools); - Assert.IsTrue(config.Tools.ContainsKey("dotnet")); + Assert.NotNull(config); + Assert.Single(config.Tools); + Assert.True(config.Tools.ContainsKey("dotnet")); } /// /// Test reading a valid YAML configuration file. /// - [TestMethod] + [Fact] public void VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig() { // Arrange @@ -88,20 +87,20 @@ public void VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig() var config = VersionMarkConfig.ReadFromFile(tempFile); // Assert - Assert.IsNotNull(config); - Assert.HasCount(2, config.Tools); - Assert.IsTrue(config.Tools.TryGetValue("tool1", out var tool1)); - Assert.IsTrue(config.Tools.TryGetValue("tool2", out var tool2)); + Assert.NotNull(config); + Assert.Equal(2, config.Tools.Count); + Assert.True(config.Tools.TryGetValue("tool1", out var tool1)); + Assert.True(config.Tools.TryGetValue("tool2", out var tool2)); // Check tool1 - Assert.AreEqual("tool1 --version", tool1.Command[string.Empty]); - Assert.AreEqual(@"Tool1\s+(?[\d\.]+)", tool1.Regex[string.Empty]); + Assert.Equal("tool1 --version", tool1.Command[string.Empty]); + Assert.Equal(@"Tool1\s+(?[\d\.]+)", tool1.Regex[string.Empty]); // Check tool2 - Assert.AreEqual("tool2 version --client", tool2.Command[string.Empty]); - Assert.AreEqual("tool2.cmd version --client", tool2.Command["win"]); - Assert.AreEqual(@"Tool2:""v(?[\d\.]+)""", tool2.Regex[string.Empty]); - Assert.AreEqual(@"Tool2 Version: v(?[\d\.]+)", tool2.Regex["linux"]); + Assert.Equal("tool2 version --client", tool2.Command[string.Empty]); + Assert.Equal("tool2.cmd version --client", tool2.Command["win"]); + Assert.Equal(@"Tool2:""v(?[\d\.]+)""", tool2.Regex[string.Empty]); + Assert.Equal(@"Tool2 Version: v(?[\d\.]+)", tool2.Regex["linux"]); } finally { @@ -115,7 +114,7 @@ public void VersionMarkConfig_ReadFromFile_ValidFile_ReturnsConfig() /// /// Test reading configuration with all OS overrides. /// - [TestMethod] + [Fact] public void VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig() { // Arrange @@ -139,18 +138,18 @@ public void VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig() var config = VersionMarkConfig.ReadFromFile(tempFile); // Assert - Assert.IsNotNull(config); - Assert.HasCount(1, config.Tools); - Assert.IsTrue(config.Tools.TryGetValue("gcc", out var gcc)); - - Assert.AreEqual("gcc --version", gcc.Command[string.Empty]); - Assert.AreEqual("gcc.exe --version", gcc.Command["win"]); - Assert.AreEqual("gcc-13 --version", gcc.Command["linux"]); - Assert.AreEqual("gcc-14 --version", gcc.Command["macos"]); - Assert.AreEqual(@"gcc.*?(?\d+\.\d+\.\d+)", gcc.Regex[string.Empty]); - Assert.AreEqual(@"gcc\.exe.*?(?\d+\.\d+\.\d+)", gcc.Regex["win"]); - Assert.AreEqual(@"gcc-13.*?(?\d+\.\d+\.\d+)", gcc.Regex["linux"]); - Assert.AreEqual(@"gcc-14.*?(?\d+\.\d+\.\d+)", gcc.Regex["macos"]); + Assert.NotNull(config); + Assert.Single(config.Tools); + Assert.True(config.Tools.TryGetValue("gcc", out var gcc)); + + Assert.Equal("gcc --version", gcc.Command[string.Empty]); + Assert.Equal("gcc.exe --version", gcc.Command["win"]); + Assert.Equal("gcc-13 --version", gcc.Command["linux"]); + Assert.Equal("gcc-14 --version", gcc.Command["macos"]); + Assert.Equal(@"gcc.*?(?\d+\.\d+\.\d+)", gcc.Regex[string.Empty]); + Assert.Equal(@"gcc\.exe.*?(?\d+\.\d+\.\d+)", gcc.Regex["win"]); + Assert.Equal(@"gcc-13.*?(?\d+\.\d+\.\d+)", gcc.Regex["linux"]); + Assert.Equal(@"gcc-14.*?(?\d+\.\d+\.\d+)", gcc.Regex["macos"]); } finally { @@ -164,14 +163,14 @@ public void VersionMarkConfig_ReadFromFile_WithAllOsOverrides_ReturnsConfig() /// /// Test reading from non-existent file throws ArgumentException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_ReadFromFile_NonExistentFile_ThrowsArgumentException() { // Arrange var nonExistentFile = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.yaml"); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => VersionMarkConfig.ReadFromFile(nonExistentFile)); Assert.Contains("Configuration file not found", ex.Message); @@ -180,7 +179,7 @@ public void VersionMarkConfig_ReadFromFile_NonExistentFile_ThrowsArgumentExcepti /// /// Test reading invalid YAML throws ArgumentException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_ReadFromFile_InvalidYaml_ThrowsArgumentException() { // Arrange @@ -190,7 +189,7 @@ public void VersionMarkConfig_ReadFromFile_InvalidYaml_ThrowsArgumentException() File.WriteAllText(tempFile, "invalid: yaml: content: [[["); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => VersionMarkConfig.ReadFromFile(tempFile)); Assert.Contains("Failed to parse YAML file", ex.Message); @@ -207,7 +206,7 @@ public void VersionMarkConfig_ReadFromFile_InvalidYaml_ThrowsArgumentException() /// /// Test reading YAML with no tools throws ArgumentException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_ReadFromFile_NoTools_ThrowsArgumentException() { // Arrange @@ -217,7 +216,7 @@ public void VersionMarkConfig_ReadFromFile_NoTools_ThrowsArgumentException() File.WriteAllText(tempFile, "tools: {}"); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => VersionMarkConfig.ReadFromFile(tempFile)); Assert.Contains("must contain at least one tool", ex.Message); @@ -234,7 +233,7 @@ public void VersionMarkConfig_ReadFromFile_NoTools_ThrowsArgumentException() /// /// Test GetEffectiveCommand returns default command when no OS override exists. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveCommand_NoOverride_ReturnsDefaultCommand() { // Arrange @@ -247,13 +246,13 @@ public void ToolConfig_GetEffectiveCommand_NoOverride_ReturnsDefaultCommand() var command = tool.GetEffectiveCommand(); // Assert - Assert.AreEqual("tool --version", command); + Assert.Equal("tool --version", command); } /// /// Test GetEffectiveRegex returns default regex when no OS override exists. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveRegex_NoOverride_ReturnsDefaultRegex() { // Arrange @@ -266,13 +265,13 @@ public void ToolConfig_GetEffectiveRegex_NoOverride_ReturnsDefaultRegex() var regex = tool.GetEffectiveRegex(); // Assert - Assert.AreEqual(@"(?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"(?\d+\.\d+\.\d+)", regex); } /// /// Test GetEffectiveCommand with explicit OS parameter. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveCommand_WithExplicitOs_ReturnsCorrectCommand() { // Arrange @@ -288,16 +287,16 @@ public void ToolConfig_GetEffectiveCommand_WithExplicitOs_ReturnsCorrectCommand( ); // Act & Assert - Assert.AreEqual("tool.exe --version", tool.GetEffectiveCommand("win")); - Assert.AreEqual("tool-linux --version", tool.GetEffectiveCommand("linux")); - Assert.AreEqual("tool-macos --version", tool.GetEffectiveCommand("macos")); - Assert.AreEqual("tool --version", tool.GetEffectiveCommand("unknown")); + Assert.Equal("tool.exe --version", tool.GetEffectiveCommand("win")); + Assert.Equal("tool-linux --version", tool.GetEffectiveCommand("linux")); + Assert.Equal("tool-macos --version", tool.GetEffectiveCommand("macos")); + Assert.Equal("tool --version", tool.GetEffectiveCommand("unknown")); } /// /// Test GetEffectiveRegex with explicit OS parameter. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveRegex_WithExplicitOs_ReturnsCorrectRegex() { // Arrange @@ -313,16 +312,16 @@ public void ToolConfig_GetEffectiveRegex_WithExplicitOs_ReturnsCorrectRegex() ); // Act & Assert - Assert.AreEqual(@"Windows: (?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("win")); - Assert.AreEqual(@"Linux: (?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("linux")); - Assert.AreEqual(@"macOS: (?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("macos")); - Assert.AreEqual(@"(?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("unknown")); + Assert.Equal(@"Windows: (?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("win")); + Assert.Equal(@"Linux: (?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("linux")); + Assert.Equal(@"macOS: (?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("macos")); + Assert.Equal(@"(?\d+\.\d+\.\d+)", tool.GetEffectiveRegex("unknown")); } /// /// Test GetEffectiveCommand on Windows returns Windows override when available. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveCommand_WindowsOverride_ReturnsWindowsCommand() { // Arrange @@ -342,18 +341,18 @@ public void ToolConfig_GetEffectiveCommand_WindowsOverride_ReturnsWindowsCommand // On Windows, should return Windows override; otherwise default if (OperatingSystem.IsWindows()) { - Assert.AreEqual("tool.exe --version", command); + Assert.Equal("tool.exe --version", command); } else { - Assert.AreEqual("tool --version", command); + Assert.Equal("tool --version", command); } } /// /// Test GetEffectiveCommand on Linux returns Linux override when available. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveCommand_LinuxOverride_ReturnsLinuxCommand() { // Arrange @@ -373,18 +372,18 @@ public void ToolConfig_GetEffectiveCommand_LinuxOverride_ReturnsLinuxCommand() // On Linux, should return Linux override; otherwise default if (OperatingSystem.IsLinux()) { - Assert.AreEqual("tool-linux --version", command); + Assert.Equal("tool-linux --version", command); } else { - Assert.AreEqual("tool --version", command); + Assert.Equal("tool --version", command); } } /// /// Test GetEffectiveCommand on macOS returns macOS override when available. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveCommand_MacOsOverride_ReturnsMacOsCommand() { // Arrange @@ -404,18 +403,18 @@ public void ToolConfig_GetEffectiveCommand_MacOsOverride_ReturnsMacOsCommand() // On macOS, should return macOS override; otherwise default if (OperatingSystem.IsMacOS()) { - Assert.AreEqual("tool-macos --version", command); + Assert.Equal("tool-macos --version", command); } else { - Assert.AreEqual("tool --version", command); + Assert.Equal("tool --version", command); } } /// /// Test GetEffectiveRegex on Windows returns Windows override when available. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveRegex_WindowsOverride_ReturnsWindowsRegex() { // Arrange @@ -435,18 +434,18 @@ public void ToolConfig_GetEffectiveRegex_WindowsOverride_ReturnsWindowsRegex() // On Windows, should return Windows override; otherwise default if (OperatingSystem.IsWindows()) { - Assert.AreEqual(@"Windows: (?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"Windows: (?\d+\.\d+\.\d+)", regex); } else { - Assert.AreEqual(@"(?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"(?\d+\.\d+\.\d+)", regex); } } /// /// Test GetEffectiveRegex on Linux returns Linux override when available. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveRegex_LinuxOverride_ReturnsLinuxRegex() { // Arrange @@ -466,18 +465,18 @@ public void ToolConfig_GetEffectiveRegex_LinuxOverride_ReturnsLinuxRegex() // On Linux, should return Linux override; otherwise default if (OperatingSystem.IsLinux()) { - Assert.AreEqual(@"Linux: (?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"Linux: (?\d+\.\d+\.\d+)", regex); } else { - Assert.AreEqual(@"(?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"(?\d+\.\d+\.\d+)", regex); } } /// /// Test GetEffectiveRegex on macOS returns macOS override when available. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveRegex_MacOsOverride_ReturnsMacOsRegex() { // Arrange @@ -497,18 +496,18 @@ public void ToolConfig_GetEffectiveRegex_MacOsOverride_ReturnsMacOsRegex() // On macOS, should return macOS override; otherwise default if (OperatingSystem.IsMacOS()) { - Assert.AreEqual(@"macOS: (?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"macOS: (?\d+\.\d+\.\d+)", regex); } else { - Assert.AreEqual(@"(?\d+\.\d+\.\d+)", regex); + Assert.Equal(@"(?\d+\.\d+\.\d+)", regex); } } /// /// Test GetEffectiveCommand throws InvalidOperationException when no default key is present. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveCommand_NoDefaultKey_ThrowsInvalidOperationException() { // Arrange - a ToolConfig with only an OS-specific command and no default key @@ -518,13 +517,13 @@ public void ToolConfig_GetEffectiveCommand_NoDefaultKey_ThrowsInvalidOperationEx ); // Act & Assert - requesting an OS with no matching key and no default should throw - Assert.ThrowsExactly(() => tool.GetEffectiveCommand("linux")); + Assert.Throws(() => tool.GetEffectiveCommand("linux")); } /// /// Test GetEffectiveRegex throws InvalidOperationException when no default key is present. /// - [TestMethod] + [Fact] public void ToolConfig_GetEffectiveRegex_NoDefaultKey_ThrowsInvalidOperationException() { // Arrange - a ToolConfig with only an OS-specific regex and no default key @@ -534,13 +533,13 @@ public void ToolConfig_GetEffectiveRegex_NoDefaultKey_ThrowsInvalidOperationExce ); // Act & Assert - requesting an OS with no matching key and no default should throw - Assert.ThrowsExactly(() => tool.GetEffectiveRegex("linux")); + Assert.Throws(() => tool.GetEffectiveRegex("linux")); } /// /// Test FindVersions with dotnet command. /// - [TestMethod] + [Fact] public void VersionMarkConfig_FindVersions_DotnetCommand_ReturnsVersionInfo() { // Arrange @@ -557,17 +556,17 @@ public void VersionMarkConfig_FindVersions_DotnetCommand_ReturnsVersionInfo() var versionInfo = config.FindVersions(s_dotnetToolArray, "test-job"); // Assert - Assert.IsNotNull(versionInfo); - Assert.AreEqual("test-job", versionInfo.JobId); - Assert.HasCount(1, versionInfo.Versions); - Assert.IsTrue(versionInfo.Versions.TryGetValue("dotnet", out var dotnetVersion)); - Assert.IsTrue(VersionRegex().IsMatch(dotnetVersion)); + Assert.NotNull(versionInfo); + Assert.Equal("test-job", versionInfo.JobId); + Assert.Single(versionInfo.Versions); + Assert.True(versionInfo.Versions.TryGetValue("dotnet", out var dotnetVersion)); + Assert.Matches(VersionRegex(), dotnetVersion); } /// /// Test FindVersions with multiple tools. /// - [TestMethod] + [Fact] public void VersionMarkConfig_FindVersions_MultipleTools_ReturnsAllVersions() { // Arrange @@ -588,19 +587,19 @@ public void VersionMarkConfig_FindVersions_MultipleTools_ReturnsAllVersions() var versionInfo = config.FindVersions(s_dotnetGitToolArray, "test-job"); // Assert - Assert.IsNotNull(versionInfo); - Assert.AreEqual("test-job", versionInfo.JobId); - Assert.HasCount(2, versionInfo.Versions); - Assert.IsTrue(versionInfo.Versions.TryGetValue("dotnet", out var dotnetVersion)); - Assert.IsTrue(versionInfo.Versions.TryGetValue("git", out var gitVersion)); - Assert.IsTrue(VersionRegex().IsMatch(dotnetVersion)); - Assert.IsTrue(VersionRegex().IsMatch(gitVersion)); + Assert.NotNull(versionInfo); + Assert.Equal("test-job", versionInfo.JobId); + Assert.Equal(2, versionInfo.Versions.Count); + Assert.True(versionInfo.Versions.TryGetValue("dotnet", out var dotnetVersion)); + Assert.True(versionInfo.Versions.TryGetValue("git", out var gitVersion)); + Assert.Matches(VersionRegex(), dotnetVersion); + Assert.Matches(VersionRegex(), gitVersion); } /// /// Test FindVersions with non-existent tool throws ArgumentException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_FindVersions_NonExistentTool_ThrowsArgumentException() { // Arrange @@ -614,7 +613,7 @@ public void VersionMarkConfig_FindVersions_NonExistentTool_ThrowsArgumentExcepti var config = new VersionMarkConfig(tools); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => config.FindVersions(s_nonexistentToolArray, "test-job")); Assert.Contains("Tool 'nonexistent' not found in configuration", ex.Message); @@ -623,7 +622,7 @@ public void VersionMarkConfig_FindVersions_NonExistentTool_ThrowsArgumentExcepti /// /// Test FindVersions with invalid command throws InvalidOperationException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_FindVersions_InvalidCommand_ThrowsInvalidOperationException() { // Arrange @@ -637,7 +636,7 @@ public void VersionMarkConfig_FindVersions_InvalidCommand_ThrowsInvalidOperation var config = new VersionMarkConfig(tools); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => config.FindVersions(s_invalidToolArray, "test-job")); Assert.Contains("Failed to run command", ex.Message); @@ -646,7 +645,7 @@ public void VersionMarkConfig_FindVersions_InvalidCommand_ThrowsInvalidOperation /// /// Test FindVersions with regex that doesn't match throws InvalidOperationException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_FindVersions_RegexNoMatch_ThrowsInvalidOperationException() { // Arrange @@ -660,7 +659,7 @@ public void VersionMarkConfig_FindVersions_RegexNoMatch_ThrowsInvalidOperationEx var config = new VersionMarkConfig(tools); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => config.FindVersions(s_dotnetToolArray, "test-job")); Assert.Contains("Failed to extract version for tool 'dotnet'", ex.Message); @@ -669,7 +668,7 @@ public void VersionMarkConfig_FindVersions_RegexNoMatch_ThrowsInvalidOperationEx /// /// Test FindVersions with regex without version group throws InvalidOperationException. /// - [TestMethod] + [Fact] public void VersionMarkConfig_FindVersions_RegexNoVersionGroup_ThrowsInvalidOperationException() { // Arrange @@ -683,7 +682,7 @@ public void VersionMarkConfig_FindVersions_RegexNoVersionGroup_ThrowsInvalidOper var config = new VersionMarkConfig(tools); // Act & Assert - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => config.FindVersions(s_dotnetToolArray, "test-job")); Assert.Contains("must contain a named 'version' capture group", ex.Message); diff --git a/test/DemaConsulting.VersionMark.Tests/DemaConsulting.VersionMark.Tests.csproj b/test/DemaConsulting.VersionMark.Tests/DemaConsulting.VersionMark.Tests.csproj index e4295a2..b6a1248 100644 --- a/test/DemaConsulting.VersionMark.Tests/DemaConsulting.VersionMark.Tests.csproj +++ b/test/DemaConsulting.VersionMark.Tests/DemaConsulting.VersionMark.Tests.csproj @@ -5,6 +5,7 @@ latest enable enable + Exe false true @@ -20,6 +21,7 @@ + @@ -33,8 +35,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs b/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs index a399bb7..3cdbc2e 100644 --- a/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs @@ -26,29 +26,27 @@ namespace DemaConsulting.VersionMark.Tests; /// /// Integration tests that run the VersionMark application through dotnet. /// -[TestClass] public class IntegrationTests { - private string _dllPath = string.Empty; + private readonly string _dllPath; /// - /// Initialize test by locating the VersionMark DLL. + /// Initializes a new instance of IntegrationTests by locating the VersionMark DLL. /// - [TestInitialize] - public void TestInitialize() + public IntegrationTests() { // The DLL should be in the same directory as the test assembly // because the test project references the main project var baseDir = AppContext.BaseDirectory; _dllPath = PathHelpers.SafePathCombine(baseDir, "DemaConsulting.VersionMark.dll"); - Assert.IsTrue(File.Exists(_dllPath), $"Could not find VersionMark DLL at {_dllPath}"); + Assert.True(File.Exists(_dllPath), $"Could not find VersionMark DLL at {_dllPath}"); } /// /// Test that version flag outputs version information. /// - [TestMethod] + [Fact] public void IntegrationTest_VersionFlag_OutputsVersion() { // Arrange & Act - Run the application with --version flag @@ -59,10 +57,10 @@ public void IntegrationTest_VersionFlag_OutputsVersion() "--version"); // Assert - Verify success and version output - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify version is output - Assert.IsFalse(string.IsNullOrWhiteSpace(output)); + Assert.False(string.IsNullOrWhiteSpace(output)); Assert.DoesNotContain("Error", output); Assert.DoesNotContain("Copyright", output); } @@ -70,7 +68,7 @@ public void IntegrationTest_VersionFlag_OutputsVersion() /// /// Test that help flag outputs usage information. /// - [TestMethod] + [Fact] public void IntegrationTest_HelpFlag_OutputsUsageInformation() { // Arrange & Act - Run the application with --help flag @@ -81,7 +79,7 @@ public void IntegrationTest_HelpFlag_OutputsUsageInformation() "--help"); // Assert - Verify success and usage information output - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify usage information is output Assert.Contains("Usage:", output); @@ -92,7 +90,7 @@ public void IntegrationTest_HelpFlag_OutputsUsageInformation() /// /// Test that validate flag runs self-validation. /// - [TestMethod] + [Fact] public void IntegrationTest_ValidateFlag_RunsValidation() { // Arrange & Act - Run the application with --validate flag @@ -103,7 +101,7 @@ public void IntegrationTest_ValidateFlag_RunsValidation() "--validate"); // Assert - Verify success and validation output - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify validation output Assert.Contains("Total Tests:", output); @@ -113,7 +111,7 @@ public void IntegrationTest_ValidateFlag_RunsValidation() /// /// Test that validate with results flag generates TRX file. /// - [TestMethod] + [Fact] public void IntegrationTest_ValidateWithResults_GeneratesTrxFile() { // Arrange - Create temp file path for results @@ -132,10 +130,10 @@ public void IntegrationTest_ValidateWithResults_GeneratesTrxFile() resultsFile); // Assert - Verify success and TRX file creation - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify TRX file was created - Assert.IsTrue(File.Exists(resultsFile), "Results file was not created"); + Assert.True(File.Exists(resultsFile), "Results file was not created"); // Verify TRX file contains expected content var trxContent = File.ReadAllText(resultsFile); @@ -154,7 +152,7 @@ public void IntegrationTest_ValidateWithResults_GeneratesTrxFile() /// /// Test that validate with results flag generates JUnit XML file. /// - [TestMethod] + [Fact] public void IntegrationTest_ValidateWithResults_GeneratesJUnitFile() { // Arrange - Create temp file path for results @@ -173,10 +171,10 @@ public void IntegrationTest_ValidateWithResults_GeneratesJUnitFile() resultsFile); // Assert - Verify success and JUnit file creation - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify JUnit file was created - Assert.IsTrue(File.Exists(resultsFile), "Results file was not created"); + Assert.True(File.Exists(resultsFile), "Results file was not created"); // Verify JUnit file contains expected content var xmlContent = File.ReadAllText(resultsFile); @@ -194,7 +192,7 @@ public void IntegrationTest_ValidateWithResults_GeneratesJUnitFile() /// /// Test that silent flag suppresses output. /// - [TestMethod] + [Fact] public void IntegrationTest_SilentFlag_SuppressesOutput() { // Arrange & Act - Run the application with --silent flag @@ -205,7 +203,7 @@ public void IntegrationTest_SilentFlag_SuppressesOutput() "--silent"); // Assert - Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify the tool's normal output is suppressed Assert.DoesNotContain("VersionMark version", output); @@ -215,7 +213,7 @@ public void IntegrationTest_SilentFlag_SuppressesOutput() /// /// Test that log flag writes output to file. /// - [TestMethod] + [Fact] public void IntegrationTest_LogFlag_WritesOutputToFile() { // Arrange - Create temp log file path @@ -232,10 +230,10 @@ public void IntegrationTest_LogFlag_WritesOutputToFile() logFile); // Assert - Verify success and log file creation - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify log file was created - Assert.IsTrue(File.Exists(logFile), "Log file was not created"); + Assert.True(File.Exists(logFile), "Log file was not created"); // Verify log file contains output var logContent = File.ReadAllText(logFile); @@ -253,7 +251,7 @@ public void IntegrationTest_LogFlag_WritesOutputToFile() /// /// Test that unknown argument returns error. /// - [TestMethod] + [Fact] public void IntegrationTest_UnknownArgument_ReturnsError() { // Arrange & Act - Run the application with unknown argument @@ -264,14 +262,14 @@ public void IntegrationTest_UnknownArgument_ReturnsError() "--unknown"); // Assert - Verify error is returned - Assert.AreNotEqual(0, exitCode); + Assert.NotEqual(0, exitCode); Assert.Contains("Error", output); } /// /// Test that capture command captures tool versions. /// - [TestMethod] + [Fact] public void IntegrationTest_CaptureCommand_CapturesToolVersions() { // Arrange - Set up unique temp directory with config file @@ -290,7 +288,7 @@ public void IntegrationTest_CaptureCommand_CapturesToolVersions() // Assert - Verify the command succeeded and captured the expected tool versions // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify output contains expected information Assert.Contains("Capturing tool versions", output); @@ -298,12 +296,12 @@ public void IntegrationTest_CaptureCommand_CapturesToolVersions() Assert.Contains("git", output); // Verify output file was created - Assert.IsTrue(File.Exists(outputFile), "Output file was not created"); + Assert.True(File.Exists(outputFile)); // Verify output file contains expected data var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet")); - Assert.IsTrue(versionInfo.Versions.ContainsKey("git")); + Assert.True(versionInfo.Versions.ContainsKey("dotnet")); + Assert.True(versionInfo.Versions.ContainsKey("git")); } /// @@ -403,7 +401,7 @@ private static string FindRepositoryRoot(string startPath) /// /// Test that capture command without job ID returns error. /// - [TestMethod] + [Fact] public void IntegrationTest_CaptureCommandWithoutJobId_ReturnsError() { // Arrange - No special setup needed for testing error condition @@ -416,14 +414,14 @@ public void IntegrationTest_CaptureCommandWithoutJobId_ReturnsError() "--capture"); // Assert - Verify the command fails with appropriate error message - Assert.AreNotEqual(0, exitCode); + Assert.NotEqual(0, exitCode); Assert.Contains("--job-id is required", output); } /// /// Test that capture command with missing config file returns error. /// - [TestMethod] + [Fact] public void IntegrationTest_CaptureCommandWithMissingConfig_ReturnsError() { // Arrange - Create temp directory without .versionmark.yaml config file @@ -438,14 +436,14 @@ public void IntegrationTest_CaptureCommandWithMissingConfig_ReturnsError() "--job-id", "test-job"); // Assert - Verify the command fails with error message about missing config - Assert.AreNotEqual(0, exitCode); + Assert.NotEqual(0, exitCode); Assert.Contains("error:", output); } /// /// Test that capture command with default output filename works. /// - [TestMethod] + [Fact] public void IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename() { // Arrange - Set up to test default output filename generation @@ -466,14 +464,14 @@ public void IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename( // Assert - Verify command succeeded and created file with default name pattern // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify output file was created with default name - Assert.IsTrue(File.Exists(outputFile), $"Output file '{outputFile}' was not created"); + Assert.True(File.Exists(outputFile), $"Output file '{outputFile}' was not created"); // Verify output file contains expected data var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.AreEqual(jobId, versionInfo.JobId); + Assert.Equal(jobId, versionInfo.JobId); } /// @@ -481,7 +479,7 @@ public void IntegrationTest_CaptureCommandWithDefaultOutput_UsesDefaultFilename( /// What is tested: PUB-001, PUB-002, FMT-001 - End-to-end publish workflow /// What the assertions prove: Publish command processes JSON files and creates markdown /// - [TestMethod] + [Fact] public void VersionMark_PublishCommand_GeneratesMarkdownReport() { // Arrange - Set up unique temp directory with multiple JSON files @@ -513,7 +511,7 @@ public void VersionMark_PublishCommand_GeneratesMarkdownReport() // Act - Run publish command var exitCode = Runner.Run( - out var output, + out var _, "dotnet", _dllPath, "--publish", @@ -521,8 +519,8 @@ public void VersionMark_PublishCommand_GeneratesMarkdownReport() // Assert - Verify command succeeded // What is proved: Publish command successfully generates markdown from JSON files - Assert.AreEqual(0, exitCode, $"Command failed with output: {output}"); - Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); + Assert.Equal(0, exitCode); + Assert.True(File.Exists(reportFile), "Report file was not created"); var reportContent = File.ReadAllText(reportFile); @@ -548,7 +546,7 @@ public void VersionMark_PublishCommand_GeneratesMarkdownReport() /// What is tested: PUB-003, FMT-005 - Custom report depth parameter /// What the assertions prove: --report-depth controls markdown heading level /// - [TestMethod] + [Fact] public void VersionMark_PublishCommand_WithReportDepth_AdjustsHeadingLevels() { // Arrange - Set up unique temp directory with JSON file @@ -570,7 +568,7 @@ public void VersionMark_PublishCommand_WithReportDepth_AdjustsHeadingLevels() // Act - Run publish command with custom depth var exitCode = Runner.Run( - out var output, + out var _, "dotnet", _dllPath, "--publish", @@ -579,8 +577,8 @@ public void VersionMark_PublishCommand_WithReportDepth_AdjustsHeadingLevels() // Assert - Verify command succeeded and used custom depth // What is proved: --report-depth parameter controls heading level - Assert.AreEqual(0, exitCode, $"Command failed with output: {output}"); - Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); + Assert.Equal(0, exitCode); + Assert.True(File.Exists(reportFile), "Report file was not created"); var reportContent = File.ReadAllText(reportFile); Assert.Contains("#### Tool Versions", reportContent); @@ -600,7 +598,7 @@ public void VersionMark_PublishCommand_WithReportDepth_AdjustsHeadingLevels() /// What is tested: PUB-004 - --report is required with --publish /// What the assertions prove: Command fails when --report is missing /// - [TestMethod] + [Fact] public void VersionMark_PublishCommandWithoutReport_ReturnsError() { // Arrange & Act - Run publish command without --report @@ -612,7 +610,7 @@ public void VersionMark_PublishCommandWithoutReport_ReturnsError() // Assert - Verify command failed with appropriate error // What is proved: --publish requires --report parameter - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Error: --report is required for publish mode", output); } @@ -621,7 +619,7 @@ public void VersionMark_PublishCommandWithoutReport_ReturnsError() /// What is tested: PUB-007 - Error when no JSON files match glob patterns /// What the assertions prove: Command fails with clear error when no files found /// - [TestMethod] + [Fact] public void VersionMark_PublishCommandWithNoMatchingFiles_ReturnsError() { // Arrange - Set up empty temp directory @@ -645,7 +643,7 @@ public void VersionMark_PublishCommandWithNoMatchingFiles_ReturnsError() // Assert - Verify command failed with appropriate error // What is proved: No matching files results in an error - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Error: No JSON files found matching patterns:", output); } finally @@ -663,7 +661,7 @@ public void VersionMark_PublishCommandWithNoMatchingFiles_ReturnsError() /// What is tested: PUB-008 - Error when JSON files cannot be parsed /// What the assertions prove: Command fails with clear error for malformed JSON /// - [TestMethod] + [Fact] public void VersionMark_PublishCommandWithInvalidJson_ReturnsError() { // Arrange - Set up temp directory with invalid JSON file @@ -690,7 +688,7 @@ public void VersionMark_PublishCommandWithInvalidJson_ReturnsError() // Assert - Verify command failed with appropriate error // What is proved: Invalid JSON results in an error - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("Error: Failed to parse JSON file", output); } finally @@ -708,7 +706,7 @@ public void VersionMark_PublishCommandWithInvalidJson_ReturnsError() /// What is tested: PUB-005 - Custom glob patterns after -- separator /// What the assertions prove: Custom patterns correctly filter JSON files /// - [TestMethod] + [Fact] public void VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles() { // Arrange - Set up temp directory with multiple JSON files @@ -736,7 +734,7 @@ public void VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles() // Act - Run publish command with custom glob pattern var exitCode = Runner.Run( - out var output, + out var _, "dotnet", _dllPath, "--publish", @@ -745,8 +743,8 @@ public void VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles() // Assert - Verify only matching files were included // What is proved: Custom glob patterns correctly filter input files - Assert.AreEqual(0, exitCode, $"Command failed with output: {output}"); - Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); + Assert.Equal(0, exitCode); + Assert.True(File.Exists(reportFile), "Report file was not created"); var reportContent = File.ReadAllText(reportFile); Assert.Contains("tool1", reportContent); // From included file @@ -765,7 +763,7 @@ public void VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles() /// /// Integration test that lint command passes for a valid config file. /// - [TestMethod] + [Fact] public void IntegrationTest_LintFlag_ValidConfig_ReturnsSuccess() { // Arrange - Set up temp directory with a valid config file @@ -790,8 +788,8 @@ public void IntegrationTest_LintFlag_ValidConfig_ReturnsSuccess() "--lint", configFile); // Assert - Verify success with no output (lint mode is silent when no issues) - Assert.AreEqual(0, exitCode, $"Command failed with output: {output}"); - Assert.IsTrue(string.IsNullOrEmpty(output), "Lint mode should produce no output when there are no issues"); + Assert.Equal(0, exitCode); + Assert.True(string.IsNullOrEmpty(output), "Lint mode should produce no output when there are no issues"); } finally { @@ -805,7 +803,7 @@ public void IntegrationTest_LintFlag_ValidConfig_ReturnsSuccess() /// /// Integration test that lint command reports errors for an invalid config file. /// - [TestMethod] + [Fact] public void IntegrationTest_LintFlag_InvalidConfig_ReturnsError() { // Arrange - Set up temp directory with an invalid config file @@ -831,7 +829,7 @@ public void IntegrationTest_LintFlag_InvalidConfig_ReturnsError() "--lint", configFile); // Assert - Verify error is reported - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("error", output); } finally @@ -846,7 +844,7 @@ public void IntegrationTest_LintFlag_InvalidConfig_ReturnsError() /// /// Integration test that lint command reports error for missing config file. /// - [TestMethod] + [Fact] public void IntegrationTest_LintFlag_MissingConfig_ReturnsError() { // Arrange - Use a path that doesn't exist @@ -860,7 +858,7 @@ public void IntegrationTest_LintFlag_MissingConfig_ReturnsError() "--lint", nonExistentFile); // Assert - Verify error is reported - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); Assert.Contains("error", output); } } diff --git a/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs b/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs index 63d633a..5fa94a6 100644 --- a/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.VersionMark.Tests; /// /// Unit tests for the Program class. /// -[TestClass] public class ProgramTests { /// /// Test that Run with version flag displays version only. /// - [TestMethod] + [Fact] public void Program_Run_WithVersionFlag_DisplaysVersionOnly() { // Arrange - Redirect console output @@ -49,7 +48,7 @@ public void Program_Run_WithVersionFlag_DisplaysVersionOnly() // Assert - Verify version-only output var output = outWriter.ToString(); - Assert.IsFalse(string.IsNullOrWhiteSpace(output), "Version string should be printed"); + Assert.False(string.IsNullOrWhiteSpace(output), "Version string should be printed"); Assert.DoesNotContain("Copyright", output); Assert.DoesNotContain("VersionMark version", output); } @@ -62,7 +61,7 @@ public void Program_Run_WithVersionFlag_DisplaysVersionOnly() /// /// Test that Run with help flag displays usage information. /// - [TestMethod] + [Fact] public void Program_Run_WithHelpFlag_DisplaysUsageInformation() { // Arrange - Redirect console output @@ -92,7 +91,7 @@ public void Program_Run_WithHelpFlag_DisplaysUsageInformation() /// /// Test that Run with validate flag runs validation. /// - [TestMethod] + [Fact] public void Program_Run_WithValidateFlag_RunsValidation() { // Arrange - Redirect console output @@ -119,7 +118,7 @@ public void Program_Run_WithValidateFlag_RunsValidation() /// /// Test that Run with no arguments displays default behavior. /// - [TestMethod] + [Fact] public void Program_Run_NoArguments_DisplaysDefaultBehavior() { // Arrange - Redirect console output @@ -147,20 +146,20 @@ public void Program_Run_NoArguments_DisplaysDefaultBehavior() /// /// Test that version property returns non-empty version string. /// - [TestMethod] + [Fact] public void Program_Version_ReturnsNonEmptyString() { // Arrange & Act - Get version property var version = Program.Version; // Assert - Verify version is non-empty - Assert.IsFalse(string.IsNullOrWhiteSpace(version)); + Assert.False(string.IsNullOrWhiteSpace(version)); } /// /// Test that Run with capture command captures tool versions. /// - [TestMethod] + [Fact] public void Program_Run_WithCaptureCommand_CapturesToolVersions() { // Arrange - Set up unique temp directory with config file and redirect console output @@ -205,15 +204,15 @@ public void Program_Run_WithCaptureCommand_CapturesToolVersions() Assert.Contains("Capturing tool versions", output); Assert.Contains("test-job", output); Assert.Contains("dotnet", output); - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); // Verify output file was created - Assert.IsTrue(File.Exists(outputFile), "Output file was not created"); + Assert.True(File.Exists(outputFile), "Output file was not created"); // Verify output file contains expected data var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.AreEqual("test-job", versionInfo.JobId); - Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet")); + Assert.Equal("test-job", versionInfo.JobId); + Assert.True(versionInfo.Versions.ContainsKey("dotnet")); } finally { @@ -236,7 +235,7 @@ public void Program_Run_WithCaptureCommand_CapturesToolVersions() /// /// Test that Run with capture command and no tool filter captures all configured tools. /// - [TestMethod] + [Fact] public void Program_Run_WithCaptureCommandNoToolFilter_CapturesAllConfiguredTools() { // Arrange - Set up unique temp directory with a two-tool config; do NOT specify tool names @@ -278,13 +277,13 @@ public void Program_Run_WithCaptureCommandNoToolFilter_CapturesAllConfiguredTool Program.Run(context); // Assert - All tools defined in configuration must be captured - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(outputFile), "Output file was not created"); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(outputFile), "Output file was not created"); var versionInfo = VersionInfo.LoadFromFile(outputFile); - Assert.AreEqual("test-job", versionInfo.JobId); - Assert.IsTrue(versionInfo.Versions.ContainsKey("dotnet"), "Expected 'dotnet' to be captured"); - Assert.IsTrue(versionInfo.Versions.ContainsKey("git"), "Expected 'git' to be captured"); + Assert.Equal("test-job", versionInfo.JobId); + Assert.True(versionInfo.Versions.ContainsKey("dotnet"), "Expected 'dotnet' to be captured"); + Assert.True(versionInfo.Versions.ContainsKey("git"), "Expected 'git' to be captured"); } finally { @@ -305,7 +304,7 @@ public void Program_Run_WithCaptureCommandNoToolFilter_CapturesAllConfiguredTool /// /// Test that Run with capture command without job ID fails. /// - [TestMethod] + [Fact] public void Program_Run_WithCaptureCommandWithoutJobId_ReturnsError() { // Arrange - Set up context with capture flag but no job-id @@ -325,7 +324,7 @@ public void Program_Run_WithCaptureCommandWithoutJobId_ReturnsError() // Assert - Verify error message on stderr and non-zero exit code var errorOutput = errWriter.ToString(); Assert.Contains("--job-id is required", errorOutput); - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -337,7 +336,7 @@ public void Program_Run_WithCaptureCommandWithoutJobId_ReturnsError() /// /// Test that Run with capture command with missing config file fails. /// - [TestMethod] + [Fact] public void Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError() { // Arrange - Create an isolated temp directory with no .versionmark.yaml present @@ -367,7 +366,7 @@ public void Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError() // Assert - Verify error is reported on stderr and exit code indicates failure var errorOutput = errWriter.ToString(); Assert.Contains("error:", errorOutput); - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -391,7 +390,7 @@ public void Program_Run_WithCaptureCommandWithMissingConfig_ReturnsError() /// What is tested: PUB-004 - --report parameter is required in publish mode /// What the assertions prove: Program exits with error when --report is missing /// - [TestMethod] + [Fact] public void Program_Run_WithPublishCommandWithoutReport_ReturnsError() { // Arrange - Set up context without --report parameter @@ -412,7 +411,7 @@ public void Program_Run_WithPublishCommandWithoutReport_ReturnsError() // What is proved: --publish without --report results in an error var errorOutput = errWriter.ToString(); Assert.Contains("Error: --report is required for publish mode", errorOutput); - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -426,7 +425,7 @@ public void Program_Run_WithPublishCommandWithoutReport_ReturnsError() /// What is tested: PUB-007 - Error reported when no JSON files match glob patterns /// What the assertions prove: Program exits with error when no files are found /// - [TestMethod] + [Fact] public void Program_Run_WithPublishCommandNoMatchingFiles_ReturnsError() { // Arrange - Set up unique temp directory with no JSON files @@ -460,7 +459,7 @@ public void Program_Run_WithPublishCommandNoMatchingFiles_ReturnsError() // What is proved: No matching files results in an error var errorOutput = errWriter.ToString(); Assert.Contains("Error: No JSON files found matching patterns:", errorOutput); - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -483,7 +482,7 @@ public void Program_Run_WithPublishCommandNoMatchingFiles_ReturnsError() /// What is tested: PUB-008 - Error reported when JSON files cannot be parsed /// What the assertions prove: Program exits with error when JSON is malformed /// - [TestMethod] + [Fact] public void Program_Run_WithPublishCommandInvalidJson_ReturnsError() { // Arrange - Set up unique temp directory with invalid JSON file @@ -520,7 +519,7 @@ public void Program_Run_WithPublishCommandInvalidJson_ReturnsError() // What is proved: Invalid JSON results in an error var errorOutput = errWriter.ToString(); Assert.Contains("Error: Failed to parse JSON file", errorOutput); - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -543,7 +542,7 @@ public void Program_Run_WithPublishCommandInvalidJson_ReturnsError() /// What is tested: PUB-001, PUB-002, PUB-005, PUB-006, FMT-001 - Publish mode generates report /// What the assertions prove: Valid JSON files are processed and markdown report is created /// - [TestMethod] + [Fact] public void Program_Run_WithPublishCommand_GeneratesMarkdownReport() { // Arrange - Set up unique temp directory with multiple JSON files @@ -592,8 +591,8 @@ public void Program_Run_WithPublishCommand_GeneratesMarkdownReport() // Assert - Verify report was created successfully // What is proved: Publish mode generates a markdown report from JSON files - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(reportFile), "Report file was not created"); var reportContent = File.ReadAllText(reportFile); Assert.Contains("# Tool Versions", reportContent); @@ -621,7 +620,7 @@ public void Program_Run_WithPublishCommand_GeneratesMarkdownReport() /// What is tested: PUB-003, FMT-005 - Report depth controls markdown heading levels /// What the assertions prove: Custom report depth is applied to generated markdown /// - [TestMethod] + [Fact] public void Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels() { // Arrange - Set up unique temp directory with JSON file @@ -657,8 +656,8 @@ public void Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels() // Assert - Verify report uses custom heading depth // What is proved: --report-depth parameter controls markdown heading level - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(reportFile), "Report file was not created"); + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(reportFile), "Report file was not created"); var reportContent = File.ReadAllText(reportFile); Assert.Contains("### Tool Versions", reportContent); @@ -681,7 +680,7 @@ public void Program_Run_WithPublishCommandCustomDepth_AdjustsHeadingLevels() /// /// Test that Run with lint flag passes for a valid config file. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_ValidConfig_ReturnsSuccess() { // Arrange - Set up temp directory with a valid config file @@ -711,9 +710,9 @@ public void Program_Run_WithLintFlag_ValidConfig_ReturnsSuccess() Program.Run(context); // Assert - Verify success with no output (lint mode is silent when no issues) - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var output = outWriter.ToString(); - Assert.IsTrue(string.IsNullOrEmpty(output), "Lint mode should produce no output when there are no issues"); + Assert.True(string.IsNullOrEmpty(output), "Lint mode should produce no output when there are no issues"); } finally { @@ -729,7 +728,7 @@ public void Program_Run_WithLintFlag_ValidConfig_ReturnsSuccess() /// /// Test that Run with lint flag fails for an invalid config file. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_InvalidConfig_ReturnsError() { // Arrange - Set up temp directory with an invalid config file @@ -760,7 +759,7 @@ public void Program_Run_WithLintFlag_InvalidConfig_ReturnsError() Program.Run(context); // Assert - Verify error is reported - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); var errorOutput = errWriter.ToString(); Assert.Contains("error", errorOutput); } @@ -778,7 +777,7 @@ public void Program_Run_WithLintFlag_InvalidConfig_ReturnsError() /// /// Test that Run with lint flag without file uses default .versionmark.yaml. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_NoFile_UsesDefaultConfigFile() { // Arrange - Set up temp directory with a default config file @@ -810,8 +809,8 @@ public void Program_Run_WithLintFlag_NoFile_UsesDefaultConfigFile() Program.Run(context); // Assert - Verify it found and linted the default file (exit code 0 means success) - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(string.IsNullOrEmpty(outWriter.ToString()), + Assert.Equal(0, context.ExitCode); + Assert.True(string.IsNullOrEmpty(outWriter.ToString()), "Lint mode should produce no output when there are no issues"); } finally @@ -829,7 +828,7 @@ public void Program_Run_WithLintFlag_NoFile_UsesDefaultConfigFile() /// /// Test that Run with lint flag suppresses the application banner. /// - [TestMethod] + [Fact] public void Program_Run_WithLintFlag_ValidConfig_SuppressesBanner() { // Arrange - Set up temp directory with a valid config file @@ -859,10 +858,10 @@ public void Program_Run_WithLintFlag_ValidConfig_SuppressesBanner() Program.Run(context); // Assert - Verify the banner is not present in the output - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var output = outWriter.ToString(); - Assert.DoesNotContain("VersionMark version", output, "Banner should be suppressed in lint mode"); - Assert.DoesNotContain("Copyright", output, "Banner should be suppressed in lint mode"); + Assert.DoesNotContain("VersionMark version", output); + Assert.DoesNotContain("Copyright", output); } finally { @@ -878,7 +877,7 @@ public void Program_Run_WithLintFlag_ValidConfig_SuppressesBanner() /// /// Test that Run with lint flag outputs lint information in help. /// - [TestMethod] + [Fact] public void Program_Run_WithHelpFlag_IncludesLintInformation() { // Arrange - Redirect console output diff --git a/test/DemaConsulting.VersionMark.Tests/Publishing/MarkdownFormatterTests.cs b/test/DemaConsulting.VersionMark.Tests/Publishing/MarkdownFormatterTests.cs index 75f8cd6..3393f26 100644 --- a/test/DemaConsulting.VersionMark.Tests/Publishing/MarkdownFormatterTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Publishing/MarkdownFormatterTests.cs @@ -26,7 +26,6 @@ namespace DemaConsulting.VersionMark.Tests.Publishing; /// /// Unit tests for the MarkdownFormatter class. /// -[TestClass] public class MarkdownFormatterTests { /// @@ -34,7 +33,7 @@ public class MarkdownFormatterTests /// What is tested: FMT-001 - Tools are sorted in case-insensitive alphabetical order /// What the assertions prove: The output lists tools in the correct alphabetical sequence /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_SortsToolsAlphabetically() { // Arrange - Create VersionInfo with tools in non-alphabetical order @@ -59,7 +58,7 @@ public void MarkdownFormatter_FormatVersions_SortsToolsAlphabetically() var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries); var toolLines = lines.Where(l => l.StartsWith("- **")).ToArray(); - Assert.HasCount(4, toolLines); + Assert.Equal(4, toolLines.Length); Assert.Contains("- **dotnet**:", toolLines[0]); Assert.Contains("- **node**:", toolLines[1]); Assert.Contains("- **python**:", toolLines[2]); @@ -71,7 +70,7 @@ public void MarkdownFormatter_FormatVersions_SortsToolsAlphabetically() /// What is tested: FMT-002 - Versions that are the same across all jobs show just the version /// What the assertions prove: The output displays only the version when all jobs have the same version /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_WithUniformVersions_ShowsVersionOnly() { // Arrange - Create multiple VersionInfos with the same version across jobs @@ -119,7 +118,7 @@ public void MarkdownFormatter_FormatVersions_WithUniformVersions_ShowsVersionOnl /// What is tested: FMT-003, FMT-004 - Different versions show job IDs in parentheses /// What the assertions prove: The output displays job IDs in parentheses when versions differ /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividualJobs() { // Arrange - Create VersionInfos with different versions across jobs @@ -168,7 +167,7 @@ public void MarkdownFormatter_FormatVersions_WithDifferentVersions_ShowsIndividu /// What is tested: FMT-005 - The report-depth parameter controls markdown heading levels /// What the assertions prove: The output heading level matches the specified depth /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_WithCustomDepth_UsesCorrectHeadingLevel() { // Arrange - Create simple VersionInfo @@ -208,7 +207,7 @@ public void MarkdownFormatter_FormatVersions_WithCustomDepth_UsesCorrectHeadingL /// What is tested: Edge case - empty input list /// What the assertions prove: The formatter produces valid output with just the header /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_EmptyList_ProducesHeaderOnly() { // Arrange - Create empty VersionInfo list @@ -228,7 +227,7 @@ public void MarkdownFormatter_FormatVersions_EmptyList_ProducesHeaderOnly() /// What is tested: Edge case - single job shows just the version /// What the assertions prove: Single job is treated as uniform (shows version only) /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_SingleJob_ShowsAllJobs() { // Arrange - Create single VersionInfo @@ -258,7 +257,7 @@ public void MarkdownFormatter_FormatVersions_SingleJob_ShowsAllJobs() /// What is tested: Some tools uniform, some tools different versions across jobs /// What the assertions prove: The formatter correctly handles both uniform and varying versions /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_MixedVersions_HandlesCorrectly() { // Arrange - Create VersionInfos with some tools uniform, some different @@ -298,7 +297,7 @@ public void MarkdownFormatter_FormatVersions_MixedVersions_HandlesCorrectly() /// What is tested: Job IDs in parentheses are sorted alphabetically within each version group /// What the assertions prove: Job IDs appear in alphabetical order within version groups /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_SortsJobIdsAlphabetically() { // Arrange - Create VersionInfos where two jobs share a version and one job has a different version @@ -329,7 +328,7 @@ public void MarkdownFormatter_FormatVersions_SortsJobIdsAlphabetically() /// What is tested: Version strings with hyphens, plus signs, and other special characters /// What the assertions prove: Special characters in versions are preserved in output /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_WithSpecialCharacters_PreservesVersions() { // Arrange - Create VersionInfo with special version strings @@ -360,7 +359,7 @@ public void MarkdownFormatter_FormatVersions_WithSpecialCharacters_PreservesVers /// What is tested: Tool names with different cases are sorted case-insensitively /// What the assertions prove: Sorting is case-insensitive (Dotnet comes before node) /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_CaseInsensitiveSorting() { // Arrange - Create VersionInfo with mixed-case tool names @@ -385,7 +384,7 @@ public void MarkdownFormatter_FormatVersions_CaseInsensitiveSorting() var lines = result.Split('\n', StringSplitOptions.RemoveEmptyEntries); var toolLines = lines.Where(l => l.StartsWith("- **")).ToArray(); - Assert.HasCount(4, toolLines); + Assert.Equal(4, toolLines.Length); Assert.Contains("- **dotnet**:", toolLines[0]); Assert.Contains("- **Node**:", toolLines[1]); Assert.Contains("- **PYTHON**:", toolLines[2]); @@ -397,7 +396,7 @@ public void MarkdownFormatter_FormatVersions_CaseInsensitiveSorting() /// What is tested: Multiple different versions are displayed in sorted order /// What the assertions prove: Version groups are sorted alphabetically /// - [TestMethod] + [Fact] public void MarkdownFormatter_FormatVersions_SortsVersionsAlphabetically() { // Arrange - Create VersionInfos with multiple different versions @@ -423,7 +422,7 @@ public void MarkdownFormatter_FormatVersions_SortsVersionsAlphabetically() var toolLines = lines.Where(l => l.StartsWith("- **tool**:")).ToArray(); // Should have 3 separate bullets for the 3 different versions - Assert.HasCount(3, toolLines); + Assert.Equal(3, toolLines.Length); // Verify they appear in sorted order: 1.0.0, 2.0.0, 3.0.0 Assert.Contains("1.0.0", toolLines[0]); @@ -436,7 +435,7 @@ public void MarkdownFormatter_FormatVersions_SortsVersionsAlphabetically() /// What is tested: Boundary behavior for invalid reportDepth values /// What the assertions prove: An exception is thrown before any output is generated /// - [TestMethod] + [Fact] public void MarkdownFormatter_Format_WithZeroDepth_ThrowsArgumentOutOfRangeException() { // Arrange - Create a simple VersionInfo to use with the invalid depth @@ -446,11 +445,11 @@ public void MarkdownFormatter_Format_WithZeroDepth_ThrowsArgumentOutOfRangeExcep }; // Act & Assert - Depth of zero should throw before generating any output - Assert.ThrowsExactly( + Assert.Throws( () => MarkdownFormatter.Format(versionInfos, reportDepth: 0)); // A negative depth should also throw - Assert.ThrowsExactly( + Assert.Throws( () => MarkdownFormatter.Format(versionInfos, reportDepth: -1)); } @@ -459,7 +458,7 @@ public void MarkdownFormatter_Format_WithZeroDepth_ThrowsArgumentOutOfRangeExcep /// What is tested: A tool present in some jobs but not all is included in the output /// What the assertions prove: Tools from all contributing jobs appear; the version is shown without job IDs when only one job has it /// - [TestMethod] + [Fact] public void MarkdownFormatter_Format_WithPartialToolCoverage_ShowsAllContributingTools() { // Arrange - job-1 has dotnet only; job-2 has both dotnet and node diff --git a/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingTests.cs b/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingTests.cs index 78bcd3f..51405d8 100644 --- a/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/Publishing/PublishingTests.cs @@ -28,13 +28,12 @@ namespace DemaConsulting.VersionMark.Tests.Publishing; /// /// Subsystem tests for the Publishing subsystem (capture data to markdown report pipeline). /// -[TestClass] public class PublishingTests { /// /// Test that the publishing pipeline produces a valid markdown report from multiple captures. /// - [TestMethod] + [Fact] public void Publishing_Format_MultipleCaptureFiles_ProducesConsolidatedReport() { // Arrange - Create version infos representing captures from multiple CI jobs @@ -58,17 +57,17 @@ public void Publishing_Format_MultipleCaptureFiles_ProducesConsolidatedReport() var report = MarkdownFormatter.Format(versionInfos); // Assert - The report should contain version information for all tools - Assert.IsFalse(string.IsNullOrWhiteSpace(report), + Assert.False(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"); + Assert.Contains("dotnet", report); + Assert.Contains("git", report); + Assert.Contains("8.0.100", report); } /// /// Test that the publishing pipeline consolidates identical versions across jobs. /// - [TestMethod] + [Fact] public void Publishing_Format_IdenticalVersionsAcrossJobs_ConsolidatesVersions() { // Arrange - Create version infos with the same dotnet version across all jobs @@ -83,15 +82,14 @@ public void Publishing_Format_IdenticalVersionsAcrossJobs_ConsolidatesVersions() 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"); + Assert.Contains("8.0.100", report); + Assert.DoesNotContain("job-1", report); } /// /// Test that the publishing pipeline shows individual job IDs when versions conflict across jobs. /// - [TestMethod] + [Fact] public void Publishing_Format_ConflictingVersions_ShowsJobIds() { // Arrange - Create two version infos with different versions for the same tool @@ -109,14 +107,14 @@ public void Publishing_Format_ConflictingVersions_ShowsJobIds() var report = MarkdownFormatter.Format(versionInfos); // Assert - Each job ID should appear in the report to attribute the conflicting versions - StringAssert.Contains(report, "job-a"); - StringAssert.Contains(report, "job-b"); + Assert.Contains("job-a", report); + Assert.Contains("job-b", report); } /// /// Test that the publishing pipeline uses the correct heading level when a custom report depth is specified. /// - [TestMethod] + [Fact] public void Publishing_Format_WithCustomDepth_UsesCorrectHeadingLevel() { // Arrange - Create a simple version info to exercise the heading depth parameter @@ -130,13 +128,13 @@ public void Publishing_Format_WithCustomDepth_UsesCorrectHeadingLevel() var report = MarkdownFormatter.Format(versionInfos, reportDepth: 3); // Assert - The heading prefix should match the requested depth - StringAssert.Contains(report, "###"); + Assert.Contains("###", report); } /// /// Test that the publishing pipeline requires the --report parameter and reports an error when it is missing. /// - [TestMethod] + [Fact] public void Publishing_Run_WithoutReport_ReportsError() { // Arrange - Create a publish context without --report @@ -151,10 +149,8 @@ public void Publishing_Run_WithoutReport_ReportsError() 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"); + Assert.Equal(1, context.ExitCode); + Assert.Contains("--report", errWriter.ToString()); } finally { @@ -165,7 +161,7 @@ public void Publishing_Run_WithoutReport_ReportsError() /// /// Test that the publishing pipeline accepts glob patterns after -- and reads all matching files. /// - [TestMethod] + [Fact] public void Publishing_Run_WithGlobPattern_ReadsMatchingFiles() { // Arrange - Create a temp directory with JSON files and use a glob pattern to match them @@ -187,12 +183,10 @@ public void Publishing_Run_WithGlobPattern_ReadsMatchingFiles() 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), + Assert.Equal(0, context.ExitCode); + Assert.True(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"); + Assert.Contains("dotnet", File.ReadAllText(reportFile)); } finally { @@ -207,7 +201,7 @@ public void Publishing_Run_WithGlobPattern_ReadsMatchingFiles() /// /// Test that the publishing pipeline reports an error when a JSON file is malformed. /// - [TestMethod] + [Fact] public void Publishing_Run_WithMalformedJsonFile_ReportsError() { // Arrange - Create a temp directory with a malformed JSON file @@ -235,8 +229,7 @@ public void Publishing_Run_WithMalformedJsonFile_ReportsError() 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"); + Assert.Equal(1, context.ExitCode); } finally { @@ -256,7 +249,7 @@ public void Publishing_Run_WithMalformedJsonFile_ReportsError() /// /// Test that the publishing pipeline reports an error when no JSON files match the glob pattern. /// - [TestMethod] + [Fact] public void Publishing_Run_WithGlobPatternMatchingNoFiles_ReportsError() { // Arrange - Create a temp directory with no JSON files matching the pattern @@ -281,9 +274,8 @@ public void Publishing_Run_WithGlobPatternMatchingNoFiles_ReportsError() Program.Run(context); // Assert - An error should be reported and exit code should be non-zero - Assert.AreEqual(1, context.ExitCode, - "Publishing with no matching files should result in a non-zero exit code"); - Assert.IsTrue( + Assert.Equal(1, context.ExitCode); + Assert.True( errWriter.ToString().Length > 0, "An error message should be written when no files match the glob pattern"); } @@ -305,7 +297,7 @@ public void Publishing_Run_WithGlobPatternMatchingNoFiles_ReportsError() /// /// Test that the --report-depth parameter is applied end-to-end through Context.Create and Program.Run. /// - [TestMethod] + [Fact] public void Publishing_Run_WithReportDepth_UsesCorrectDepth() { // Arrange - Create a temp directory with a JSON file and run with --report-depth 3 @@ -327,12 +319,10 @@ public void Publishing_Run_WithReportDepth_UsesCorrectDepth() Program.Run(context); // Assert - The report heading should use depth-3 prefix "###" - Assert.AreEqual(0, context.ExitCode, - "Publishing with --report-depth should succeed"); - Assert.IsTrue(File.Exists(reportFile), + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(reportFile), "Report file should be created"); - StringAssert.Contains(File.ReadAllText(reportFile), "###", - "Report heading should use the depth-3 heading prefix specified via --report-depth"); + Assert.Contains("###", File.ReadAllText(reportFile)); } finally { diff --git a/test/DemaConsulting.VersionMark.Tests/SelfTest/PathHelpersTests.cs b/test/DemaConsulting.VersionMark.Tests/SelfTest/PathHelpersTests.cs index d97e037..ff86e1e 100644 --- a/test/DemaConsulting.VersionMark.Tests/SelfTest/PathHelpersTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/SelfTest/PathHelpersTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.VersionMark.Tests.SelfTest; /// /// Tests for the PathHelpers class. /// -[TestClass] public class PathHelpersTests { /// /// Test that SafePathCombine throws ArgumentNullException when basePath is null. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException() { // Arrange @@ -39,14 +38,14 @@ public void PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException var relativePath = "subfolder/file.txt"; // Act & Assert - Assert.ThrowsExactly(() => + Assert.Throws(() => PathHelpers.SafePathCombine(basePath!, relativePath)); } /// /// Test that SafePathCombine throws ArgumentNullException when relativePath is null. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException() { // Arrange @@ -54,14 +53,14 @@ public void PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullExcep string? relativePath = null; // Act & Assert - Assert.ThrowsExactly(() => + Assert.Throws(() => PathHelpers.SafePathCombine(basePath, relativePath!)); } /// /// Test that SafePathCombine correctly combines valid paths. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly() { // Arrange @@ -72,13 +71,13 @@ public void PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly() var result = PathHelpers.SafePathCombine(basePath, relativePath); // Assert - Assert.AreEqual(Path.Combine(basePath, relativePath), result); + Assert.Equal(Path.Combine(basePath, relativePath), result); } /// /// Test that SafePathCombine throws ArgumentException for path traversal with double dots. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException() { // Arrange @@ -86,7 +85,7 @@ public void PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgume var relativePath = "../etc/passwd"; // Act & Assert - var exception = Assert.ThrowsExactly(() => + var exception = Assert.Throws(() => PathHelpers.SafePathCombine(basePath, relativePath)); Assert.Contains("Invalid path component", exception.Message); } @@ -94,7 +93,7 @@ public void PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgume /// /// Test that SafePathCombine throws ArgumentException for path with double dots in middle. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentException() { // Arrange @@ -102,7 +101,7 @@ public void PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentExcepti var relativePath = "subfolder/../../../etc/passwd"; // Act & Assert - var exception = Assert.ThrowsExactly(() => + var exception = Assert.Throws(() => PathHelpers.SafePathCombine(basePath, relativePath)); Assert.Contains("Invalid path component", exception.Message); } @@ -110,13 +109,13 @@ public void PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentExcepti /// /// Test that SafePathCombine throws ArgumentException for absolute paths. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException() { // Arrange & Act - Test Unix absolute path var unixBasePath = "/home/user/project"; var unixRelativePath = "/etc/passwd"; - var unixException = Assert.ThrowsExactly(() => + var unixException = Assert.Throws(() => PathHelpers.SafePathCombine(unixBasePath, unixRelativePath)); // Assert - Verify exception is thrown for Unix absolute path @@ -127,7 +126,7 @@ public void PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException() { var windowsBasePath = "C:\\Users\\project"; var windowsRelativePath = "C:\\Windows\\System32\\file.txt"; - var windowsException = Assert.ThrowsExactly(() => + var windowsException = Assert.Throws(() => PathHelpers.SafePathCombine(windowsBasePath, windowsRelativePath)); Assert.Contains("Invalid path component", windowsException.Message); } @@ -136,7 +135,7 @@ public void PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException() /// /// Test that SafePathCombine correctly handles current directory reference. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrectly() { // Arrange @@ -147,13 +146,13 @@ public void PathHelpers_SafePathCombine_CurrentDirectoryReference_CombinesCorrec var result = PathHelpers.SafePathCombine(basePath, relativePath); // Assert - Assert.AreEqual(Path.Combine(basePath, relativePath), result); + Assert.Equal(Path.Combine(basePath, relativePath), result); } /// /// Test that SafePathCombine correctly handles nested paths. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly() { // Arrange @@ -164,13 +163,13 @@ public void PathHelpers_SafePathCombine_NestedPaths_CombinesCorrectly() var result = PathHelpers.SafePathCombine(basePath, relativePath); // Assert - Assert.AreEqual(Path.Combine(basePath, relativePath), result); + Assert.Equal(Path.Combine(basePath, relativePath), result); } /// /// Test that SafePathCombine correctly handles empty relative path. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath() { // Arrange @@ -181,14 +180,14 @@ public void PathHelpers_SafePathCombine_EmptyRelativePath_ReturnsBasePath() var result = PathHelpers.SafePathCombine(basePath, relativePath); // Assert - Assert.AreEqual(Path.Combine(basePath, relativePath), result); + Assert.Equal(Path.Combine(basePath, relativePath), result); } /// /// Test that SafePathCombine correctly handles paths where a directory name starts with "..". /// Such names are valid and must not be rejected as false positives. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_DotDotAsNamePrefix_CombinesCorrectly() { // Arrange - "..data" is a valid directory name that starts with ".." but is not a traversal segment @@ -199,6 +198,6 @@ public void PathHelpers_SafePathCombine_DotDotAsNamePrefix_CombinesCorrectly() var result = PathHelpers.SafePathCombine(basePath, relativePath); // Assert - Assert.AreEqual(Path.Combine(basePath, relativePath), result); + Assert.Equal(Path.Combine(basePath, relativePath), result); } } diff --git a/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestTests.cs b/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestTests.cs index 9db3790..286b548 100644 --- a/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/SelfTest/SelfTestTests.cs @@ -26,13 +26,12 @@ namespace DemaConsulting.VersionMark.Tests.SelfTest; /// /// Subsystem tests for the SelfTest subsystem (Validation and PathHelpers working together). /// -[TestClass] public class SelfTestTests { /// /// Test that PathHelpers prevents path traversal attacks within the self-test subsystem context. /// - [TestMethod] + [Fact] public void SelfTest_PathHelpers_PathTraversal_ThrowsArgumentException() { // Arrange - Define a base directory and an attacker-controlled traversal path @@ -40,15 +39,14 @@ public void SelfTest_PathHelpers_PathTraversal_ThrowsArgumentException() 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"); + Assert.Throws(() => + PathHelpers.SafePathCombine(baseDir, traversalPath)); } /// /// Test that PathHelpers correctly combines valid paths within the self-test subsystem context. /// - [TestMethod] + [Fact] public void SelfTest_PathHelpers_ValidRelativePath_ProducesExpectedPath() { // Arrange - Use the application base directory as the root @@ -59,9 +57,9 @@ public void SelfTest_PathHelpers_ValidRelativePath_ProducesExpectedPath() var result = PathHelpers.SafePathCombine(baseDir, relativePath); // Assert - The combined path should be under the base directory - Assert.IsFalse(string.IsNullOrEmpty(result), + Assert.False(string.IsNullOrEmpty(result), "Valid path combination should produce a non-empty result"); - Assert.IsTrue(result.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase) || + Assert.True(result.StartsWith(baseDir, StringComparison.OrdinalIgnoreCase) || Path.GetFullPath(result).StartsWith(Path.GetFullPath(baseDir), StringComparison.OrdinalIgnoreCase), "Combined path should be rooted within the base directory"); } @@ -69,20 +67,20 @@ public void SelfTest_PathHelpers_ValidRelativePath_ProducesExpectedPath() /// /// Test that the self-test subsystem can locate the main DLL in the base directory. /// - [TestMethod] + [Fact] public void SelfTest_PathHelpers_FindsDllInBaseDirectory_FileExists() { // Arrange var dllPath = PathHelpers.SafePathCombine(AppContext.BaseDirectory, "DemaConsulting.VersionMark.dll"); // Act & Assert - Assert.IsTrue(File.Exists(dllPath)); + Assert.True(File.Exists(dllPath)); } /// /// Test that the self-validation pipeline writes results to a TRX file when --results is specified. /// - [TestMethod] + [Fact] public void SelfTest_Run_WithResultsFlag_WritesResultsFile() { // Arrange - Set up a TRX results file path @@ -95,11 +93,11 @@ public void SelfTest_Run_WithResultsFlag_WritesResultsFile() Validation.Run(context); // Assert - The TRX file should exist and contain XML content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(resultsFile), + Assert.Equal(0, context.ExitCode); + Assert.True(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"), + Assert.True(content.Contains("TestRun") || content.Contains("testsuites"), "Results file should contain TRX or JUnit test result data"); } finally @@ -114,7 +112,7 @@ public void SelfTest_Run_WithResultsFlag_WritesResultsFile() /// /// Test that the self-validation pipeline writes JUnit XML results when --results specifies a .xml file. /// - [TestMethod] + [Fact] public void SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile() { // Arrange - Set up a JUnit XML results file path @@ -127,11 +125,11 @@ public void SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile() Validation.Run(context); // Assert - The XML file should exist and contain JUnit content - Assert.AreEqual(0, context.ExitCode); - Assert.IsTrue(File.Exists(resultsFile), + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(resultsFile), "Self-validation should write JUnit results to the .xml file specified by --results"); var content = File.ReadAllText(resultsFile); - Assert.IsTrue(content.Contains("testsuites") || content.Contains("testsuite"), + Assert.True(content.Contains("testsuites") || content.Contains("testsuite"), "JUnit results file should contain testsuites element"); } finally @@ -148,7 +146,7 @@ public void SelfTest_Run_WithResultsXmlFlag_WritesJUnitResultsFile() /// What is tested: The --depth argument controls the heading level in the self-validation report /// What the assertions prove: Output contains "## DEMA Consulting VersionMark" with depth 2 /// - [TestMethod] + [Fact] public void SelfTest_Run_WithDepthTwo_WritesHashHashHeader() { // Arrange - Redirect console output to capture the validation report @@ -164,7 +162,7 @@ public void SelfTest_Run_WithDepthTwo_WritesHashHashHeader() // Assert - Output should contain the ## heading for depth 2 var output = writer.ToString(); - Assert.IsTrue(output.Contains("## DEMA Consulting VersionMark"), + Assert.True(output.Contains("## DEMA Consulting VersionMark"), "Self-validation report should use ## heading when --depth 2 is specified"); } finally