diff --git a/.cspell.yaml b/.cspell.yaml index 1ea91b09..1b319f18 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -19,8 +19,8 @@ words: - demaconsulting - Dema - fileassert - - mstest - myorg + - xunit - myproject - myrepo - Pandoc @@ -33,6 +33,11 @@ words: - sonarmark - versionmark - Weasyprint + - mylib + - opencover + - unconfigured + - unparseable + - tagobj - wiql - workitems - yamlfix @@ -45,6 +50,7 @@ ignorePaths: - "**/thirdparty/**" - "**/third-party/**" - "**/3rd-party/**" + - "**/generated/**" - "**/AGENT_REPORT_*.md" - "**/.agent-logs/**" - "**/bin/**" diff --git a/.fileassert.yaml b/.fileassert.yaml index f95fb47a..1e5ea7bc 100644 --- a/.fileassert.yaml +++ b/.fileassert.yaml @@ -1,7 +1,7 @@ --- # FileAssert document validation tests for BuildMark. # 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/BuildMark Build Notes.pdf" + - pattern: "docs/generated/BuildMark 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/BuildMark Code Quality.pdf" + - pattern: "docs/generated/BuildMark 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/BuildMark Review Plan.pdf" + - pattern: "docs/generated/BuildMark 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/BuildMark Review Report.pdf" + - pattern: "docs/generated/BuildMark 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/BuildMark Software Design.pdf" + - pattern: "docs/generated/BuildMark 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/BuildMark 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/BuildMark User Guide.pdf" + - pattern: "docs/generated/BuildMark User Guide.pdf" count: 1 pdf: metadata: @@ -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/BuildMark Requirements.pdf" + - pattern: "docs/generated/BuildMark 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/BuildMark Trace Matrix.pdf" + - pattern: "docs/generated/BuildMark Trace Matrix.pdf" count: 1 pdf: metadata: diff --git a/.github/agents/developer.agent.md b/.github/agents/developer.agent.md index 35f5dda3..a95c562f 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 88b06910..7dd8e845 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 83ad8cbb..549e751c 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 5dbe99f4..00000000 --- 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 61935413..82a413e9 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 tests pass: `dotnet test --configuration Release` -- [ ] Self-validation tests pass: - `dotnet run --project src/DemaConsulting.BuildMark --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,7 +38,7 @@ Before submitting this pull request, ensure you have completed the following: Please run the following checks before submitting: -- [ ] **All linters pass**: `./lint.sh` (Unix/macOS) or `cmd /c lint.bat` / `./lint.bat` (Windows) +- [ ] **All linters pass**: `pwsh ./lint.ps1` ### Testing @@ -55,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 213c0316..9e67fbb1 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 707b0f90..6df39cd4 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 1591eebe..181de025 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 30becb5a..3b448f32 100644 --- a/.github/standards/design-documentation.md +++ b/.github/standards/design-documentation.md @@ -108,6 +108,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 @@ -122,6 +129,7 @@ parallel directory trees: - Requirements: `docs/reqstream/{system}/.../{item}.yaml` (kebab-case) - Design docs: `docs/design/{system}/.../{item}.md` (kebab-case) +- Verification design: `docs/verification/{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` @@ -168,6 +176,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 diff --git a/.github/standards/reqstream-usage.md b/.github/standards/reqstream-usage.md index ae5e565a..58b08b4d 100644 --- a/.github/standards/reqstream-usage.md +++ b/.github/standards/reqstream-usage.md @@ -104,16 +104,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 diff --git a/.github/standards/requirements-principles.md b/.github/standards/requirements-principles.md index 7d2d5729..b6cf136c 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 5d6219e4..2f778dcd 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,6 +56,7 @@ 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: @@ -109,6 +111,8 @@ Reviews system architecture and operational validation: - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - System design: `docs/design/{system-name}/{system-name}.md` + - Verification introduction: `docs/verification/introduction.md` + - System verification design: `docs/verification/{system-name}/{system-name}.md` - System integration tests: `test/{SystemName}.Tests/{SystemName}Tests.{ext}` ## `{System}-Design` Review (one per system) @@ -147,6 +151,7 @@ Reviews subsystem architecture and interfaces: - **File Path Patterns**: - Requirements: `docs/reqstream/{system-name}/.../{subsystem-name}/{subsystem-name}.yaml` - Design: `docs/design/{system-name}/.../{subsystem-name}/{subsystem-name}.md` + - Verification design: `docs/verification/{system-name}/.../{subsystem-name}/{subsystem-name}.md` - Tests: `test/{SystemName}.Tests/.../{SubsystemName}/{SubsystemName}Tests.{ext}` ## `{System}-{Subsystem[-Child...]}-{Unit}` Review (one per unit) @@ -159,6 +164,7 @@ 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}` @@ -175,6 +181,9 @@ 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 - [ ] 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 bb67b1d3..4e5c90e5 100644 --- a/.github/standards/software-items.md +++ b/.github/standards/software-items.md @@ -84,11 +84,12 @@ Choose the appropriate category based on scope and testability: # 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 455b2fd4..7ff5b5ad 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,8 +68,17 @@ 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 @@ -135,6 +106,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 +140,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 00000000..f6f407fd --- /dev/null +++ b/.github/standards/verification-documentation.md @@ -0,0 +1,128 @@ +--- +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 # Verification overview +├── {system-name}/ # System-level verification folder (one per system) +│ ├── {system-name}.md # System-level verification design +│ ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively +│ │ ├── {subsystem-name}.md # Subsystem verification design +│ │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) +│ │ └── {unit-name}.md # Unit-level verification design documents +│ └── {unit-name}.md # Top-level unit verification documents (if not in subsystem) +└── ots/ # OTS items (one verification file per OTS item) + └── {ots-name}.md # Verification evidence for each OTS item +``` + +## 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}/.../{item}.yaml` (kebab-case) +- Design: `docs/design/{system}/.../{item}.md` (kebab-case) +- Verification: `docs/verification/{system}/.../{item}.md` (kebab-case) +- Source: `src/{System}/.../{Item}.{ext}` (cased per language) +- Tests: `test/{System}.Tests/.../{Item}Tests.{ext}` (cased per language) + +OTS items have parallel artifacts in: +- Requirements: `docs/reqstream/ots/{ots-name}.yaml` (kebab-case) +- Verification: `docs/verification/ots/{ots-name}.md` (kebab-case) +- Tests: `test/{OtsName}.Tests/...` (cased per language, if required) + +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 a kebab-case folder and `{system-name}.md` covering: + +- 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, create a kebab-case folder and `{subsystem-name}.md` covering: + +- 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) + +For each unit, create `{unit-name}.md` covering: + +- 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) + +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 +- [ ] Subsystem documentation folders use kebab-case names mirroring the source subsystem 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 +- [ ] Documents are integrated into ReviewMark review-sets for formal review diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b02eec01..3de17b66 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -472,6 +472,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. @@ -479,6 +488,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 @@ -498,20 +511,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" @@ -519,7 +532,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 @@ -529,14 +542,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/BuildMark Build Notes.pdf" + docs/build_notes/generated/build_notes.html + "docs/generated/BuildMark Build Notes.pdf" - name: Assert Build Notes Documents with FileAssert run: > @@ -544,6 +557,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. @@ -551,6 +568,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 @@ -567,7 +588,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 "BuildMark CodeQL Analysis" --report-depth 1 @@ -575,7 +596,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 @@ -587,14 +608,14 @@ jobs: --project-key demaconsulting_BuildMark --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 @@ -604,14 +625,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/BuildMark Code Quality.pdf" + docs/code_quality/generated/quality.html + "docs/generated/BuildMark Code Quality.pdf" - name: Assert Code Quality Documents with FileAssert run: > @@ -626,6 +647,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 @@ -637,22 +662,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 @@ -662,14 +687,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/BuildMark Review Plan.pdf" + docs/code_review_plan/generated/plan.html + "docs/generated/BuildMark Review Plan.pdf" - name: Generate Review Report HTML with Pandoc shell: bash @@ -679,14 +704,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/BuildMark Review Report.pdf" + docs/code_review_report/generated/report.html + "docs/generated/BuildMark Review Report.pdf" - name: Assert Code Review Documents with FileAssert run: > @@ -699,6 +724,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: > @@ -707,14 +736,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/BuildMark Software Design.pdf" + docs/design/generated/design.html + "docs/generated/BuildMark Software Design.pdf" - name: Assert Design Documents with FileAssert run: > @@ -722,11 +751,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/BuildMark 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: > @@ -735,14 +799,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/BuildMark User Guide.pdf" + docs/user_guide/generated/user_guide.html + "docs/generated/BuildMark User Guide.pdf" - name: Assert User Guide Documents with FileAssert run: > @@ -751,8 +815,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. @@ -772,6 +836,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 @@ -783,9 +851,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 @@ -796,14 +864,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/BuildMark Requirements.pdf" + docs/requirements_doc/generated/requirements.html + "docs/generated/BuildMark Requirements.pdf" - name: Generate Trace Matrix HTML with Pandoc shell: bash @@ -813,14 +881,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/BuildMark Trace Matrix.pdf" + docs/requirements_report/generated/trace_matrix.html + "docs/generated/BuildMark Trace Matrix.pdf" - name: Assert Requirements Documents with FileAssert run: > @@ -836,6 +904,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 227b3448..ee4cdc90 100644 --- a/.gitignore +++ b/.gitignore @@ -60,16 +60,7 @@ __pycache__/ .venv/ # Generated documentation -docs/**/*.html -docs/**/*.pdf -!docs/template/** -docs/requirements_doc/requirements.md -docs/requirements_doc/justifications.md -docs/requirements_report/trace_matrix.md -docs/code_quality/sonar-quality.md -docs/code_quality/codeql-quality.md -docs/build_notes.md -docs/build_notes/versions.md +**/generated/ # VersionMark captures (generated during CI/CD) versionmark-*.json diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index c16c4430..49427468 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 a30d57dc..c02eba20 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -1,19 +1,21 @@ --- # 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/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,11 +25,12 @@ evidence-source: type: url location: https://raw.githubusercontent.com/demaconsulting/BuildMark/reviews/index.json -# Review sets following standardized patterns for hierarchical compliance coverage +# Review sets following hierarchical scope principles. +# Each review-set focuses on a single compliance question with manageable file counts. reviews: - # Purpose Review (only one per repository) - - id: BuildMark-Purpose - title: Review that Advertised Features Match System Design + # Purpose + - id: Purpose + title: Review of user-facing capabilities and system promises paths: - "README.md" - "docs/user_guide/**/*.md" @@ -35,9 +38,9 @@ reviews: - "docs/design/introduction.md" - "docs/design/build-mark/build-mark.md" - # System-level reviews + # BuildMark - Specials - id: BuildMark-Architecture - title: Review that BuildMark Architecture Satisfies Requirements + title: Review of BuildMark system architecture and operational validation paths: - "docs/reqstream/build-mark/build-mark.yaml" - "docs/design/introduction.md" @@ -47,7 +50,7 @@ reviews: - "test/**/AssemblyInfo.cs" - id: BuildMark-Design - title: Review that BuildMark Design is Consistent and Complete + title: Review of BuildMark architectural and design consistency paths: - "docs/reqstream/build-mark/build-mark.yaml" - "docs/reqstream/build-mark/platform-requirements.yaml" @@ -55,92 +58,63 @@ reviews: - "docs/design/build-mark/**/*.md" - id: BuildMark-AllRequirements - title: Review that All BuildMark Requirements are Complete + title: Review of All BuildMark requirements quality and traceability paths: - "requirements.yaml" - "docs/reqstream/build-mark/**/*.yaml" - "docs/reqstream/ots/**/*.yaml" - # Subsystem reviews - - id: BuildMark-Cli - title: Review that BuildMark Cli Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/cli/cli.yaml" - - "docs/design/build-mark/cli/cli.md" - - "test/**/Cli/CliTests.cs" - - - id: BuildMark-SelfTest - title: Review that BuildMark SelfTest Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/self-test/self-test.yaml" - - "docs/design/build-mark/self-test/self-test.md" - - "test/**/SelfTest/SelfTestTests.cs" - - - id: BuildMark-Utilities - title: Review that BuildMark Utilities Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/utilities/utilities.yaml" - - "docs/design/build-mark/utilities/utilities.md" - - "test/**/Utilities/UtilitiesTests.cs" - - - id: BuildMark-Version - title: Review that BuildMark Version Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/version/version.yaml" - - "docs/design/build-mark/version/version.md" - - "test/**/Version/VersionTests.cs" - - - id: BuildMark-RepoConnectors - title: Review that BuildMark RepoConnectors Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml" - - "docs/design/build-mark/repo-connectors/repo-connectors.md" - - "test/**/RepoConnectors/RepoConnectorsTests.cs" - - - id: BuildMark-BuildNotes - title: Review that BuildMark BuildNotes Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/build-notes/build-notes.yaml" - - "docs/design/build-mark/build-notes/build-notes.md" - - "test/**/BuildNotes/BuildNotesTests.cs" - - - id: BuildMark-Configuration - title: Review that BuildMark Configuration Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/configuration/configuration.yaml" - - "docs/design/build-mark/configuration/configuration.md" - - "test/**/Configuration/ConfigurationSubsystemTests.cs" - - # Top-level unit reviews + # BuildMark - Program - id: BuildMark-Program - title: Review that BuildMark Program Implementation is Correct + title: Review of BuildMark Program unit implementation paths: - "docs/reqstream/build-mark/program.yaml" - "docs/design/build-mark/program.md" - "src/**/Program.cs" - "test/**/ProgramTests.cs" - # Unit reviews - Cli subsystem + # BuildMark - Cli + - id: BuildMark-Cli + title: Review of BuildMark Cli subsystem architecture and interfaces + paths: + - "docs/reqstream/build-mark/cli/cli.yaml" + - "docs/design/build-mark/cli/cli.md" + - "test/**/Cli/CliTests.cs" + - id: BuildMark-Cli-Context - title: Review that BuildMark Cli Context Implementation is Correct + title: Review of BuildMark Cli Context unit implementation paths: - "docs/reqstream/build-mark/cli/context.yaml" - "docs/design/build-mark/cli/context.md" - "src/**/Cli/Context.cs" - "test/**/Cli/ContextTests.cs" - # Unit reviews - SelfTest subsystem + # BuildMark - SelfTest + - id: BuildMark-SelfTest + title: Review that BuildMark SelfTest Satisfies Subsystem Requirements + paths: + - "docs/reqstream/build-mark/self-test/self-test.yaml" + - "docs/design/build-mark/self-test/self-test.md" + - "test/**/SelfTest/SelfTestTests.cs" + - id: BuildMark-SelfTest-Validation - title: Review that BuildMark SelfTest Validation Implementation is Correct + title: Review of BuildMark SelfTest Validation unit implementation paths: - "docs/reqstream/build-mark/self-test/validation.yaml" - "docs/design/build-mark/self-test/validation.md" - "src/**/SelfTest/Validation.cs" - "test/**/SelfTest/ValidationTests.cs" - # Unit reviews - Utilities subsystem + # BuildMark - Utilities + - id: BuildMark-Utilities + title: Review that BuildMark Utilities Satisfies Subsystem Requirements + paths: + - "docs/reqstream/build-mark/utilities/utilities.yaml" + - "docs/design/build-mark/utilities/utilities.md" + - "test/**/Utilities/UtilitiesTests.cs" + - id: BuildMark-Utilities-PathHelpers - title: Review that BuildMark Utilities PathHelpers Implementation is Correct + title: Review of BuildMark Utilities PathHelpers unit implementation paths: - "docs/reqstream/build-mark/utilities/path-helpers.yaml" - "docs/design/build-mark/utilities/path-helpers.md" @@ -148,16 +122,23 @@ reviews: - "test/**/Utilities/PathHelpersTests.cs" - id: BuildMark-Utilities-ProcessRunner - title: Review that BuildMark Utilities ProcessRunner Implementation is Correct + title: Review of BuildMark Utilities ProcessRunner unit implementation paths: - "docs/reqstream/build-mark/utilities/process-runner.yaml" - "docs/design/build-mark/utilities/process-runner.md" - "src/**/Utilities/ProcessRunner.cs" - "test/**/Utilities/ProcessRunnerTests.cs" - # Unit reviews - Version subsystem + # BuildMark - Version + - id: BuildMark-Version + title: Review that BuildMark Version Satisfies Subsystem Requirements + paths: + - "docs/reqstream/build-mark/version/version.yaml" + - "docs/design/build-mark/version/version.md" + - "test/**/Version/VersionTests.cs" + - id: BuildMark-Version-VersionComparable - title: Review that BuildMark Version VersionComparable Implementation is Correct + title: Review of BuildMark Version VersionComparable unit implementation paths: - "docs/reqstream/build-mark/version/version-comparable.yaml" - "docs/design/build-mark/version/version-comparable.md" @@ -165,7 +146,7 @@ reviews: - "test/**/Version/VersionComparableTests.cs" - id: BuildMark-Version-VersionSemantic - title: Review that BuildMark Version VersionSemantic Implementation is Correct + title: Review of BuildMark Version VersionSemantic unit implementation paths: - "docs/reqstream/build-mark/version/version-semantic.yaml" - "docs/design/build-mark/version/version-semantic.md" @@ -173,7 +154,7 @@ reviews: - "test/**/Version/VersionSemanticTests.cs" - id: BuildMark-Version-VersionTag - title: Review that BuildMark Version VersionTag Implementation is Correct + title: Review of BuildMark Version VersionTag unit implementation paths: - "docs/reqstream/build-mark/version/version-tag.yaml" - "docs/design/build-mark/version/version-tag.md" @@ -181,7 +162,7 @@ reviews: - "test/**/Version/VersionTagTests.cs" - id: BuildMark-Version-VersionInterval - title: Review that BuildMark Version VersionInterval Implementation is Correct + title: Review of BuildMark Version VersionInterval unit implementation paths: - "docs/reqstream/build-mark/version/version-interval.yaml" - "docs/design/build-mark/version/version-interval.md" @@ -191,17 +172,23 @@ reviews: - "test/**/Version/VersionIntervalSetTests.cs" - id: BuildMark-Version-VersionCommitTag - title: Review that BuildMark Version VersionCommitTag Implementation is Correct + title: Review of BuildMark Version VersionCommitTag unit implementation paths: - "docs/reqstream/build-mark/version/version-commit-tag.yaml" - "docs/design/build-mark/version/version-commit-tag.md" - "src/**/Version/VersionCommitTag.cs" - "test/**/BuildNotes/BuildInformationTests.cs" - # Unit reviews - BuildNotes subsystem + # BuildMark - BuildNotes + - id: BuildMark-BuildNotes + title: Review that BuildMark BuildNotes Satisfies Subsystem Requirements + paths: + - "docs/reqstream/build-mark/build-notes/build-notes.yaml" + - "docs/design/build-mark/build-notes/build-notes.md" + - "test/**/BuildNotes/BuildNotesTests.cs" - id: BuildMark-BuildNotes-BuildInformation - title: Review that BuildMark BuildNotes BuildInformation Implementation is Correct + title: Review of BuildMark BuildNotes BuildInformation unit implementation paths: - "docs/reqstream/build-mark/build-notes/build-information.yaml" - "docs/design/build-mark/build-notes/build-information.md" @@ -209,22 +196,30 @@ reviews: - "test/**/BuildNotes/BuildInformationTests.cs" - id: BuildMark-BuildNotes-ItemInfo - title: Review that BuildMark BuildNotes ItemInfo Implementation is Correct + title: Review of BuildMark BuildNotes ItemInfo unit implementation paths: - "docs/reqstream/build-mark/build-notes/item-info.yaml" - "docs/design/build-mark/build-notes/item-info.md" - "src/**/BuildNotes/ItemInfo.cs" - id: BuildMark-BuildNotes-WebLink - title: Review that BuildMark BuildNotes WebLink Implementation is Correct + title: Review of BuildMark BuildNotes WebLink unit implementation paths: - "docs/reqstream/build-mark/build-notes/web-link.yaml" - "docs/design/build-mark/build-notes/web-link.md" - "src/**/BuildNotes/WebLink.cs" - "test/**/BuildNotes/BuildInformationTests.cs" + # BuildMark - Configuration + - id: BuildMark-Configuration + title: Review that BuildMark Configuration Satisfies Subsystem Requirements + paths: + - "docs/reqstream/build-mark/configuration/configuration.yaml" + - "docs/design/build-mark/configuration/configuration.md" + - "test/**/Configuration/ConfigurationSubsystemTests.cs" + - id: BuildMark-Configuration-BuildMarkConfigReader - title: Review that BuildMark Configuration BuildMarkConfigReader Implementation is Correct + title: Review of BuildMark Configuration BuildMarkConfigReader unit implementation paths: - "docs/reqstream/build-mark/configuration/configuration.yaml" - "docs/design/build-mark/configuration/configuration.md" @@ -232,7 +227,7 @@ reviews: - "test/**/Configuration/ConfigurationTests.cs" - id: BuildMark-Configuration-ConfigurationLoadResult - title: Review that BuildMark Configuration ConfigurationLoadResult Implementation is Correct + title: Review of BuildMark Configuration ConfigurationLoadResult unit implementation paths: - "docs/reqstream/build-mark/configuration/configuration.yaml" - "docs/design/build-mark/configuration/configuration.md" @@ -241,7 +236,7 @@ reviews: - "test/**/Configuration/ConfigurationTests.cs" - id: BuildMark-Configuration-ConnectorConfig - title: Review that BuildMark Configuration ConnectorConfig Implementation is Correct + title: Review of BuildMark Configuration ConnectorConfig unit implementation paths: - "docs/reqstream/build-mark/configuration/configuration.yaml" - "docs/design/build-mark/configuration/configuration.md" @@ -254,41 +249,16 @@ reviews: - "src/**/Configuration/RuleConfig.cs" - "test/**/Configuration/ConfigurationTests.cs" - # Sub-subsystem reviews - RepoConnectors subsystem - - id: BuildMark-RepoConnectors-Mock - title: Review that BuildMark RepoConnectors Mock Satisfies Subsystem Requirements - paths: - - "docs/reqstream/build-mark/repo-connectors/mock/mock-repo-connector.yaml" - - "docs/design/build-mark/repo-connectors/mock/mock.md" - - "docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md" - - "src/**/RepoConnectors/Mock/MockRepoConnector.cs" - - "test/**/RepoConnectors/Mock/MockRepoConnectorTests.cs" - - - id: BuildMark-RepoConnectors-GitHub - title: Review that BuildMark RepoConnectors GitHub Satisfies Subsystem Requirements + # BuildMark - RepoConnectors + - id: BuildMark-RepoConnectors + title: Review that BuildMark RepoConnectors Satisfies Subsystem Requirements paths: - - "docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml" - - "docs/design/build-mark/repo-connectors/github/github.md" - - "docs/design/build-mark/repo-connectors/github/github-graphql-client.md" - - "docs/design/build-mark/repo-connectors/github/github-graphql-types.md" - - "docs/design/build-mark/repo-connectors/github/github-repo-connector.md" - - "src/**/RepoConnectors/GitHub/GitHubRepoConnector.cs" - - "src/**/RepoConnectors/GitHub/GitHubGraphQLClient.cs" - - "src/**/RepoConnectors/GitHub/GitHubGraphQLTypes.cs" - - "test/**/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs" - - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs" - - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetAllIssuesTests.cs" - - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetAllTagsTests.cs" - - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetCommitsTests.cs" - - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs" - - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetReleasesTests.cs" - - "test/**/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandler.cs" - - "test/**/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandlerTests.cs" - - "test/**/RepoConnectors/GitHub/MockableGitHubRepoConnector.cs" + - "docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml" + - "docs/design/build-mark/repo-connectors/repo-connectors.md" + - "test/**/RepoConnectors/RepoConnectorsTests.cs" - # Unit reviews - RepoConnectors subsystem - id: BuildMark-RepoConnectors-RepoConnectorBase - title: Review that BuildMark RepoConnectors RepoConnectorBase Implementation is Correct + title: Review of BuildMark RepoConnectors RepoConnectorBase unit implementation paths: - "docs/reqstream/build-mark/repo-connectors/repo-connector-base.yaml" - "docs/design/build-mark/repo-connectors/repo-connector-base.md" @@ -297,7 +267,7 @@ reviews: - "test/**/RepoConnectors/RepoConnectorBaseTests.cs" - id: BuildMark-RepoConnectors-RepoConnectorFactory - title: Review that BuildMark RepoConnectors RepoConnectorFactory Implementation is Correct + title: Review of BuildMark RepoConnectors RepoConnectorFactory unit implementation paths: - "docs/reqstream/build-mark/repo-connectors/repo-connector-factory.yaml" - "docs/design/build-mark/repo-connectors/repo-connector-factory.md" @@ -305,7 +275,7 @@ reviews: - "test/**/RepoConnectors/RepoConnectorFactoryTests.cs" - id: BuildMark-RepoConnectors-ItemControlsParser - title: Review that BuildMark RepoConnectors ItemControlsParser Implementation is Correct + title: Review of BuildMark RepoConnectors ItemControlsParser unit implementation paths: - "docs/reqstream/build-mark/repo-connectors/item-controls-parser.yaml" - "docs/design/build-mark/repo-connectors/item-controls-info.md" @@ -316,26 +286,92 @@ reviews: - "test/**/ItemControls/ItemControlsTests.cs" - id: BuildMark-RepoConnectors-ItemRouter - title: Review that BuildMark RepoConnectors ItemRouter Implementation is Correct + title: Review of BuildMark RepoConnectors ItemRouter unit implementation paths: - "docs/reqstream/build-mark/repo-connectors/item-router.yaml" - "docs/design/build-mark/repo-connectors/item-router.md" - "src/**/RepoConnectors/ItemRouter.cs" - "test/**/RepoConnectors/ItemRouterTests.cs" + - id: BuildMark-RepoConnectors-Mock + title: Review that BuildMark RepoConnectors Mock Satisfies Sub-Subsystem Requirements + paths: + - "docs/reqstream/build-mark/repo-connectors/mock/mock.yaml" + - "docs/design/build-mark/repo-connectors/mock/mock.md" + - "test/**/RepoConnectors/Mock/MockTests.cs" + + - id: BuildMark-RepoConnectors-Mock-MockRepoConnector + title: Review of BuildMark RepoConnectors Mock MockRepoConnector unit implementation + paths: + - "docs/reqstream/build-mark/repo-connectors/mock/mock-repo-connector.yaml" + - "docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md" + - "src/**/RepoConnectors/Mock/MockRepoConnector.cs" + - "test/**/RepoConnectors/Mock/MockRepoConnectorTests.cs" + + - id: BuildMark-RepoConnectors-GitHub + title: Review that BuildMark RepoConnectors GitHub Satisfies Sub-Subsystem Requirements + paths: + - "docs/reqstream/build-mark/repo-connectors/github/github.yaml" + - "docs/design/build-mark/repo-connectors/github/github.md" + - "test/**/RepoConnectors/GitHub/GitHubTests.cs" + + - id: BuildMark-RepoConnectors-GitHub-GitHubGraphQLClient + title: Review of BuildMark RepoConnectors GitHub GitHubGraphQLClient unit implementation + paths: + - "docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml" + - "docs/design/build-mark/repo-connectors/github/github-graphql-client.md" + - "docs/design/build-mark/repo-connectors/github/github-graphql-types.md" + - "src/**/RepoConnectors/GitHub/GitHubGraphQLClient.cs" + - "src/**/RepoConnectors/GitHub/GitHubGraphQLTypes.cs" + - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs" + - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetAllIssuesTests.cs" + - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetAllTagsTests.cs" + - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetCommitsTests.cs" + - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs" + - "test/**/RepoConnectors/GitHub/GitHubGraphQLClientGetReleasesTests.cs" + - "test/**/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandler.cs" + - "test/**/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandlerTests.cs" + + - id: BuildMark-RepoConnectors-GitHub-GitHubRepoConnector + title: Review of BuildMark RepoConnectors GitHub GitHubRepoConnector unit implementation + paths: + - "docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml" + - "docs/design/build-mark/repo-connectors/github/github-repo-connector.md" + - "src/**/RepoConnectors/GitHub/GitHubRepoConnector.cs" + - "test/**/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs" + - "test/**/RepoConnectors/GitHub/MockableGitHubRepoConnector.cs" + - id: BuildMark-RepoConnectors-AzureDevOps - title: Review that BuildMark RepoConnectors AzureDevOps Satisfies Subsystem Requirements + title: Review that BuildMark RepoConnectors AzureDevOps Satisfies Sub-Subsystem Requirements paths: - - "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml" + - "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml" - "docs/design/build-mark/repo-connectors/azure-devops/azure-devops.md" - - "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md" + - "test/**/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs" + + - id: BuildMark-RepoConnectors-AzureDevOps-AzureDevOpsRestClient + title: Review of BuildMark RepoConnectors AzureDevOps AzureDevOpsRestClient unit implementation + paths: + - "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml" - "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md" - "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md" - - "docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md" - - "src/**/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs" - "src/**/RepoConnectors/AzureDevOps/AzureDevOpsRestClient.cs" - "src/**/RepoConnectors/AzureDevOps/AzureDevOpsApiTypes.cs" + - "test/**/RepoConnectors/AzureDevOps/MockAzureDevOpsHttpMessageHandler.cs" + - "test/**/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs" + + - id: BuildMark-RepoConnectors-AzureDevOps-WorkItemMapper + title: Review of BuildMark RepoConnectors AzureDevOps WorkItemMapper unit implementation + paths: + - "docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml" + - "docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md" - "src/**/RepoConnectors/AzureDevOps/WorkItemMapper.cs" + - "test/**/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs" + + - id: BuildMark-RepoConnectors-AzureDevOps-AzureDevOpsRepoConnector + title: Review of BuildMark RepoConnectors AzureDevOps AzureDevOpsRepoConnector unit implementation + paths: + - "docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml" + - "docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md" + - "src/**/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs" - "test/**/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnectorTests.cs" - "test/**/RepoConnectors/AzureDevOps/MockableAzureDevOpsRepoConnector.cs" - - "test/**/RepoConnectors/AzureDevOps/MockAzureDevOpsHttpMessageHandler.cs" diff --git a/.yamllint.yaml b/.yamllint.yaml index 4fbc811b..79c3aee4 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 2b7dfac5..443a0853 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,10 @@ +# Project Overview + +- **name**: BuildMark +- **description**: Tool for generating Markdown build notes from repositories +- **languages**: C# +- **technologies**: DotNet, YamlDotNet, xUnit, GraphQL, Pandoc, WeasyPrint + # Project Structure ```text @@ -10,7 +17,8 @@ │ ├── requirements_doc/ │ ├── requirements_report/ │ ├── reqstream/ -│ └── user_guide/ +│ ├── user_guide/ +│ └── verification/ ├── src/ │ └── DemaConsulting.BuildMark/ └── test/ @@ -45,16 +53,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 +78,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 +100,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 +108,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 +117,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 5fe996e4..472df2a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -319,14 +319,14 @@ Releases follow this process: 2. Update version references as needed 3. Create a pull request with the release changes 4. Merge the pull request after review -5. Create a GitHub release with the new version tag — CI/CD will automatically build and publish the NuGet package +5. Create a GitHub release with the new version tag - CI/CD will automatically build and publish the NuGet package ## Getting Help If you need help or have questions: -- **GitHub Issues**: For bug reports and feature requests — [open an issue][issues] -- **GitHub Discussions**: For general questions and discussions — [start a discussion][discussions] +- **GitHub Issues**: For bug reports and feature requests - [open an issue][issues] +- **GitHub Discussions**: For general questions and discussions - [start a discussion][discussions] - **Pull Request Comments**: For questions about specific code changes - **Security Vulnerabilities**: Please review the [Security Policy][security] before reporting diff --git a/README.md b/README.md index 6f137f04..7d74d489 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,15 @@ For command-line options, see the [CLI Reference](https://github.com/demaconsult ## Features -- 📄 **Git Integration** — Analyze repository history, tags, and branches -- 📝 **Markdown Reports** — Generate structured build notes -- 🐛 **Issue Tracking** — Pull changes and bugs from GitHub and Azure DevOps -- 🔀 **Configurable Routing** — Route items to sections by label or type -- 🎯 **Customizable Output** — Control report depth, sections, and content -- 🚀 **CI/CD Ready** — Integrate with GitHub Actions and Azure Pipelines -- 🌐 **Multi-Platform** — Windows, Linux, and macOS on .NET 8, 9, and 10 -- ✅ **Self-Validation** — Built-in qualification tests -- 📊 **Dependency Updates** — Track changes from Dependabot and Renovate +- 📄 **Git Integration** - Analyze repository history, tags, and branches +- 📝 **Markdown Reports** - Generate structured build notes +- 🐛 **Issue Tracking** - Pull changes and bugs from GitHub and Azure DevOps +- 🔀 **Configurable Routing** - Route items to sections by label or type +- 🎯 **Customizable Output** - Control report depth, sections, and content +- 🚀 **CI/CD Ready** - Integrate with GitHub Actions and Azure Pipelines +- 🌐 **Multi-Platform** - Windows, Linux, and macOS on .NET 8, 9, and 10 +- ✅ **Self-Validation** - Built-in qualification tests +- 📊 **Dependency Updates** - Track changes from Dependabot and Renovate ## Installation @@ -228,12 +228,13 @@ For details, see the [Item Controls Guide](https://github.com/demaconsulting/Bui The generated markdown report includes: -1. **Build Report** — title heading -2. **Version Information** — current version, baseline version, and commit hashes -3. **Routed Sections** — items distributed by routing rules (e.g., Changes, Bugs Fixed, +1. **Build Report** - title heading +2. **Version Information** - current version, baseline version, and commit hashes +3. **Routed Sections** - items distributed by routing rules (e.g., Changes, Bugs Fixed, Dependency Updates), or legacy Changes/Bugs Fixed sections when no rules are configured -4. **Known Issues** — open bugs (when `--include-known-issues` is specified) -5. **Full Changelog** — link to the platform compare view between versions (when available) +4. **Known Issues** - open bugs, plus any bugs (open or closed) whose `affected-versions` + field includes the current build version (when `--include-known-issues` is specified) +5. **Full Changelog** - link to the platform compare view between versions (when available) Sections with no items are omitted. When routing rules are active, the section order and titles are determined by the configuration. diff --git a/docs/build_notes/definition.yaml b/docs/build_notes/definition.yaml index ff158c9f..708f7fb2 100644 --- a/docs/build_notes/definition.yaml +++ b/docs/build_notes/definition.yaml @@ -6,8 +6,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 diff --git a/docs/code_quality/definition.yaml b/docs/code_quality/definition.yaml index 3566771a..78f82ee1 100644 --- a/docs/code_quality/definition.yaml +++ b/docs/code_quality/definition.yaml @@ -6,8 +6,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 diff --git a/docs/code_review_plan/definition.yaml b/docs/code_review_plan/definition.yaml index 3a24f0b9..56989bfa 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 6498e6ce..b238d43a 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/code_review_report/introduction.md b/docs/code_review_report/introduction.md index 4a0cd8e1..6d428331 100644 --- a/docs/code_review_report/introduction.md +++ b/docs/code_review_report/introduction.md @@ -4,7 +4,7 @@ This document contains the review report for the BuildMark project. ## Purpose -This review report provides evidence that each review-set is current — the review +This review report provides evidence that each review-set is current - the review evidence matches the current file fingerprints. It confirms that all formal reviews conducted for BuildMark remain valid for the current state of the reviewed files. diff --git a/docs/design/build-mark/build-mark.md b/docs/design/build-mark/build-mark.md index 9f29e206..1b0e72bf 100644 --- a/docs/design/build-mark/build-mark.md +++ b/docs/design/build-mark/build-mark.md @@ -4,7 +4,7 @@ BuildMark is a .NET command-line tool that generates markdown build notes from Git repository metadata hosted on GitHub or Azure DevOps. It queries the -appropriate platform API — GitHub's GraphQL API or the Azure DevOps REST API — to +appropriate platform API - GitHub's GraphQL API or the Azure DevOps REST API — to retrieve commits, issues or work items, pull requests, and version tags, then formats the results as a structured markdown report suitable for embedding in release documentation. @@ -13,28 +13,28 @@ release documentation. BuildMark is composed of seven subsystems and a top-level entry point: -- `Program` (Unit) — entry point; dispatches to handlers based on CLI flags -- `Cli` (Subsystem) — command-line argument parsing and output channel control -- `BuildNotes` (Subsystem) — output data model shared by all connectors and `Program` -- `Configuration` (Subsystem) — parses the `.buildmark.yaml` configuration file -- `RepoConnectors` (Subsystem) — repository metadata retrieval, including item-controls +- `Program` (Unit) - entry point; dispatches to handlers based on CLI flags +- `Cli` (Subsystem) - command-line argument parsing and output channel control +- `BuildNotes` (Subsystem) - output data model shared by all connectors and `Program` +- `Configuration` (Subsystem) - parses the `.buildmark.yaml` configuration file +- `RepoConnectors` (Subsystem) - repository metadata retrieval, including item-controls parsing and concrete connectors -- `SelfTest` (Subsystem) — built-in self-validation test framework -- `Utilities` (Subsystem) — shared path combination and process execution helpers -- `Version` (Subsystem) — semantic version processing and comparison engine +- `SelfTest` (Subsystem) - built-in self-validation test framework +- `Utilities` (Subsystem) - shared path combination and process execution helpers +- `Version` (Subsystem) - semantic version processing and comparison engine ## External Interfaces -| Interface | Direction | Protocol / Format | -|----------------------|-----------|----------------------------------------------------------| -| Command line | Input | POSIX-style flags parsed by `Context` | -| `.buildmark.yaml` | Input | YAML file read from the repository root | -| GitHub GraphQL | Output | HTTPS POST to `https://api.github.com/graphql` | -| Azure DevOps REST | Output | HTTPS GET/POST to Azure DevOps `_apis` endpoints v6.0 | -| Markdown report | Output | File written to `--report` path, UTF-8 markdown | -| Log file | Output | Optional file written to `--log` path, plain text | -| Test results | Output | TRX or JUnit XML written to `--results` path | -| Exit code | Output | 0 = success, 1 = error | +| Interface | Direction | Protocol / Format | +|----------------------|---------------|----------------------------------------------------------| +| Command line | Input | POSIX-style flags parsed by `Context` | +| `.buildmark.yaml` | Input | YAML file read from the repository root | +| GitHub GraphQL | Bidirectional | HTTPS POST to `https://api.github.com/graphql` | +| Azure DevOps REST | Bidirectional | HTTPS GET/POST to Azure DevOps `_apis` endpoints v6.0 | +| Markdown report | Output | File written to `--report` path, UTF-8 markdown | +| Log file | Output | Optional file written to `--log` path, plain text | +| Test results | Output | TRX or JUnit XML written to `--results` path | +| Exit code | Output | 0 = success, 1 = error | ## Data Flow @@ -127,7 +127,7 @@ allowing operators to validate the configuration file without running a build. When a valid `BuildMarkConfig` is available, its properties are consumed as follows: -- `BuildMarkConfig.Connector` — optional `ConnectorConfig` carrying the connector +- `BuildMarkConfig.Connector` - optional `ConnectorConfig` carrying the connector `Type` (`"github"` or `"azure-devops"`), a `GitHub` property holding a `GitHubConnectorConfig` for GitHub-based operation, and an `AzureDevOps` property holding an `AzureDevOpsConnectorConfig` for Azure DevOps-based @@ -137,10 +137,10 @@ follows: supply `OrganizationUrl`, `Project`, and `Repository` overrides. The full `ConnectorConfig` is also passed to `RepoConnectorFactory` to select the appropriate connector implementation. -- `BuildMarkConfig.Sections` — ordered list of `SectionConfig` objects (each with +- `BuildMarkConfig.Sections` - ordered list of `SectionConfig` objects (each with an `Id` and `Title`) that define the report sections. Passed to the active connector for output structuring. -- `BuildMarkConfig.Rules` — list of `RuleConfig` objects that map item attributes +- `BuildMarkConfig.Rules` - list of `RuleConfig` objects that map item attributes (labels, work-item types) to report sections. Passed to the active connector for item routing. @@ -191,10 +191,10 @@ custom fields take precedence over buildmark blocks when both are present. The connector applies these overrides as follows: -- `visibility: internal` — the item is excluded from all report sections -- `visibility: public` — the item is included regardless of its label-derived type -- `type: bug` or `type: feature` — overrides the label-derived type classification -- `affected-versions` — stored on the `ItemInfo` record for downstream use +- `visibility: internal` - the item is excluded from all report sections +- `visibility: public` - the item is included regardless of its label-derived type +- `type: bug` or `type: feature` - overrides the label-derived type classification +- `affected-versions` - stored on the `ItemInfo` record for downstream use When no `buildmark` block is present, the existing label-based rules apply unchanged. diff --git a/docs/design/build-mark/build-notes/build-information.md b/docs/design/build-mark/build-notes/build-information.md index 60eba981..ef71b0f9 100644 --- a/docs/design/build-mark/build-notes/build-information.md +++ b/docs/design/build-mark/build-notes/build-information.md @@ -18,15 +18,15 @@ public record BuildInformation( WebLink? CompleteChangelogLink); ``` -- `BaselineVersionTag` (`VersionCommitTag?`) — the previous version tag, which is the +- `BaselineVersionTag` (`VersionCommitTag?`) - the previous version tag, which is the lower boundary of the reported range -- `CurrentVersionTag` (`VersionCommitTag`) — the version tag being reported -- `Changes` (`List`) — feature and other non-bug items in this build -- `Bugs` (`List`) — bug-fix items in this build -- `KnownIssues` (`List`) — open issues not yet fixed -- `CompleteChangelogLink` (`WebLink?`) — optional link to the full changelog on +- `CurrentVersionTag` (`VersionCommitTag`) - the version tag being reported +- `Changes` (`List`) - feature and other non-bug items in this build +- `Bugs` (`List`) - bug-fix items in this build +- `KnownIssues` (`List`) - open issues not yet fixed +- `CompleteChangelogLink` (`WebLink?`) - optional link to the full changelog on the host -- `RoutedSections` (`IReadOnlyList<(string SectionId, string SectionTitle, IReadOnlyList Items)>?`) — +- `RoutedSections` (`IReadOnlyList<(string SectionId, string SectionTitle, IReadOnlyList Items)>?`) - optional ordered list of custom report sections populated by `RepoConnectorBase.ApplyRules` when routing rules are configured; `null` when no rules are active @@ -46,15 +46,15 @@ empty, `ToMarkdown` falls back to the legacy sections. The rendered output contains the following sections: -1. **Version Information** — baseline and current version tags with commit hashes. -2. **Custom sections from `RoutedSections`** *(when rules are configured)* — one +1. **Version Information** - baseline and current version tags with commit hashes. +2. **Custom sections from `RoutedSections`** *(when rules are configured)* - one sub-heading per section with the section title and its items. **OR** the following legacy sections *(when no rules are configured)*: - - **Changes** — list of `ItemInfo` records from `Changes`. - - **Bugs Fixed** — list of `ItemInfo` records from `Bugs`. - - **Known Issues** *(optional)* — list of `ItemInfo` records from `KnownIssues`, + - **Changes** - list of `ItemInfo` records from `Changes`. + - **Bugs Fixed** - list of `ItemInfo` records from `Bugs`. + - **Known Issues** *(optional)* - list of `ItemInfo` records from `KnownIssues`, emitted only when `includeKnownIssues` is `true`. -3. **Full Changelog** *(optional)* — hyperlink from `CompleteChangelogLink`, emitted +3. **Full Changelog** *(optional)* - hyperlink from `CompleteChangelogLink`, emitted only when the link is non-null. ## Interactions diff --git a/docs/design/build-mark/build-notes/build-notes.md b/docs/design/build-mark/build-notes/build-notes.md index e2bcdd09..39177153 100644 --- a/docs/design/build-mark/build-notes/build-notes.md +++ b/docs/design/build-mark/build-notes/build-notes.md @@ -20,14 +20,9 @@ calls `BuildInformation.ToMarkdown` to write the final report file. ## Interactions -+---------------------+-------------------------------------------------------------------+ -| Unit / Subsystem | Role | -+=====================+===================================================================+ -| `Version` | Supplies the version types used by `VersionCommitTag` | -+---------------------+-------------------------------------------------------------------+ -| `RepoConnectors` | Connectors construct and populate `BuildInformation` records | -+---------------------+-------------------------------------------------------------------+ -| `Program` | Calls `BuildInformation.ToMarkdown` to produce the report file | -+---------------------+-------------------------------------------------------------------+ -| `SelfTest` | `Validation` creates `BuildInformation` records during self-tests| -+---------------------+-------------------------------------------------------------------+ +| Unit / Subsystem | Role | +|---------------------|--------------------------------------------------------------------| +| `Version` | Supplies the version types used by `VersionCommitTag` | +| `RepoConnectors` | Connectors construct and populate `BuildInformation` records | +| `Program` | Calls `BuildInformation.ToMarkdown` to produce the report file | +| `SelfTest` | `Validation` creates `BuildInformation` records during self-tests | diff --git a/docs/design/build-mark/configuration/build-mark-config-reader.md b/docs/design/build-mark/configuration/build-mark-config-reader.md new file mode 100644 index 00000000..718d56e3 --- /dev/null +++ b/docs/design/build-mark/configuration/build-mark-config-reader.md @@ -0,0 +1,36 @@ +# BuildMarkConfigReader + +## Overview + +`BuildMarkConfigReader` is a static utility class responsible for reading and deserializing +the optional `.buildmark.yaml` file from the repository root. It uses the YamlDotNet library's +representation model (`YamlStream`) to parse YAML content, then walks the resulting node tree +to produce a strongly-typed `BuildMarkConfig` object. + +The method always returns a `ConfigurationLoadResult` and never throws. Parse errors and +validation warnings are captured as `ConfigurationIssue` records within the result. + +## Interface + +| Member | Kind | Description | +|-------------------|---------------|---------------------------------------------------------------------------| +| `ReadAsync(path)` | Static method | Reads and deserializes `.buildmark.yaml`; always returns a load result | + +### `ReadAsync(string path) → Task` + +Looks for a `.buildmark.yaml` file at the supplied path (normally the repository root): + +- If the file is absent, returns a result with `Config = null` and an empty issues list. +- If the file is present but contains YAML errors or invalid values, returns a result with + `Config = null` and one or more `ConfigurationIssue` records describing each problem. +- If the file is valid, returns a result with a fully populated `BuildMarkConfig` and an empty + issues list. + +## Interactions + +| Unit / Subsystem | Role | +|---------------------------|----------------------------------------------------------------------------| +| `BuildMarkConfig` | Produced by `ReadAsync` when parsing succeeds | +| `ConfigurationLoadResult` | Always returned by `ReadAsync`, carries config and any issues | +| `ConfigurationIssue` | Created for each parse error or validation warning encountered | +| `Program` | Calls `ReadAsync(Environment.CurrentDirectory)` via `LoadConfiguration()` | diff --git a/docs/design/build-mark/configuration/build-mark-config.md b/docs/design/build-mark/configuration/build-mark-config.md new file mode 100644 index 00000000..63c78454 --- /dev/null +++ b/docs/design/build-mark/configuration/build-mark-config.md @@ -0,0 +1,28 @@ +# BuildMarkConfig + +## Overview + +`BuildMarkConfig` is the top-level configuration data model for BuildMark. It holds all +settings read from the `.buildmark.yaml` file, including connector configuration, report +settings, section definitions, and item routing rules. + +When no `.buildmark.yaml` file is present, `Program` calls `BuildMarkConfig.CreateDefault()` +to obtain a default configuration with built-in section and rule definitions. + +## Data Model + +| Property | Type | Description | +|-------------|--------------------------|-------------------------------------------------------| +| `Connector` | `ConnectorConfig?` | Optional connector configuration; `null` when absent | +| `Report` | `ReportConfig?` | Optional report settings; `null` when absent | +| `Sections` | `IList` | Ordered list of report section definitions | +| `Rules` | `IList` | List of item routing rules | + +## Interactions + +| Unit / Subsystem | Role | +|--------------------------|----------------------------------------------------------------------------------| +| `BuildMarkConfigReader` | Produces `BuildMarkConfig` instances by parsing `.buildmark.yaml` | +| `Program` | Reads `Connector`, `Report`, `Sections`, and `Rules` to drive build notes output | +| `RepoConnectorBase` | Receives `Rules` and `Sections` via `Configure(rules, sections)` | +| `RepoConnectorFactory` | Receives `Connector` to select the appropriate connector implementation | diff --git a/docs/design/build-mark/configuration/configuration-issue.md b/docs/design/build-mark/configuration/configuration-issue.md new file mode 100644 index 00000000..7b98c9fb --- /dev/null +++ b/docs/design/build-mark/configuration/configuration-issue.md @@ -0,0 +1,31 @@ +# ConfigurationIssue + +## Overview + +`ConfigurationIssue` is an immutable record representing a single problem found while reading +or validating the `.buildmark.yaml` file. Each issue carries a file path, line number, +severity, and human-readable description. + +## Data Model + +| Property | Type | Description | +|---------------|-----------------------------|----------------------------------------------------| +| `FilePath` | `string` | Path to the file containing the issue | +| `Line` | `int` | Line number (1-based) of the issue | +| `Severity` | `ConfigurationIssueSeverity`| `Warning` or `Error` | +| `Description` | `string` | Human-readable description of the issue | + +`ConfigurationIssueSeverity` is a public enum: + +| Value | Description | +|-----------|----------------------------------------------------------| +| `Warning` | Non-fatal issue; tool continues and exit code is 0 | +| `Error` | Fatal issue; tool reports all errors, exits with code 1 | + +## Interactions + +| Unit / Subsystem | Role | +|---------------------------|----------------------------------------------------------------| +| `BuildMarkConfigReader` | Creates `ConfigurationIssue` records for each problem found | +| `ConfigurationLoadResult` | Holds the ordered list of `ConfigurationIssue` records | +| `Program` | Reads issues via `ConfigurationLoadResult.ReportTo(context)` | diff --git a/docs/design/build-mark/configuration/configuration-load-result.md b/docs/design/build-mark/configuration/configuration-load-result.md new file mode 100644 index 00000000..1501edfc --- /dev/null +++ b/docs/design/build-mark/configuration/configuration-load-result.md @@ -0,0 +1,33 @@ +# ConfigurationLoadResult + +## Overview + +`ConfigurationLoadResult` is an immutable record that carries the output of +`BuildMarkConfigReader.ReadAsync`. It holds the parsed configuration (or `null` if parsing +failed) together with an ordered list of issues found during parsing. + +`Program` calls `result.ReportTo(context)` immediately after reading the configuration to +surface any issues to the user and set the exit code when errors are present. + +## Data Model + +| Member | Kind | Description | +|---------------------|----------|----------------------------------------------------------| +| `Config` | Property | Parsed `BuildMarkConfig`; `null` if parsing failed | +| `Issues` | Property | Ordered list of `ConfigurationIssue` objects | +| `HasErrors` | Property | `true` when any issue has `Severity` of `Error` | +| `ReportTo(context)` | Method | Writes all issues to `Context`; sets exit code on errors | + +### `ReportTo(Context context)` + +Iterates `Issues` and writes each one to the context output. If any issue has severity +`Error`, sets `context.ExitCode` to 1. + +## Interactions + +| Unit / Subsystem | Role | +|-------------------------|-------------------------------------------------------------------------| +| `BuildMarkConfigReader` | Produces `ConfigurationLoadResult` from `ReadAsync` | +| `BuildMarkConfig` | Held by the `Config` property when parsing succeeds | +| `ConfigurationIssue` | Each issue in the `Issues` list | +| `Program` | Calls `ReportTo(context)` and checks `HasErrors` before proceeding | diff --git a/docs/design/build-mark/configuration/connector-config.md b/docs/design/build-mark/configuration/connector-config.md new file mode 100644 index 00000000..287e9fb9 --- /dev/null +++ b/docs/design/build-mark/configuration/connector-config.md @@ -0,0 +1,47 @@ +# ConnectorConfig, GitHubConnectorConfig, AzureDevOpsConnectorConfig + +## Overview + +These three configuration data models define the connector selection and per-connector +settings read from the `connector:` section of `.buildmark.yaml`. + +`ConnectorConfig` is the envelope that carries the connector type and the platform-specific +settings. `GitHubConnectorConfig` holds GitHub-specific overrides. `AzureDevOpsConnectorConfig` +holds Azure DevOps-specific connection details. + +## Data Models + +### ConnectorConfig + +| Property | Type | Description | +|---------------|-------------------------------|-----------------------------------------------| +| `Type` | `string?` | Connector type: `"github"` or `"azure-devops"`| +| `GitHub` | `GitHubConnectorConfig?` | Optional GitHub connector settings | +| `AzureDevOps` | `AzureDevOpsConnectorConfig?` | Optional Azure DevOps connector settings | + +### GitHubConnectorConfig + +| Property | Type | Description | +|-----------|-----------|---------------------------------------------------------------------| +| `Owner` | `string?` | Repository owner override | +| `Repo` | `string?` | Repository name override | +| `BaseUrl` | `string?` | Optional GitHub Enterprise API base URL; `null` uses the public API | + +### AzureDevOpsConnectorConfig + +| Property | Type | Description | +|-------------------|-----------|---------------------------------------------------------------------------| +| `OrganizationUrl` | `string?` | Azure DevOps organization URL (e.g. `https://dev.azure.com/myorg`) | +| `Organization` | `string?` | Optional organization name override; `null` when not specified | +| `Project` | `string?` | Azure DevOps project name | +| `Repository` | `string?` | Repository name within the project | + +## Interactions + +| Unit / Subsystem | Role | +|-----------------------------|---------------------------------------------------------------------------------| +| `BuildMarkConfig` | Holds `ConnectorConfig` in its `Connector` property | +| `BuildMarkConfigReader` | Parses the `connector:` YAML node and populates these records | +| `RepoConnectorFactory` | Receives `ConnectorConfig` to select the appropriate connector implementation | +| `GitHubRepoConnector` | Reads `GitHubConnectorConfig` for owner, repo, and base URL overrides | +| `AzureDevOpsRepoConnector` | Reads `AzureDevOpsConnectorConfig` for organization URL, project, and repo | diff --git a/docs/design/build-mark/configuration/report-config.md b/docs/design/build-mark/configuration/report-config.md new file mode 100644 index 00000000..12bb4668 --- /dev/null +++ b/docs/design/build-mark/configuration/report-config.md @@ -0,0 +1,23 @@ +# ReportConfig + +## Overview + +`ReportConfig` is a configuration data model holding the optional report output settings +read from the `report:` section of `.buildmark.yaml`. All properties are nullable; when +absent, `Program` uses CLI argument values or built-in defaults. + +## Data Model + +| Property | Type | Description | +|----------------------|-----------|--------------------------------------------------------------------------------| +| `File` | `string?` | Optional output file path override; `null` uses the `--report` CLI argument | +| `Depth` | `int?` | Optional heading depth for report sections; `null` defaults to 1 | +| `IncludeKnownIssues` | `bool?` | Optional flag to include known issues; `null` defaults to `false` | + +## Interactions + +| Unit / Subsystem | Role | +|--------------------------|-----------------------------------------------------------------------------------| +| `BuildMarkConfig` | Holds `ReportConfig` in its `Report` property | +| `BuildMarkConfigReader` | Parses the `report:` YAML node and populates this record | +| `Program` | Reads `File`, `Depth`, and `IncludeKnownIssues` as fallbacks to CLI arguments | diff --git a/docs/design/build-mark/configuration/rule-config.md b/docs/design/build-mark/configuration/rule-config.md new file mode 100644 index 00000000..aba0cec2 --- /dev/null +++ b/docs/design/build-mark/configuration/rule-config.md @@ -0,0 +1,34 @@ +# RuleConfig and RuleMatchConfig + +## Overview + +`RuleConfig` is a configuration data model representing a single item routing rule read from +the `rules:` list in `.buildmark.yaml`. Each rule carries match conditions and a destination +section ID. `RuleMatchConfig` holds the conditions that must be satisfied for a rule to fire. + +Both types are defined in `Configuration/RuleConfig.cs`. + +## Data Models + +### RuleConfig + +| Property | Type | Description | +|----------|-------------------|---------------------------------------------------------| +| `Match` | `RuleMatchConfig` | Match conditions (labels, work-item types) for the rule | +| `Route` | `string` | Destination section `Id` for matched items | + +### RuleMatchConfig + +| Property | Type | Description | +|----------------|-----------------|-------------------------------------------------------------------| +| `Label` | `IList` | List of label values; rule matches when any label is present | +| `WorkItemType` | `IList` | List of work-item type values; rule matches when any type matches | + +## Interactions + +| Unit / Subsystem | Role | +|-------------------------|-------------------------------------------------------------------------| +| `BuildMarkConfig` | Holds the list of `RuleConfig` objects in `Rules` | +| `BuildMarkConfigReader` | Parses the `rules:` YAML list and creates `RuleConfig` records | +| `RepoConnectorBase` | Receives the list via `Configure(rules, sections)` | +| `ItemRouter` | Uses `RuleConfig` and `RuleMatchConfig` to route items to sections | diff --git a/docs/design/build-mark/configuration/section-config.md b/docs/design/build-mark/configuration/section-config.md new file mode 100644 index 00000000..658cc64b --- /dev/null +++ b/docs/design/build-mark/configuration/section-config.md @@ -0,0 +1,23 @@ +# SectionConfig + +## Overview + +`SectionConfig` is a configuration data model representing a single report section definition +read from the `sections:` list in `.buildmark.yaml`. Each section has a unique identifier +used by routing rules and a display title used as the markdown heading. + +## Data Model + +| Property | Type | Description | +|----------|----------|--------------------------------------| +| `Id` | `string` | Unique identifier for the section | +| `Title` | `string` | Display title for the report section | + +## Interactions + +| Unit / Subsystem | Role | +|-------------------------|-------------------------------------------------------------------------| +| `BuildMarkConfig` | Holds the ordered list of `SectionConfig` objects in `Sections` | +| `BuildMarkConfigReader` | Parses the `sections:` YAML list and creates `SectionConfig` records | +| `RepoConnectorBase` | Receives the list via `Configure(rules, sections)` for output ordering | +| `ItemRouter` | Uses section IDs to map routed items to display sections | diff --git a/docs/design/build-mark/program.md b/docs/design/build-mark/program.md index d55f1128..5451cc2e 100644 --- a/docs/design/build-mark/program.md +++ b/docs/design/build-mark/program.md @@ -53,10 +53,33 @@ The exit code is managed through `context.ExitCode` rather than as a return valu Calls `BuildMarkConfigReader.ReadAsync` to load the optional `.buildmark.yaml` file, then calls `result.ReportTo(context)` to surface any configuration issues. -If no errors occurred, resolves the build version, creates a repository connector -via `RepoConnectorFactory.Create(result.Config?.Connector)`, fetches -`BuildInformation`, writes a summary to the console, and optionally writes the -markdown report to `context.ReportFile`. +If errors occurred, the method returns early. Otherwise: + +1. **Effective configuration**: derives `effectiveConfig` as `loadResult.Config ?? + BuildMarkConfig.CreateDefault()`. When no `.buildmark.yaml` file is present (i.e., + `loadResult.Config` is `null`), `BuildMarkConfig.CreateDefault()` supplies built-in + section and rule definitions so the tool functions without any configuration file. +2. **Effective option resolution**: derives `effectiveReportFile` from `context.ReportFile` if set, + or from `effectiveConfig.Report?.File` as fallback; derives `effectiveReportDepth` from + `context.Depth` if set, or `effectiveConfig.Report?.Depth`, defaulting to 1; derives + `effectiveIncludeKnownIssues` from `context.IncludeKnownIssues` OR + `effectiveConfig.Report?.IncludeKnownIssues`. +3. **ConnectorFactory injection**: if `context.ConnectorFactory` is non-null it is invoked directly + (test injection path); otherwise `RepoConnectorFactory.Create(effectiveConfig.Connector)` is used. +4. **Configuration step**: when the production factory path is used and the connector implements + `RepoConnectorBase`, calls `configurableConnector.Configure(effectiveConfig.Rules, + effectiveConfig.Sections)`. +5. Parses `context.BuildVersion` using `VersionTag.Create`; on `ArgumentException`, + writes an error and returns early. +6. Calls `connector.GetBuildInformationAsync(buildVersion)` synchronously; on + `InvalidOperationException`, writes an error and returns early. +7. Writes a build summary to the context output. +8. If `effectiveReportFile` is non-null, renders the markdown and writes it to that path. + Any file-system exception during write is caught, reported via `context.WriteError`, and + execution continues - the method does not propagate the exception. This graceful-degradation + choice ensures that a report-write failure does not obscure the build summary already written + to the console and allows the exit code to reflect only semantic errors rather than I/O + failures outside the tool's control. ### `PrintBanner(Context context)` diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md index dbeabc84..3e1ec1d3 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-api-types.md @@ -91,7 +91,7 @@ Git reference (tag or branch) returned by the Azure DevOps refs endpoint. Fields: `name`, `objectId`, `peeledObjectId` The `objectId` field contains the SHA of the object this reference points to -directly — for lightweight tags this is the commit SHA, and for annotated tags +directly - for lightweight tags this is the commit SHA, and for annotated tags this is the tag object SHA. The `peeledObjectId` field contains the commit SHA for annotated tags (resolved through the tag object), or `null` for lightweight tags. The type exposes a computed `CommitId` property that returns diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md index 7fd559ca..db53f9a9 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.md @@ -16,13 +16,13 @@ construct a `BuildInformation` record. The connector resolves the Azure DevOps token using the following priority order: -1. `AZURE_DEVOPS_PAT` environment variable — authenticated as Basic (PAT) -2. `AZURE_DEVOPS_TOKEN` environment variable — authenticated as Basic (PAT) -3. `AZURE_DEVOPS_EXT_PAT` environment variable — authenticated as Basic (PAT) +1. `AZURE_DEVOPS_PAT` environment variable - authenticated as Basic (PAT) +2. `AZURE_DEVOPS_TOKEN` environment variable - authenticated as Basic (PAT) +3. `AZURE_DEVOPS_EXT_PAT` environment variable - authenticated as Basic (PAT) 4. `SYSTEM_ACCESSTOKEN` environment variable (set automatically by Azure Pipelines) - — authenticated as Bearer + - authenticated as Bearer 5. Output of `az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv` - — authenticated as Bearer + - authenticated as Bearer If no token is found, the connector throws `InvalidOperationException`. @@ -49,22 +49,22 @@ After the work-item-type-derived categorization is determined, the connector cal and pull request. If the parser returns a non-null `ItemControlsInfo`, the following overrides are applied: -1. **`visibility: internal`** — The item is excluded from all report sections, +1. **`visibility: internal`** - The item is excluded from all report sections, regardless of its type. -2. **`visibility: public`** — The item is included in the report even if its +2. **`visibility: public`** - The item is included in the report even if its type-derived category would otherwise suppress it. -3. **`type: bug`** — The item is placed in the `Bugs` list regardless of work item +3. **`type: bug`** - The item is placed in the `Bugs` list regardless of work item type. -4. **`type: feature`** — The item is placed in the `Changes` list regardless of +4. **`type: feature`** - The item is placed in the `Changes` list regardless of work item type. -5. **`affected-versions`** — The parsed `VersionIntervalSet` is stored on the +5. **`affected-versions`** - The parsed `VersionIntervalSet` is stored on the `ItemInfo.AffectedVersions` property. In addition, the connector reads the following Azure DevOps custom fields from each work item: -- `Custom.Visibility` — overrides the `visibility` control when present. -- `Custom.AffectedVersions` — overrides the `affected-versions` control when present. +- `Custom.Visibility` - overrides the `visibility` control when present. +- `Custom.AffectedVersions` - overrides the `affected-versions` control when present. Custom fields take precedence over buildmark blocks when both are present. @@ -75,22 +75,22 @@ work-item-type-based rules apply unchanged. The `AzureDevOpsRestClient` returns the following record types: -- **`AzureDevOpsRepository`** — repository metadata including id, name, and remoteUrl. -- **`AzureDevOpsCommit`** — commit data including commitId and comment. -- **`AzureDevOpsGitCommitRef`** — minimal commit reference containing only commitId. -- **`AzureDevOpsPullRequest`** — pull request data including pullRequestId, title, +- **`AzureDevOpsRepository`** - repository metadata including id, name, and remoteUrl. +- **`AzureDevOpsCommit`** - commit data including commitId and comment. +- **`AzureDevOpsGitCommitRef`** - minimal commit reference containing only commitId. +- **`AzureDevOpsPullRequest`** - pull request data including pullRequestId, title, url, status, lastMergeCommit (an `AzureDevOpsGitCommitRef` object representing the most recent merge commit), sourceRefName, and description. Exposes a computed `MergeCommitId` property that returns `LastMergeCommit?.CommitId`. -- **`AzureDevOpsWorkItem`** — work item data including id and a fields dictionary +- **`AzureDevOpsWorkItem`** - work item data including id and a fields dictionary containing System.Title, System.WorkItemType, System.State, System.Description, Custom.Visibility, and Custom.AffectedVersions. -- **`AzureDevOpsWorkItemQuery`** — result of a WIQL query, containing a list of +- **`AzureDevOpsWorkItemQuery`** - result of a WIQL query, containing a list of work item id references. -- **`AzureDevOpsRef`** — git reference including name, objectId, and +- **`AzureDevOpsRef`** - git reference including name, objectId, and peeledObjectId. Exposes a computed `CommitId` property that returns `PeeledObjectId ?? ObjectId`, resolving annotated tags to their commit SHA. -- **`AzureDevOpsCollectionResponse`** — wraps paginated responses with a count +- **`AzureDevOpsCollectionResponse`** - wraps paginated responses with a count and value list. ## Methods @@ -114,7 +114,7 @@ Throws `ArgumentException` when the URL does not match any supported format. Main entry point. Performs the following steps: 1. Get repository metadata (URL, branch, current commit hash) from Git. -2. Determine the organization URL, project, and repository name — from +2. Determine the organization URL, project, and repository name - from `AzureDevOpsConnectorConfig` if provided, otherwise parsed from the Git remote URL (supports `dev.azure.com`, `visualstudio.com`, and on-premises Azure DevOps Server URL formats by locating the `_git` path segment). diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md index b7b16c9d..89e5aa66 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.md @@ -11,10 +11,10 @@ all Azure DevOps API communication to this client. The client authenticates using either: -- **Basic authentication** — the PAT is supplied as the password field of a `Basic` +- **Basic authentication** - the PAT is supplied as the password field of a `Basic` authorization header (with an empty username), which is the standard Azure DevOps PAT authentication scheme. -- **Bearer authentication** — an Entra ID (Azure AD) access token is supplied as a +- **Bearer authentication** - an Entra ID (Azure AD) access token is supplied as a `Bearer` authorization header, used when authenticating via `az account get-access-token` or the `SYSTEM_ACCESSTOKEN` Azure Pipelines variable with OAuth scope. @@ -34,7 +34,7 @@ per-property `[JsonPropertyName]` attributes on the response records. The `AllowReadingFromString` setting handles numeric fields (such as work item IDs) that the API may return as JSON string values rather than JSON numbers. -The sole exception is the `AzureDevOpsWorkItem.Fields` dictionary — its keys are +The sole exception is the `AzureDevOpsWorkItem.Fields` dictionary - its keys are Azure DevOps field reference names (e.g. `System.WorkItemType`, `Custom.Visibility`) and are preserved as-is without any naming transformation. diff --git a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops.md b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops.md index b259a18a..689f0c88 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/azure-devops.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/azure-devops.md @@ -8,13 +8,13 @@ connector used when the repository host is Azure DevOps. ## Units -- `AzureDevOpsRepoConnector` — `RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs` — +- `AzureDevOpsRepoConnector` - `RepoConnectors/AzureDevOps/AzureDevOpsRepoConnector.cs` - implements `IRepoConnector` for Azure DevOps. -- `AzureDevOpsRestClient` — `RepoConnectors/AzureDevOps/AzureDevOpsRestClient.cs` — +- `AzureDevOpsRestClient` - `RepoConnectors/AzureDevOps/AzureDevOpsRestClient.cs` - issues paginated REST API requests. -- `AzureDevOpsApiTypes` — `RepoConnectors/AzureDevOps/AzureDevOpsApiTypes.cs` — +- `AzureDevOpsApiTypes` - `RepoConnectors/AzureDevOps/AzureDevOpsApiTypes.cs` - provides record types for REST API request and response data. -- `WorkItemMapper` — `RepoConnectors/AzureDevOps/WorkItemMapper.cs` — +- `WorkItemMapper` - `RepoConnectors/AzureDevOps/WorkItemMapper.cs` - maps Azure DevOps work items to `ItemInfo` objects. ### `AzureDevOpsRepoConnector` diff --git a/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md b/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md index db746cf8..04110c49 100644 --- a/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md +++ b/docs/design/build-mark/repo-connectors/azure-devops/work-item-mapper.md @@ -36,11 +36,11 @@ unresolved and included in known-issues reporting. Item controls are extracted from two sources and merged: -1. **Buildmark blocks** — `ItemControlsParser.Parse(description)` is called on the +1. **Buildmark blocks** - `ItemControlsParser.Parse(description)` is called on the `System.Description` field of the work item. The resulting `ItemControlsInfo` provides `Visibility`, `Type`, and `AffectedVersions` overrides from embedded YAML blocks in the description body. -2. **Custom fields** — The `Custom.Visibility` and `Custom.AffectedVersions` fields +2. **Custom fields** - The `Custom.Visibility` and `Custom.AffectedVersions` fields in the work item's fields dictionary are read directly. **Precedence**: custom fields take priority over buildmark blocks when both are diff --git a/docs/design/build-mark/repo-connectors/github/github-graphql-client.md b/docs/design/build-mark/repo-connectors/github/github-graphql-client.md index a0ee9007..2768fe19 100644 --- a/docs/design/build-mark/repo-connectors/github/github-graphql-client.md +++ b/docs/design/build-mark/repo-connectors/github/github-graphql-client.md @@ -11,10 +11,10 @@ GitHub API communication to this client. The class provides two constructors: -- **Public constructor** — accepts a GitHub authentication token and creates an +- **Public constructor** - accepts a GitHub authentication token and creates an owned `HttpClient` configured with the token. Used by `GitHubRepoConnector` in production. -- **Internal constructor** — accepts an existing `HttpClient` directly. Used by +- **Internal constructor** - accepts an existing `HttpClient` directly. Used by tests to inject a mock `HttpClient` without network access. ## Lifecycle diff --git a/docs/design/build-mark/repo-connectors/github/github-repo-connector.md b/docs/design/build-mark/repo-connectors/github/github-repo-connector.md index 8a4cf10b..056fc181 100644 --- a/docs/design/build-mark/repo-connectors/github/github-repo-connector.md +++ b/docs/design/build-mark/repo-connectors/github/github-repo-connector.md @@ -82,14 +82,14 @@ After the label-derived type is determined, the connector calls request. If the parser returns a non-null `ItemControlsInfo`, the following overrides are applied: -1. **`visibility: internal`** — The item is excluded from all report sections, +1. **`visibility: internal`** - The item is excluded from all report sections, regardless of its labels or type. -2. **`visibility: public`** — The item is included in the report even if its +2. **`visibility: public`** - The item is included in the report even if its label-derived type is `"other"`. -3. **`type: bug`** — The item is placed in the `Bugs` list regardless of labels. -4. **`type: feature`** — The item is placed in the `Changes` list regardless of +3. **`type: bug`** - The item is placed in the `Bugs` list regardless of labels. +4. **`type: feature`** - The item is placed in the `Changes` list regardless of labels. -5. **`affected-versions`** — The parsed `VersionIntervalSet` is stored on the +5. **`affected-versions`** - The parsed `VersionIntervalSet` is stored on the `ItemInfo.AffectedVersions` property. When no `buildmark` block is present, the existing label-based rules apply @@ -102,7 +102,7 @@ unchanged. Main entry point. Performs the following steps: 1. Get repository metadata (URL, branch, current commit hash) from Git. -2. Determine the owner and repository name — from `GitHubConnectorConfig.Owner` +2. Determine the owner and repository name - from `GitHubConnectorConfig.Owner` and `GitHubConnectorConfig.Repo` if provided, otherwise parsed from the Git remote URL. 3. Resolve the GitHub authentication token (see Authentication above). diff --git a/docs/design/build-mark/repo-connectors/github/github.md b/docs/design/build-mark/repo-connectors/github/github.md index 3ac6a731..de45e608 100644 --- a/docs/design/build-mark/repo-connectors/github/github.md +++ b/docs/design/build-mark/repo-connectors/github/github.md @@ -8,11 +8,11 @@ production connector used when the repository host is GitHub or GitHub Enterpris ## Units -- `GitHubRepoConnector` — `RepoConnectors/GitHub/GitHubRepoConnector.cs` — +- `GitHubRepoConnector` - `RepoConnectors/GitHub/GitHubRepoConnector.cs` - implements `IRepoConnector` for GitHub. -- `GitHubGraphQLClient` — `RepoConnectors/GitHub/GitHubGraphQLClient.cs` — +- `GitHubGraphQLClient` - `RepoConnectors/GitHub/GitHubGraphQLClient.cs` - issues paginated GraphQL queries. -- `GitHubGraphQLTypes` — `RepoConnectors/GitHub/GitHubGraphQLTypes.cs` — +- `GitHubGraphQLTypes` - `RepoConnectors/GitHub/GitHubGraphQLTypes.cs` - provides record types for GraphQL request and response data. ### `GitHubRepoConnector` diff --git a/docs/design/build-mark/repo-connectors/item-controls-parser.md b/docs/design/build-mark/repo-connectors/item-controls-parser.md index a6fc5a17..8a6e6a64 100644 --- a/docs/design/build-mark/repo-connectors/item-controls-parser.md +++ b/docs/design/build-mark/repo-connectors/item-controls-parser.md @@ -32,9 +32,9 @@ Each non-empty line inside the block is treated as a `key: value` pair: - The value is the text after the first `:`, trimmed of whitespace. - Lines that do not contain `:` are ignored. - Unknown keys are silently ignored. -- Key matching is **case-insensitive** — keys are normalized to lowercase before +- Key matching is **case-insensitive** - keys are normalized to lowercase before comparison. -- Value matching is **case-sensitive** — only the exact values listed below are +- Value matching is **case-sensitive** - only the exact values listed below are recognized; other values are silently ignored. Recognized keys: @@ -48,20 +48,9 @@ Recognized keys: Unrecognized values for a known key are silently ignored (the field remains `null`). -## Data Model — ItemControlsInfo +## Data Model - ItemControlsInfo -```csharp -public record ItemControlsInfo( - string? Visibility, - string? Type, - VersionIntervalSet? AffectedVersions); -``` - -| Property | Type | Description | -|--------------------|-----------------------|-----------------------------------------------| -| `Visibility` | `string?` | `"public"`, `"internal"`, or `null` | -| `Type` | `string?` | `"bug"`, `"feature"`, or `null` | -| `AffectedVersions` | `VersionIntervalSet?` | Parsed interval set, or `null` if not present | +See `item-controls-info.md` for the `ItemControlsInfo` data model definition. ## Methods diff --git a/docs/design/build-mark/repo-connectors/item-router.md b/docs/design/build-mark/repo-connectors/item-router.md index 167c9d31..55738127 100644 --- a/docs/design/build-mark/repo-connectors/item-router.md +++ b/docs/design/build-mark/repo-connectors/item-router.md @@ -18,10 +18,10 @@ Takes a list of `ItemInfo` objects, a list of `RuleConfig` entries, and a list o `SectionConfig` entries, and returns a dictionary mapping each section ID to the items assigned to that section. -- `items` (`IReadOnlyList`) — items to be distributed into sections -- `rules` (`IReadOnlyList`) — routing rules that map item attributes to +- `items` (`IReadOnlyList`) - items to be distributed into sections +- `rules` (`IReadOnlyList`) - routing rules that map item attributes to sections -- `sections` (`IReadOnlyList`) — ordered list of report sections +- `sections` (`IReadOnlyList`) - ordered list of report sections #### Algorithm @@ -38,7 +38,7 @@ ad-hoc sections without requiring them to be pre-declared. #### Rule matching -- A `null` `Match` block is a **catch-all** — the rule matches every item. +- A `null` `Match` block is a **catch-all** - the rule matches every item. - A non-null `Match` block may specify `Label` and/or `WorkItemType` filter lists. Both lists are matched case-insensitively against the item's `Type` field. All non-empty filter lists must match for the rule to apply. diff --git a/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md b/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md index 6871e839..2ac61226 100644 --- a/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md +++ b/docs/design/build-mark/repo-connectors/mock/mock-repo-connector.md @@ -6,7 +6,7 @@ self-validation and unit testing. It returns a fixed, deterministic dataset without making any network or filesystem calls. -`MockRepoConnector` lives in production code — not in the test project — because +`MockRepoConnector` lives in production code - not in the test project - because the `--validate` flag must work in any deployment without requiring a separate test assembly or external tooling. @@ -48,9 +48,9 @@ categorization into `Changes` and `Bugs` is used. `GetBuildInformationAsync` throws `InvalidOperationException` in the following scenarios: -1. **No version tags exist in data and no version argument provided** — throws with message: +1. **No version tags exist in data and no version argument provided** - throws with message: `"No tags found in repository and no version specified. Please provide a version parameter."` -2. **Current commit does not match any tag and no version argument provided** — throws with message: +2. **Current commit does not match any tag and no version argument provided** - throws with message: `"Target version not specified and current commit does not match any tag. Please provide a version parameter."` These conditions mirror the equivalent error paths in the production `GitHubRepoConnector` diff --git a/docs/design/build-mark/repo-connectors/mock/mock.md b/docs/design/build-mark/repo-connectors/mock/mock.md index 87e04bea..32fdd7a3 100644 --- a/docs/design/build-mark/repo-connectors/mock/mock.md +++ b/docs/design/build-mark/repo-connectors/mock/mock.md @@ -5,7 +5,7 @@ The Mock subsystem groups the in-memory connector used by the built-in `--validate` self-test. It sits within the RepoConnectors subsystem. -`MockRepoConnector` lives in production code — not in the test project — because +`MockRepoConnector` lives in production code - not in the test project - because the `--validate` flag must work in any deployment without requiring a separate test assembly or external tooling. diff --git a/docs/design/build-mark/repo-connectors/repo-connector-base.md b/docs/design/build-mark/repo-connectors/repo-connector-base.md index a761e54e..2dfc0ba2 100644 --- a/docs/design/build-mark/repo-connectors/repo-connector-base.md +++ b/docs/design/build-mark/repo-connectors/repo-connector-base.md @@ -18,19 +18,13 @@ shared utilities used by concrete connectors. `RepoConnectorBase` provides: -+------------------------------------------+-------------------+-------------------------------------------------------+ | Member | Kind | Description | -+==========================================+===================+=======================================================+ -| `Configure(rules, sections)` | Public method | Stores routing rules and section definitions | -+------------------------------------------+-------------------+-------------------------------------------------------+ -| `HasRules` | Protected bool | True when at least one rule has been configured | -+------------------------------------------+-------------------+-------------------------------------------------------+ -| `ApplyRules(allItems)` | Protected method | Routes items into sections using configured rules | -+------------------------------------------+-------------------+-------------------------------------------------------+ -| `RunCommandAsync(command, args)` | Protected virtual | Delegates shell commands to ProcessRunner | -+------------------------------------------+-------------------+-------------------------------------------------------+ -| `FindVersionIndex(versions, target)` | Protected static | Locates version using semantic equality | -+------------------------------------------+-------------------+-------------------------------------------------------+ +|------------------------------------------|-------------------|-------------------------------------------------------| +| `Configure(rules, sections)` | Public method | Stores routing rules and section definitions | +| `HasRules` | Protected bool | True when at least one rule has been configured | +| `ApplyRules(allItems)` | Protected method | Routes items into sections using configured rules | +| `RunCommandAsync(command, args)` | Protected virtual | Delegates shell commands to ProcessRunner | +| `FindVersionIndex(versions, target)` | Protected static | Locates version using semantic equality | ### `Configure(rules, sections)` diff --git a/docs/design/build-mark/repo-connectors/repo-connector-factory.md b/docs/design/build-mark/repo-connectors/repo-connector-factory.md index 5be7dae6..19667503 100644 --- a/docs/design/build-mark/repo-connectors/repo-connector-factory.md +++ b/docs/design/build-mark/repo-connectors/repo-connector-factory.md @@ -20,14 +20,14 @@ When `config?.Type` is not `"azure-devops"` (including when `config` is `null` or `Type` is `null`), the method auto-detects the environment using the following signals, checked in order: -1. The `TF_BUILD` environment variable is non-empty — indicates Azure DevOps +1. The `TF_BUILD` environment variable is non-empty - indicates Azure DevOps Pipelines; creates an `AzureDevOpsRepoConnector`. -2. The `GITHUB_ACTIONS` or `GITHUB_WORKSPACE` environment variable is non-empty - — creates a `GitHubRepoConnector`. -3. The git remote URL contains `dev.azure.com` or `visualstudio.com` — creates +2. The `GITHUB_ACTIONS` or `GITHUB_WORKSPACE` environment variable is + non-empty - creates a `GitHubRepoConnector`. +3. The git remote URL contains `dev.azure.com` or `visualstudio.com` - creates an `AzureDevOpsRepoConnector`. -4. The git remote URL contains `github.com` — creates a `GitHubRepoConnector`. -5. None of the above matched — defaults to a `GitHubRepoConnector`. +4. The git remote URL contains `github.com` - creates a `GitHubRepoConnector`. +5. None of the above matched - defaults to a `GitHubRepoConnector`. The git remote URL is obtained **once** using the sync-over-async pattern via `ProcessRunner.TryRunAsync("git", "remote", "get-url", "origin").GetAwaiter().GetResult()`, diff --git a/docs/design/build-mark/repo-connectors/repo-connectors.md b/docs/design/build-mark/repo-connectors/repo-connectors.md index 3e5b18bc..cbc9d6dd 100644 --- a/docs/design/build-mark/repo-connectors/repo-connectors.md +++ b/docs/design/build-mark/repo-connectors/repo-connectors.md @@ -16,28 +16,26 @@ self-test. ## Units -- `IRepoConnector` — `RepoConnectors/IRepoConnector.cs` — interface for all +- `IRepoConnector` - `RepoConnectors/IRepoConnector.cs` - interface for all repository connectors -- `RepoConnectorBase` — `RepoConnectors/RepoConnectorBase.cs` — base class with +- `RepoConnectorBase` - `RepoConnectors/RepoConnectorBase.cs` - base class with common connector logic -- `RepoConnectorFactory` — `RepoConnectors/RepoConnectorFactory.cs` — creates +- `RepoConnectorFactory` - `RepoConnectors/RepoConnectorFactory.cs` - creates the appropriate connector -- `ItemRouter` — `RepoConnectors/ItemRouter.cs` — shared item-routing logic for +- `ItemRouter` - `RepoConnectors/ItemRouter.cs` - shared item-routing logic for all connectors -- `ItemControlsParser` — `RepoConnectors/ItemControlsParser.cs` — parses +- `ItemControlsParser` - `RepoConnectors/ItemControlsParser.cs` - parses buildmark blocks from item description bodies -- `ItemControlsInfo` — `RepoConnectors/ItemControlsInfo.cs` — data record holding +- `ItemControlsInfo` - `RepoConnectors/ItemControlsInfo.cs` - data record holding visibility, type, and version-set values -- `ProcessRunner` — `Utilities/ProcessRunner.cs` — executes external processes - and captures output; used by `RepoConnectorBase` for shell command delegation ## Subsystems -- **`GitHub`** — `RepoConnectors/GitHub/` — `GitHubRepoConnector`, `GitHubGraphQLClient`, +- **`GitHub`** - `RepoConnectors/GitHub/` - `GitHubRepoConnector`, `GitHubGraphQLClient`, `GitHubGraphQLTypes` -- **`AzureDevOps`** — `RepoConnectors/AzureDevOps/` — `AzureDevOpsRepoConnector`, +- **`AzureDevOps`** - `RepoConnectors/AzureDevOps/` - `AzureDevOpsRepoConnector`, `AzureDevOpsRestClient`, `AzureDevOpsApiTypes`, `WorkItemMapper` -- **`Mock`** — `RepoConnectors/Mock/` — `MockRepoConnector` (used by `--validate` self-test) +- **`Mock`** - `RepoConnectors/Mock/` - `MockRepoConnector` (used by `--validate` self-test) ## Interfaces diff --git a/docs/design/build-mark/utilities/path-helpers.md b/docs/design/build-mark/utilities/path-helpers.md index 72ed4ba1..5f74555b 100644 --- a/docs/design/build-mark/utilities/path-helpers.md +++ b/docs/design/build-mark/utilities/path-helpers.md @@ -38,7 +38,7 @@ the base directory. escaping segment only when it is the entire relative result or is followed by a directory separator, avoiding false positives for valid in-base names such as `..data`. - **Post-combine canonical-path check**: Resolving paths after combining handles all traversal - patterns — `../`, embedded `/../`, absolute-path overrides, and platform edge cases — + patterns - `../`, embedded `/../`, absolute-path overrides, and platform edge cases - without fragile pre-combine string inspection of `relativePath`. - **ArgumentException on invalid input**: Callers receive a specific `ArgumentException` identifying `relativePath` as the problematic parameter, making debugging straightforward. diff --git a/docs/design/build-mark/version/version-interval-set.md b/docs/design/build-mark/version/version-interval-set.md index 0d010c5e..2b5d2a68 100644 --- a/docs/design/build-mark/version/version-interval-set.md +++ b/docs/design/build-mark/version/version-interval-set.md @@ -1,61 +1,5 @@ # VersionIntervalSet -## Purpose - -`VersionIntervalSet` is an immutable record in the Version subsystem that represents an -ordered collection of `VersionInterval` instances. It models the value of the -`affected-versions` field, which may contain multiple comma-separated ranges. - -## Structure - -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Intervals` | `IReadOnlyList` | Ordered list of parsed version intervals | - -## Methods - -### `Contains(string version) → bool` - -Returns `true` when the semantic version text falls within any interval in the set. -Delegates to `VersionInterval.Contains(string)` for each interval and returns the -logical OR of all results. - -### `Contains(VersionComparable version) → bool` - -Returns `true` when the comparable version falls within any interval in the set. -Delegates to `VersionInterval.Contains(VersionComparable)` for each interval. - -### `Contains(VersionTag version) → bool` - -Convenience overload for `VersionTag`. Delegates to -`Contains(version.Semantic.Comparable)`. - -### `Parse(string text) → VersionIntervalSet` (static factory) - -Parses a comma-separated string of interval tokens into an ordered collection. - -#### Parsing Algorithm - -The parser walks the input character by character, tracking bracket depth: - -1. Increment depth on `[` or `(`. -2. Decrement depth on `]` or `)`. -3. When a closing bracket returns depth to 0, extract the token from the last - `tokenStart` position to the current position (inclusive). -4. Trim the token and attempt to parse it via `VersionInterval.Parse`. Tokens - that do not parse are silently discarded. -5. Advance `tokenStart` past any trailing commas and whitespace. -6. Return a `VersionIntervalSet` wrapping the collected intervals. - -This depth-tracking approach correctly handles the comma that appears inside a -single interval between its lower and upper bounds. - -## Interactions - -| Unit / Subsystem | Role | -|----------------------------|----------------------------------------------------------------------------------------| -| `VersionInterval` | Each interval token is parsed by `VersionInterval.Parse` and stored in the set | -| `ItemControlsParser` | Calls `VersionIntervalSet.Parse` to build the set from the `affected-versions` field | -| `GitHubRepoConnector` | Calls `Contains(VersionTag)` to decide whether a bug is a known issue | -| `AzureDevOpsRepoConnector` | Calls `Contains(VersionTag)` to decide whether a bug is a known issue | -| `MockRepoConnector` | Uses `VersionIntervalSet` directly in the `_issueAffectedVersions` dictionary | +The `VersionIntervalSet` unit is documented in `version-interval.md`, which covers both +`VersionInterval` and `VersionIntervalSet` comprehensively, including the data model, parsing +algorithm, `Contains` overloads, and interaction table. diff --git a/docs/design/build-mark/version/version-interval.md b/docs/design/build-mark/version/version-interval.md index 0f717d85..5bf433ea 100644 --- a/docs/design/build-mark/version/version-interval.md +++ b/docs/design/build-mark/version/version-interval.md @@ -28,7 +28,7 @@ Multiple intervals are separated by `,` **between** ranges. The parser distinguishes a separating comma from the commas that appear **inside** an interval (between the bounds) by tracking bracket depth. -## Data Model — VersionInterval +## Data Model - VersionInterval ```csharp public record VersionInterval( @@ -45,7 +45,7 @@ public record VersionInterval( | `UpperBound` | `string?` | Upper version string, or `null` if unbounded | | `UpperInclusive` | `bool` | `true` if the upper bound is inclusive (`]`) | -## Data Model — VersionIntervalSet +## Data Model - VersionIntervalSet ```csharp public record VersionIntervalSet( diff --git a/docs/design/build-mark/version/version-semantic.md b/docs/design/build-mark/version/version-semantic.md index 2fbfb45d..5fb09463 100644 --- a/docs/design/build-mark/version/version-semantic.md +++ b/docs/design/build-mark/version/version-semantic.md @@ -3,7 +3,7 @@ ## Purpose The `VersionSemantic` record type extends `VersionComparable` with semantic version metadata -support. As a C# `record`, it provides structural equality by default — two `VersionSemantic` +support. As a C# `record`, it provides structural equality by default - two `VersionSemantic` instances are equal when all their properties compare equal. It provides the full semantic version structure including build metadata while preserving comparison functionality. diff --git a/docs/design/build-mark/version/version-tag.md b/docs/design/build-mark/version/version-tag.md index d8caca0e..aa7dec8b 100644 --- a/docs/design/build-mark/version/version-tag.md +++ b/docs/design/build-mark/version/version-tag.md @@ -10,13 +10,10 @@ tag strings, enabling version equality across different tag formats.** ## Structure -+----------+------------------+------------------------------------+ -| Property | Type | Description | -+==========+==================+====================================+ -| Tag | string | Original repository tag | -+----------+------------------+------------------------------------+ -| Semantic | VersionSemantic | Parsed semantic version info | -+----------+------------------+------------------------------------+ +| Property | Type | Description | +|----------|-----------------|------------------------------| +| Tag | string | Original repository tag | +| Semantic | VersionSemantic | Parsed semantic version info | ## Delegated Properties diff --git a/docs/design/build-mark/version/version.md b/docs/design/build-mark/version/version.md index e39e9701..fa6afb83 100644 --- a/docs/design/build-mark/version/version.md +++ b/docs/design/build-mark/version/version.md @@ -10,12 +10,12 @@ range operations, ensuring consistent semantic versioning behavior across all Bu The Version subsystem is composed of six units: -- `VersionComparable` (Unit) — core semantic version comparison and ordering engine -- `VersionSemantic` (Unit) — semantic version parsing and validation -- `VersionTag` (Unit) — repository version tag processing and extraction -- `VersionInterval` (Unit) — version range representation and operations -- `VersionIntervalSet` (Unit) — ordered collection of version intervals for range queries -- `VersionCommitTag` (Unit) — version-to-commit association for build information +- `VersionComparable` (Unit) - core semantic version comparison and ordering engine +- `VersionSemantic` (Unit) - semantic version parsing and validation +- `VersionTag` (Unit) - repository version tag processing and extraction +- `VersionInterval` (Unit) - version range representation and operations +- `VersionIntervalSet` (Unit) - ordered collection of version intervals for range queries +- `VersionCommitTag` (Unit) - version-to-commit association for build information ## Version Type Hierarchy @@ -55,17 +55,12 @@ Each version type serves a specific purpose with clear boundaries: ## External Interfaces -+------------------+-----------+-------------------------------------------+ -| Interface | Direction | Protocol / Format | -+==================+===========+===========================================+ -| Repository Tags | Input | String tags from GitHub/Git repositories | -+------------------+-----------+-------------------------------------------+ -| Version Parsing | Processing| SemVer 2.0.0 compliant parsing | -+------------------+-----------+-------------------------------------------+ -| Version Compare | Processing| IComparable standard interface | -+------------------+-----------+-------------------------------------------+ -| Build Info | Output | VersionCommitTag records for build notes | -+------------------+-----------+-------------------------------------------+ +| Interface | Direction | Protocol / Format | +|------------------|------------|-------------------------------------------| +| Repository Tags | Input | String tags from GitHub/Git repositories | +| Version Parsing | Processing | SemVer 2.0.0 compliant parsing | +| Version Compare | Processing | IComparable standard interface | +| Build Info | Output | VersionCommitTag records for build notes | ## Integration Points diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index dfd4ad6e..23692c1c 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -26,6 +26,14 @@ input-files: - docs/design/build-mark/cli/cli.md - docs/design/build-mark/cli/context.md - docs/design/build-mark/configuration/configuration.md + - docs/design/build-mark/configuration/build-mark-config.md + - docs/design/build-mark/configuration/build-mark-config-reader.md + - docs/design/build-mark/configuration/configuration-load-result.md + - docs/design/build-mark/configuration/configuration-issue.md + - docs/design/build-mark/configuration/connector-config.md + - docs/design/build-mark/configuration/report-config.md + - docs/design/build-mark/configuration/rule-config.md + - docs/design/build-mark/configuration/section-config.md - docs/design/build-mark/self-test/self-test.md - docs/design/build-mark/self-test/validation.md - docs/design/build-mark/utilities/utilities.md diff --git a/docs/design/introduction.md b/docs/design/introduction.md index cc8d0439..f8a60001 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -60,7 +60,8 @@ BuildMark (System) │ ├── AzureDevOpsConnectorConfig (Unit) │ ├── ReportConfig (Unit) │ ├── SectionConfig (Unit) -│ └── RuleConfig (Unit) +│ ├── RuleConfig (Unit) +│ └── RuleMatchConfig (Unit) └── RepoConnectors (Subsystem) ├── IRepoConnector (Unit) ├── RepoConnectorBase (Unit) @@ -90,54 +91,54 @@ reviewers an explicit navigation aid from design to code: ```text src/DemaConsulting.BuildMark/ -├── Program.cs — entry point and execution orchestrator +├── Program.cs - entry point and execution orchestrator ├── BuildNotes/ -│ ├── BuildInformation.cs — build information data model -│ ├── ItemInfo.cs — item information data model -│ └── WebLink.cs — web link helper +│ ├── BuildInformation.cs - build information data model +│ ├── ItemInfo.cs - item information data model +│ └── WebLink.cs - web link helper ├── Cli/ -│ └── Context.cs — command-line argument parser and I/O owner +│ └── Context.cs - command-line argument parser and I/O owner ├── SelfTest/ -│ └── Validation.cs — self-validation test runner +│ └── Validation.cs - self-validation test runner ├── Utilities/ -│ ├── PathHelpers.cs — safe path combination utilities -│ └── ProcessRunner.cs — process runner for Git commands +│ ├── PathHelpers.cs - safe path combination utilities +│ └── ProcessRunner.cs - process runner for Git commands ├── Version/ -│ ├── VersionComparable.cs — core integer-based version comparison -│ ├── VersionSemantic.cs — semantic version with build metadata -│ ├── VersionTag.cs — repository tag parsing and normalization -│ ├── VersionInterval.cs — single version interval model and parser -│ ├── VersionIntervalSet.cs — ordered set of version intervals -│ └── VersionCommitTag.cs — version commit tag representation +│ ├── VersionComparable.cs - core integer-based version comparison +│ ├── VersionSemantic.cs - semantic version with build metadata +│ ├── VersionTag.cs - repository tag parsing and normalization +│ ├── VersionInterval.cs - single version interval model and parser +│ ├── VersionIntervalSet.cs - ordered set of version intervals +│ └── VersionCommitTag.cs - version commit tag representation ├── Configuration/ -│ ├── BuildMarkConfig.cs — top-level configuration data model -│ ├── BuildMarkConfigReader.cs — reads and parses .buildmark.yaml using YamlDotNet -│ ├── ConfigurationLoadResult.cs — holds config and any load issues -│ ├── ConfigurationIssue.cs — single issue with location and severity -│ ├── ConnectorConfig.cs — connector envelope data model -│ ├── GitHubConnectorConfig.cs — GitHub connector settings data model -│ ├── AzureDevOpsConnectorConfig.cs — Azure DevOps connector settings data model -│ ├── ReportConfig.cs — report output settings data model -│ ├── SectionConfig.cs — report section definition data model -│ └── RuleConfig.cs — routing rule data model +│ ├── BuildMarkConfig.cs - top-level configuration data model +│ ├── BuildMarkConfigReader.cs - reads and parses .buildmark.yaml using YamlDotNet +│ ├── ConfigurationLoadResult.cs - holds config and any load issues +│ ├── ConfigurationIssue.cs - single issue with location and severity +│ ├── ConnectorConfig.cs - connector envelope data model +│ ├── GitHubConnectorConfig.cs - GitHub connector settings data model +│ ├── AzureDevOpsConnectorConfig.cs - Azure DevOps connector settings data model +│ ├── ReportConfig.cs - report output settings data model +│ ├── SectionConfig.cs - report section definition data model +│ └── RuleConfig.cs - routing rule and rule-match condition data models └── RepoConnectors/ - ├── IRepoConnector.cs — repository connector interface - ├── RepoConnectorBase.cs — repository connector base class - ├── RepoConnectorFactory.cs — repository connector factory - ├── ItemRouter.cs — shared item routing logic - ├── ItemControlsInfo.cs — item controls data model - ├── ItemControlsParser.cs — buildmark block parser + ├── IRepoConnector.cs - repository connector interface + ├── RepoConnectorBase.cs - repository connector base class + ├── RepoConnectorFactory.cs - repository connector factory + ├── ItemRouter.cs - shared item routing logic + ├── ItemControlsInfo.cs - item controls data model + ├── ItemControlsParser.cs - buildmark block parser ├── GitHub/ - │ ├── GitHubRepoConnector.cs — GitHub API integration - │ ├── GitHubGraphQLClient.cs — GraphQL API client - │ └── GitHubGraphQLTypes.cs — GraphQL type definitions + │ ├── GitHubRepoConnector.cs - GitHub API integration + │ ├── GitHubGraphQLClient.cs - GraphQL API client + │ └── GitHubGraphQLTypes.cs - GraphQL type definitions ├── AzureDevOps/ - │ ├── AzureDevOpsRepoConnector.cs — Azure DevOps API integration - │ ├── AzureDevOpsRestClient.cs — REST API client - │ ├── AzureDevOpsApiTypes.cs — REST API type definitions - │ └── WorkItemMapper.cs — work item to ItemInfo mapper + │ ├── AzureDevOpsRepoConnector.cs - Azure DevOps API integration + │ ├── AzureDevOpsRestClient.cs - REST API client + │ ├── AzureDevOpsApiTypes.cs - REST API type definitions + │ └── WorkItemMapper.cs - work item to ItemInfo mapper └── Mock/ - └── MockRepoConnector.cs — mock repository connector for self-test + └── MockRepoConnector.cs - mock repository connector for self-test ``` The test project mirrors the same layout under `test/DemaConsulting.BuildMark.Tests/`. @@ -154,8 +155,7 @@ Throughout this document: ## References -- [BuildMark User Guide][user-guide] +- See the BuildMark User Guide for user-facing documentation. - [BuildMark Repository][repo] -[user-guide]: ../user_guide/introduction.md [repo]: https://github.com/demaconsulting/BuildMark diff --git a/docs/reqstream/build-mark/build-mark.yaml b/docs/reqstream/build-mark/build-mark.yaml index 2ead3483..28fd32ae 100644 --- a/docs/reqstream/build-mark/build-mark.yaml +++ b/docs/reqstream/build-mark/build-mark.yaml @@ -12,15 +12,17 @@ sections: developers and build systems to invoke the tool programmatically without requiring a graphical user interface. tests: - - IntegrationTest_VersionFlag_OutputsVersion - - IntegrationTest_HelpFlag_OutputsUsageInformation + - BuildMark_VersionFlag_OutputsVersion + - BuildMark_HelpFlag_OutputsUsageInformation children: - BuildMark-Cli-Context - BuildMark-Cli-Version - BuildMark-Cli-Help - BuildMark-Cli-Silent - BuildMark-Cli-BuildVersion - - BuildMark-Cli-Report + - BuildMark-Cli-ReportFile + - BuildMark-Cli-Depth + - BuildMark-Cli-IncludeKnownIssues - BuildMark-Cli-Log - BuildMark-Cli-Validate - BuildMark-Cli-Lint @@ -36,7 +38,7 @@ sections: which is critical for debugging issues, ensuring compatibility, and tracking tool updates across different environments. tests: - - IntegrationTest_VersionFlag_OutputsVersion + - BuildMark_VersionFlag_OutputsVersion children: - BuildMark-Cli-Version @@ -47,7 +49,7 @@ sections: commands, and parameter descriptions. This improves user experience and reduces the learning curve for new users without requiring external documentation. tests: - - IntegrationTest_HelpFlag_OutputsUsageInformation + - BuildMark_HelpFlag_OutputsUsageInformation children: - BuildMark-Cli-Help @@ -58,7 +60,7 @@ sections: output can clutter logs or interfere with parsing build results. This allows users to control verbosity based on their specific use case. tests: - - IntegrationTest_SilentFlag_SuppressesOutput + - BuildMark_SilentFlag_SuppressesOutput children: - BuildMark-Cli-Silent @@ -69,7 +71,7 @@ sections: troubleshooting, and post-build analysis. This is particularly valuable in automated environments where console output may not be easily accessible. tests: - - IntegrationTest_LogParameter_IsAccepted + - BuildMark_LogParameter_IsAccepted children: - BuildMark-Cli-Log @@ -80,7 +82,7 @@ sections: from source control, enabling flexible version management strategies and supporting scenarios where version numbers are determined externally. tests: - - IntegrationTest_BuildVersionParameter_IsAccepted + - BuildMark_BuildVersionParameter_IsAccepted children: - BuildMark-Cli-BuildVersion @@ -91,7 +93,7 @@ sections: when invalid inputs are provided. This improves reliability, helps users identify configuration errors early, and prevents unexpected behavior in automated workflows. tests: - - IntegrationTest_InvalidArgument_ShowsError + - BuildMark_InvalidArgument_ShowsError children: - BuildMark-Cli-InvalidArgs @@ -102,7 +104,7 @@ sections: the tool fails safely when given incorrect paths. This improves user experience and makes troubleshooting easier in production environments. tests: - - IntegrationTest_InvalidReportPath_ShowsError + - BuildMark_InvalidReportPath_ShowsError children: - BuildMark-Cli-InvalidArgs - BuildMark-Utilities-SafePaths @@ -115,7 +117,7 @@ sections: or failure conditions programmatically. This is fundamental for proper integration into CI/CD pipelines and automated workflows. tests: - - IntegrationTest_InvalidArgument_ShowsError + - BuildMark_InvalidArgument_ShowsError children: - BuildMark-Cli-ExitCode @@ -129,7 +131,7 @@ sections: ensures consistency in release documentation by deriving information directly from source control history. tests: - - IntegrationTest_Report_GeneratesMarkdownWithVersionInformation + - BuildMark_Report_GeneratesMarkdownWithVersionInformation children: - BuildMark-RepoConnectors-GitHub @@ -140,7 +142,7 @@ sections: work items, enabling comprehensive release notes that link features, bug fixes, and improvements to their corresponding GitHub issues and pull requests. tests: - - IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks + - BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks children: - BuildMark-RepoConnectors-GitHub @@ -151,7 +153,7 @@ sections: accurately capture what changed in each version. This is essential for semantic versioning workflows and helps users understand the scope of changes between releases. tests: - - IntegrationTest_Report_ShowsVersionRangeFromPreviousRelease + - BuildMark_Report_ShowsVersionRangeFromPreviousRelease children: - BuildMark-RepoConnectors-GitHub - BuildMark-Version-Subsystem @@ -166,7 +168,7 @@ sections: during release processes and ensures consistency in release documentation by deriving information directly from source control history and work items. tests: - - IntegrationTest_AzureDevOps_Report_GeneratesMarkdownWithVersionInformation + - BuildMark_AzureDevOps_Report_GeneratesMarkdownWithVersionInformation children: - BuildMark-RepoConnectors-AzureDevOps @@ -178,7 +180,7 @@ sections: bug fixes, and improvements to their corresponding Azure DevOps work items and pull requests. tests: - - IntegrationTest_AzureDevOps_Report_ContainsChangesAndBugFixesWithHyperlinks + - BuildMark_AzureDevOps_Report_ContainsChangesAndBugFixesWithHyperlinks children: - BuildMark-RepoConnectors-AzureDevOps @@ -190,7 +192,7 @@ sections: essential for semantic versioning workflows and helps users understand the scope of changes between releases. tests: - - IntegrationTest_AzureDevOps_Report_ShowsVersionRangeFromPreviousRelease + - BuildMark_AzureDevOps_Report_ShowsVersionRangeFromPreviousRelease children: - BuildMark-RepoConnectors-AzureDevOps @@ -202,7 +204,7 @@ sections: A dedicated configuration file separates persistent repository settings from runtime arguments, simplifying CI invocations and enabling version-controlled configuration. tests: - - IntegrationTest_Report_ConsumesConfigurationFileDuringGeneration + - BuildMark_Report_ConsumesConfigurationFileDuringGeneration children: - BuildMark-Configuration-Read - BuildMark-Program-Lint @@ -214,7 +216,8 @@ sections: Centralizing connector configuration in the repository avoids repeating connection details across multiple CI pipelines. tests: - - IntegrationTest_Report_UsesConnectorForBuildData + - BuildMark_Report_UsesConnectorForBuildData + - BuildMark_Config_ConnectorType_ReadFromConfigFile children: - BuildMark-Configuration-ConnectorConfig - BuildMark-RepoConnectors-Factory @@ -226,7 +229,7 @@ sections: Configurable sections let teams tailor the structure of generated build notes to their conventions without modifying the tool. tests: - - IntegrationTest_Report_ContainsSectionDefinitions + - BuildMark_Report_ContainsSectionDefinitions children: - BuildMark-Configuration-Read @@ -238,7 +241,7 @@ sections: Declarative routing rules allow repository owners to control categorization of items without embedding logic in CI scripts. tests: - - IntegrationTest_Report_RoutesItemsToCorrectSections + - BuildMark_Report_RoutesItemsToCorrectSections children: - BuildMark-Configuration-Read - BuildMark-RepoConnectors-ItemRouter @@ -252,7 +255,7 @@ sections: running a full build. Linting with precise issue locations helps teams fix configuration errors quickly. tests: - - IntegrationTest_LintFlag_IsAccepted + - BuildMark_LintFlag_IsAccepted children: - BuildMark-Configuration-Issues - BuildMark-Program-Lint @@ -267,7 +270,7 @@ sections: markdown format ensures broad compatibility and easy integration into existing documentation workflows. tests: - - IntegrationTest_Report_GeneratesMarkdownWithVersionInformation + - BuildMark_Report_GeneratesMarkdownWithVersionInformation children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -279,7 +282,7 @@ sections: documentation structures by adjusting heading levels. This flexibility ensures reports integrate properly regardless of the document hierarchy. tests: - - IntegrationTest_Report_DepthTwo_UsesLevelTwoHeadings + - BuildMark_Report_DepthTwo_UsesLevelTwoHeadings children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -291,7 +294,7 @@ sections: providing essential context for users reviewing changes and enabling proper organization of release documentation. tests: - - IntegrationTest_Report_GeneratesMarkdownWithVersionInformation + - BuildMark_Report_GeneratesMarkdownWithVersionInformation children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -303,7 +306,7 @@ sections: release, helping users understand the evolution of the software and make informed decisions about upgrading. tests: - - IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks + - BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -315,7 +318,7 @@ sections: maintenance, and helps users determine if specific problems they encountered have been addressed in newer versions. tests: - - IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks + - BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -342,7 +345,7 @@ sections: release, but an LTS branch that was cut before the fix still needs to report the bug as a known issue. The affected-versions field captures that scenario precisely. tests: - - IntegrationTest_Report_IncludesKnownIssues_WhenFlagIsSet + - BuildMark_Report_IncludesKnownIssues_WhenFlagIsSet children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -357,7 +360,7 @@ sections: multiple versions or focused notes for specific releases. This flexibility supports various documentation scenarios and user needs. tests: - - IntegrationTest_Report_ShowsVersionRangeFromPreviousRelease + - BuildMark_Report_ShowsVersionRangeFromPreviousRelease children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -369,19 +372,48 @@ sections: enabling users to investigate specific items of interest and maintaining traceability between release notes and source repository work items. tests: - - IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks + - BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks children: - BuildMark-BuildNotes-ReportModel - BuildMark-RepoConnectors-GitHub - - id: BuildMark-Report-Structure - title: The tool shall format build notes with proper markdown structure. + - id: BuildMark-Report-Structure-Headings + title: >- + The tool shall format build notes using ATX-style headings at the configured + heading depth. + justification: | + ATX-style headings at a configurable depth allow BuildMark reports to be + embedded at any level within a larger document without disrupting the heading + hierarchy of the surrounding content. + tests: + - BuildMark_Report_DepthTwo_UsesLevelTwoHeadings + children: + - BuildMark-BuildNotes-ReportModel + - BuildMark-Program-Report + + - id: BuildMark-Report-Structure-Lists + title: >- + The tool shall format change and bug entries as bullet list items in the + generated build notes. + justification: | + Bullet lists provide a scannable, visually distinct format for change and bug + entries that renders correctly across markdown processors and documentation + platforms. + tests: + - BuildMark_Report_GeneratesMarkdownWithVersionInformation + children: + - BuildMark-BuildNotes-ReportModel + - BuildMark-Program-Report + + - id: BuildMark-Report-Structure-VersionHeading + title: >- + The tool shall place the version heading as the first section element of the + generated build notes. justification: | - Proper markdown structure ensures reports render correctly across different - markdown processors and documentation platforms. Well-structured documents are - more readable, professional, and maintainable. + Leading with the version heading makes the report immediately identifiable and + ensures a consistent structure across all generated documents. tests: - - IntegrationTest_Report_GeneratesMarkdownWithVersionInformation + - BuildMark_Report_GeneratesMarkdownWithVersionInformation children: - BuildMark-BuildNotes-ReportModel - BuildMark-Program-Report @@ -396,7 +428,7 @@ sections: eliminates the need for custom GitHub labels or external configuration files and keeps all item-level controls co-located with the work item itself. tests: - - IntegrationTest_Report_RecognizesBuildmarkCodeBlock + - BuildMark_Report_RecognizesBuildmarkCodeBlock children: - BuildMark-RepoConnectors-ItemControls @@ -409,7 +441,7 @@ sections: notes, or promote items that would otherwise be excluded. A visibility override gives fine-grained control without altering GitHub labels. tests: - - IntegrationTest_Report_VisibilityFieldControlsInclusion + - BuildMark_Report_VisibilityFieldControlsInclusion children: - BuildMark-RepoConnectors-ItemControls - BuildMark-RepoConnectors-GitHub @@ -423,7 +455,7 @@ sections: value ensures these items still appear in the generated notes when the developer explicitly requests it. tests: - - IntegrationTest_Report_PublicVisibility_IncludesItem + - BuildMark_Report_PublicVisibility_IncludesItem children: - BuildMark-RepoConnectors-ItemControls - BuildMark-RepoConnectors-GitHub @@ -437,7 +469,7 @@ sections: are often irrelevant to end-users. The internal value lets developers suppress such items without removing the labels that are used for other project-management purposes. tests: - - IntegrationTest_Report_InternalVisibility_ExcludesItem + - BuildMark_Report_InternalVisibility_ExcludesItem children: - BuildMark-RepoConnectors-ItemControls - BuildMark-RepoConnectors-GitHub @@ -451,34 +483,34 @@ sections: release notes. A type override lets developers correct the classification for any individual item without modifying project-wide label conventions. tests: - - IntegrationTest_Report_TypeFieldOverridesClassification + - BuildMark_Report_TypeFieldOverridesClassification children: - BuildMark-RepoConnectors-ItemControls - BuildMark-RepoConnectors-GitHub - id: BuildMark-Controls-TypeBug title: >- - The tool shall classify an item as a bug fix when type is set to bug, - placing it in the Bugs Fixed section of the report. + The tool shall classify an item as a bug when type is set to bug and route it to the + configured bug-fix section (default: 'Bugs Fixed'). justification: | Correct classification of bug fixes is important for end-users assessing whether a release addresses their reported problems. An explicit override ensures accuracy even when labels are missing or inconsistent. tests: - - IntegrationTest_Report_TypeBug_PlacesItemInBugsFixed + - BuildMark_Report_TypeBug_PlacesItemInBugsFixed children: - BuildMark-RepoConnectors-ItemControls - BuildMark-RepoConnectors-GitHub - id: BuildMark-Controls-TypeFeature title: >- - The tool shall classify an item as a feature or change when type is set to - feature, placing it in the Changes section of the report. + The tool shall classify an item as a feature when type is set to feature and route it + to the configured changes section (default: 'Changes'). justification: | Not all feature work carries the expected labels. An explicit override ensures the item appears under Changes rather than being misclassified or omitted. tests: - - IntegrationTest_Report_TypeFeature_PlacesItemInChanges + - BuildMark_Report_TypeFeature_PlacesItemInChanges children: - BuildMark-RepoConnectors-ItemControls - BuildMark-RepoConnectors-GitHub @@ -492,7 +524,7 @@ sections: enables downstream tooling to filter or highlight relevant items for a given release without relying on external spreadsheets or manual documentation. tests: - - IntegrationTest_Report_AffectedVersionsField_ProcessesSuccessfully + - BuildMark_Report_AffectedVersionsField_ProcessesSuccessfully children: - BuildMark-RepoConnectors-ItemControls - BuildMark-VersionInterval-Parse @@ -507,7 +539,7 @@ sections: in a single field, covering scenarios such as a bug present in two separate release branches. tests: - - IntegrationTest_Report_AffectedVersionsInterval_ParsesNotation + - BuildMark_Report_AffectedVersionsInterval_ParsesNotation children: - BuildMark-RepoConnectors-ItemControls - BuildMark-VersionInterval-Parse @@ -521,7 +553,7 @@ sections: the block to be hidden in an HTML comment satisfies both the need to supply control data and the desire for a tidy rendered appearance. tests: - - IntegrationTest_Report_HiddenBuildmarkBlock_IsRecognized + - BuildMark_Report_HiddenBuildmarkBlock_IsRecognized children: - BuildMark-RepoConnectors-ItemControls @@ -534,7 +566,7 @@ sections: environment, providing evidence that the user's environment does not introduce unexpected behaviors, and giving users evidence they can rely on the tool's output. tests: - - IntegrationTest_ValidateFlag_RunsSelfValidation + - BuildMark_ValidateFlag_RunsSelfValidation children: - BuildMark-SelfTest-Qualification @@ -545,7 +577,9 @@ sections: CI/CD pipelines, and reporting tools. This allows BuildMark's validation results to be tracked, analyzed, and reported alongside other test metrics. tests: - - IntegrationTest_ResultsParameter_IsAccepted + - BuildMark_ResultsParameter_IsAccepted + - BuildMark_ResultsParameter_WritesTrxFile + - BuildMark_ResultsParameter_WritesJUnitFile children: - BuildMark-SelfTest-ResultsOutput @@ -556,7 +590,8 @@ sections: Studio Test Explorer, providing native support for .NET development environments and Azure DevOps pipelines. tests: - - IntegrationTest_ResultsParameter_IsAccepted + - BuildMark_ResultsParameter_IsAccepted + - BuildMark_ResultsParameter_WritesTrxFile children: - BuildMark-SelfTest-ResultsOutput @@ -567,6 +602,7 @@ sections: testing tools. Supporting this format ensures BuildMark can integrate with diverse toolchains beyond the Microsoft ecosystem. tests: - - IntegrationTest_ResultsParameter_IsAccepted + - BuildMark_ResultsParameter_IsAccepted + - BuildMark_ResultsParameter_WritesJUnitFile children: - BuildMark-SelfTest-ResultsOutput diff --git a/docs/reqstream/build-mark/cli/cli.yaml b/docs/reqstream/build-mark/cli/cli.yaml index 1674b999..d7e58a32 100644 --- a/docs/reqstream/build-mark/cli/cli.yaml +++ b/docs/reqstream/build-mark/cli/cli.yaml @@ -74,14 +74,31 @@ sections: children: - BuildMark-Context-ArgumentParsing - - id: BuildMark-Cli-Report - title: >- - The Cli subsystem shall support --report, --depth, and --include-known-issues - flags to configure report output. + - id: BuildMark-Cli-ReportFile + title: The Cli subsystem shall support --report flag to specify the report output file path. + justification: | + Enables users to direct build notes output to a specific file, supporting + flexible integration into CI/CD pipelines and documentation workflows. + tests: + - Cli_ReportFlags_SetProperties + children: + - BuildMark-Context-ArgumentParsing + + - id: BuildMark-Cli-Depth + title: The Cli subsystem shall support --depth flag to configure the markdown heading depth. + justification: | + Allows users to embed the generated report at any heading level within a larger + markdown document, ensuring proper document hierarchy in compound documents. + tests: + - Cli_ReportFlags_SetProperties + children: + - BuildMark-Context-ArgumentParsing + + - id: BuildMark-Cli-IncludeKnownIssues + title: The Cli subsystem shall support --include-known-issues flag to include known issues in the report. justification: | - Enables users to direct build notes output to a specific file, embed it at - any heading level within a larger markdown document, and control whether - known issues are included in the report. + Provides users control over whether known issues are included in generated build + notes, enabling comprehensive release documentation when needed. tests: - Cli_ReportFlags_SetProperties children: diff --git a/docs/reqstream/build-mark/platform-requirements.yaml b/docs/reqstream/build-mark/platform-requirements.yaml index 8bc28b48..b921f840 100644 --- a/docs/reqstream/build-mark/platform-requirements.yaml +++ b/docs/reqstream/build-mark/platform-requirements.yaml @@ -11,9 +11,9 @@ sections: # Test source pattern "windows@" ensures these tests ran on Windows. # This filtering is necessary to prove Windows OS functionality. tests: - - windows@IntegrationTest_VersionFlag_OutputsVersion - - windows@IntegrationTest_HelpFlag_OutputsUsageInformation - - windows@IntegrationTest_ReportParameter_IsAccepted + - windows@BuildMark_VersionFlag_OutputsVersion + - windows@BuildMark_HelpFlag_OutputsUsageInformation + - windows@BuildMark_ReportParameter_IsAccepted - windows@BuildMark_MarkdownReportGeneration - windows@BuildMark_GitIntegration - windows@BuildMark_IssueTracking @@ -28,9 +28,9 @@ sections: # Test source pattern "ubuntu@" ensures these tests ran on Linux. # This filtering is necessary to prove Linux OS functionality. tests: - - ubuntu@IntegrationTest_VersionFlag_OutputsVersion - - ubuntu@IntegrationTest_HelpFlag_OutputsUsageInformation - - ubuntu@IntegrationTest_ReportParameter_IsAccepted + - ubuntu@BuildMark_VersionFlag_OutputsVersion + - ubuntu@BuildMark_HelpFlag_OutputsUsageInformation + - ubuntu@BuildMark_ReportParameter_IsAccepted - ubuntu@BuildMark_MarkdownReportGeneration - ubuntu@BuildMark_GitIntegration - ubuntu@BuildMark_IssueTracking @@ -45,9 +45,9 @@ sections: # Test source pattern "macos@" ensures these tests ran on macOS. # This filtering is necessary to prove macOS functionality. tests: - - macos@IntegrationTest_VersionFlag_OutputsVersion - - macos@IntegrationTest_HelpFlag_OutputsUsageInformation - - macos@IntegrationTest_ReportParameter_IsAccepted + - macos@BuildMark_VersionFlag_OutputsVersion + - macos@BuildMark_HelpFlag_OutputsUsageInformation + - macos@BuildMark_ReportParameter_IsAccepted - macos@BuildMark_MarkdownReportGeneration - macos@BuildMark_GitIntegration - macos@BuildMark_IssueTracking diff --git a/docs/reqstream/build-mark/program.yaml b/docs/reqstream/build-mark/program.yaml index 11c9d46d..be80c613 100644 --- a/docs/reqstream/build-mark/program.yaml +++ b/docs/reqstream/build-mark/program.yaml @@ -52,3 +52,81 @@ sections: and generation of the output markdown report. tests: - Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + + - id: BuildMark-Program-Silent + title: The Program class shall support --silent flag to suppress console output during execution. + justification: | + Silent mode enables automated build pipelines to invoke BuildMark without + cluttering their output logs when only the exit code or report file is needed. + tests: + - BuildMark_SilentFlag_SuppressesOutput + - Program_Run_WithSilentFlag_SuppressesOutput + + - id: BuildMark-Program-Log + title: The Program class shall support --log flag to write execution output to a log file. + justification: | + Log file support enables persistent capture of BuildMark output for audit trails + and post-build analysis in automated environments. + tests: + - BuildMark_LogParameter_IsAccepted + - Program_Run_WithLogFlag_WritesToLogFile + + - id: BuildMark-Program-Results + title: The Program class shall support --results flag to write validation results to a test results file. + justification: | + Writing results to standard test result formats (TRX or JUnit XML) enables + integration with CI/CD platforms that process test result files. + tests: + - BuildMark_ResultsParameter_IsAccepted + - Program_Run_WithResultsFlag_WritesResultsFile + + - id: BuildMark-Program-BuildVersion + title: The Program class shall support --build-version flag to specify the version for report generation. + justification: | + Specifying the build version allows users to override or supplement version information + from source control, enabling flexible version management strategies. + tests: + - BuildMark_BuildVersionParameter_IsAccepted + - Program_Run_WithBuildVersionFlag_AcceptsBuildVersion + + - id: BuildMark-Program-Depth + title: The Program class shall support --depth flag to configure the markdown heading depth of the report. + justification: | + Configurable heading depth allows BuildMark reports to be embedded at any level + within a larger document without disrupting the document hierarchy. + tests: + - BuildMark_DepthParameter_IsAccepted + - BuildMark_Report_DepthTwo_UsesLevelTwoHeadings + - Program_Run_WithDepthFlag_SetsHeadingDepth + + - id: BuildMark-Program-IncludeKnownIssues + title: >- + The Program class shall support --include-known-issues flag to include known + issues in the generated build notes report. + justification: | + Known-issue disclosure in release notes promotes transparency and helps users + avoid known pitfalls. The flag enables this optional section without affecting + the default report output. + tests: + - Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + + - id: BuildMark-Program-ErrorHandling-InvalidBuildVersion + title: >- + The Program class shall report an error and set exit code 1 when an invalid + build version is provided. + justification: | + Clear error reporting on invalid version inputs allows users and CI/CD pipelines + to detect and diagnose problems quickly without inspecting logs for root causes. + tests: + - Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode + + - id: BuildMark-Program-ErrorHandling-ConnectorFailure + title: >- + The Program class shall report an error and set exit code 1 when the repository + connector fails during data retrieval. + justification: | + Clear error reporting on connector failures allows users and CI/CD pipelines to + detect and diagnose integration problems quickly without inspecting logs for root + causes. + tests: + - Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml index 13898784..6bf7e359 100644 --- a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml +++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml @@ -1,10 +1,13 @@ --- -# Software Unit Requirements for the AzureDevOps Subsystem +# Software Unit Requirements for the AzureDevOpsRepoConnector Class # -# The AzureDevOps subsystem provides the production connector for Azure DevOps -# repositories. It includes AzureDevOpsRepoConnector (main connector), -# AzureDevOpsRestClient (REST API client), AzureDevOpsApiTypes (type definitions), -# and WorkItemMapper (work item to ItemInfo mapper). +# The AzureDevOpsRepoConnector class connects to Azure DevOps using the REST API +# to retrieve commits, tags, pull requests, and work items, assembling them into +# structured BuildInformation for report generation. +# +# NOTE: REST client requirements have been extracted to azure-devops-rest-client.yaml. +# NOTE: Work item mapper and custom fields requirements have been extracted to +# work-item-mapper.yaml. sections: - title: AzureDevOps Unit Requirements @@ -74,20 +77,6 @@ sections: - AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug - AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature - - id: BuildMark-AzureDevOps-CustomFields - title: >- - The WorkItemMapper class shall read Custom.Visibility and Custom.AffectedVersions - custom fields from Azure DevOps work items to control item visibility and version ranges. - justification: | - Azure DevOps work items support native custom fields for visibility and version range - metadata. These fields provide a more natural Azure DevOps-native alternative to - buildmark code blocks for teams that prefer not to embed YAML in descriptions. - Custom fields take precedence over buildmark blocks when both are present. - tests: - - WorkItemMapper_ExtractItemControls_CustomVisibilityField_ReturnsMappedControls - - WorkItemMapper_ExtractItemControls_CustomAffectedVersionsField_ReturnsMappedVersionSet - - WorkItemMapper_ExtractItemControls_CustomFieldsTakePrecedenceOverBuildmarkBlock - - id: BuildMark-AzureDevOps-Rules title: >- The AzureDevOpsRepoConnector class shall apply configured routing rules and sections @@ -100,39 +89,3 @@ sections: tests: - AzureDevOpsRepoConnector_Configure_WithRules_HasRulesReturnsTrue - AzureDevOpsRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections - - - id: BuildMark-AzureDevOps-RestClient - title: >- - The AzureDevOpsRestClient shall retrieve repository details, commits, tags, - pull requests, and work items from the Azure DevOps API, including support - for paginated responses. - justification: | - The Azure DevOps REST API returns large result sets in pages. The client must - transparently follow pagination to retrieve complete repository data so that - build information reflects the full history. Authentication and response - parsing must be handled reliably to support consistent data retrieval. - tests: - - AzureDevOpsRestClient_GetRepositoryAsync_ValidResponse_ReturnsRepository - - AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits - - AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests - - AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_ValidResponse_ReturnsWorkItemRefs - - AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems - - AzureDevOpsRestClient_QueryWorkItemsAsync_ValidWiql_ReturnsWorkItemIds - - - id: BuildMark-AzureDevOps-WorkItemMapper - title: >- - The WorkItemMapper class shall map Azure DevOps work items to ItemInfo objects, - applying type normalization and state-based filtering. - justification: | - Azure DevOps work items use platform-specific type names and state values that must - be normalized to the BuildMark ItemInfo model. The mapper centralizes this translation - logic to ensure consistent type classification and state filtering across all connector - operations. - tests: - - WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem - - WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureItem - - WorkItemMapper_MapWorkItemToItemInfo_EpicType_ReturnsFeatureItem - - WorkItemMapper_MapWorkItemToItemInfo_TaskType_ReturnsTaskItem - - WorkItemMapper_IsWorkItemResolved_ResolvedState_ReturnsTrue - - WorkItemMapper_IsWorkItemResolved_ActiveState_ReturnsFalse - - WorkItemMapper_GetWorkItemTypeForRuleMatching_ReturnsWorkItemTypeName diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml new file mode 100644 index 00000000..1bf013c3 --- /dev/null +++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml @@ -0,0 +1,78 @@ +--- +# Software Unit Requirements for the AzureDevOpsRestClient Class +# +# AzureDevOpsRestClient handles paginated HTTPS communication with the Azure +# DevOps REST API endpoint. It supports both cloud (dev.azure.com) and +# on-premises Azure DevOps Server instances. + +sections: + - title: AzureDevOpsRestClient Unit Requirements + requirements: + - id: BuildMark-AzureDevOps-RestClient + title: >- + The AzureDevOpsRestClient shall retrieve repository details, commits, tags, + pull requests, and work items from the Azure DevOps API, including support + for paginated responses. + justification: | + The Azure DevOps REST API returns large result sets in pages. The client must + transparently follow pagination to retrieve complete repository data so that + build information reflects the full history. Authentication and response + parsing must be handled reliably to support consistent data retrieval. + tests: + - AzureDevOpsRestClient_GetRepositoryAsync_ValidResponse_ReturnsRepository + - AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits + - AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests + - AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_ValidResponse_ReturnsWorkItemRefs + - AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems + - AzureDevOpsRestClient_QueryWorkItemsAsync_ValidWiql_ReturnsWorkItemIds + - AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_StringValuedIds_DeserializesCorrectly + - AzureDevOpsRestClient_QueryWorkItemsAsync_StringValuedIds_DeserializesCorrectly + children: + - BuildMark-AzureDevOps-GetRepository + - BuildMark-AzureDevOps-GetCommits + - BuildMark-AzureDevOps-GetTags + - BuildMark-AzureDevOps-GetPullRequests + - BuildMark-AzureDevOps-GetWorkItems + + - id: BuildMark-AzureDevOps-GetRepository + title: The AzureDevOpsRestClient shall retrieve repository details from the Azure DevOps API. + justification: | + Repository detail retrieval provides the repository identity needed for subsequent + API calls and for generating accurate hyperlinks in build notes. + tests: + - AzureDevOpsRestClient_GetRepositoryAsync_ValidResponse_ReturnsRepository + + - id: BuildMark-AzureDevOps-GetCommits + title: The AzureDevOpsRestClient shall retrieve commits from the Azure DevOps API with pagination support. + justification: | + Commit retrieval is required to determine the set of changes between version tags. + Pagination ensures complete history is retrieved regardless of repository size. + tests: + - AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits + + - id: BuildMark-AzureDevOps-GetTags + title: The AzureDevOpsRestClient shall retrieve tags from the Azure DevOps API. + justification: | + Tag retrieval is required to identify version boundaries for build note generation. + All tags must be fetched to correctly determine previous version baselines. + tests: + - AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags + + - id: BuildMark-AzureDevOps-GetPullRequests + title: The AzureDevOpsRestClient shall retrieve pull requests from the Azure DevOps API with pagination support. + justification: | + Pull request retrieval provides the change entries for build notes. Pagination + ensures all pull requests in the version range are included. + tests: + - AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests + - AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_StringValuedIds_DeserializesCorrectly + + - id: BuildMark-AzureDevOps-GetWorkItems + title: The AzureDevOpsRestClient shall retrieve work items from the Azure DevOps API with pagination support. + justification: | + Work item retrieval provides the issue and bug entries for build notes. Pagination + and WIQL query support ensure complete work item coverage. + tests: + - AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems + - AzureDevOpsRestClient_QueryWorkItemsAsync_ValidWiql_ReturnsWorkItemIds + - AzureDevOpsRestClient_QueryWorkItemsAsync_StringValuedIds_DeserializesCorrectly diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml new file mode 100644 index 00000000..6ab75932 --- /dev/null +++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml @@ -0,0 +1,39 @@ +--- +# AzureDevOps Sub-Subsystem Requirements +# +# PURPOSE: +# - Define requirements for the BuildMark AzureDevOps sub-subsystem +# - The AzureDevOps sub-subsystem provides the production connector used when +# the repository host is Azure DevOps (cloud or on-premises) +# - Sub-subsystem requirements describe the externally visible contract +# satisfied by the units beneath this sub-subsystem + +sections: + - title: AzureDevOps Sub-Subsystem Requirements + requirements: + - id: BuildMark-AzureDevOps-SubSystem + title: >- + The AzureDevOps sub-subsystem shall provide a repository connector that implements + the IRepoConnector interface, retrieves build information from Azure DevOps, supports + URL parsing, item controls, configurable routing rules, REST API access, and work + item mapping. + justification: | + Generating build notes from Azure DevOps repositories requires a dedicated sub-subsystem + that can authenticate to the Azure DevOps REST API, retrieve commits, tags, pull + requests, and work items, apply item-controls overrides from buildmark blocks and + custom fields, and assemble them into structured build information. + tests: + - AzureDevOps_ImplementsInterface_ReturnsTrue + - AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + - AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges + - AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues + - AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases + children: + - BuildMark-AzureDevOps-UrlParsing + - BuildMark-AzureDevOps-ConnectorConfig + - BuildMark-AzureDevOps-BuildInformation + - BuildMark-AzureDevOps-ItemControls + - BuildMark-AzureDevOps-CustomFields + - BuildMark-AzureDevOps-Rules + - BuildMark-AzureDevOps-RestClient + - BuildMark-AzureDevOps-WorkItemMapper diff --git a/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml b/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml new file mode 100644 index 00000000..d81b80ee --- /dev/null +++ b/docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml @@ -0,0 +1,41 @@ +--- +# Software Unit Requirements for the WorkItemMapper Class +# +# WorkItemMapper maps Azure DevOps work items to ItemInfo objects, applying +# type normalization, state-based filtering, and custom field extraction for +# visibility and affected-versions controls. + +sections: + - title: WorkItemMapper Unit Requirements + requirements: + - id: BuildMark-AzureDevOps-WorkItemMapper + title: >- + The WorkItemMapper class shall map Azure DevOps work items to ItemInfo objects, + applying type normalization and state-based filtering. + justification: | + Azure DevOps work items use platform-specific type names and state values that must + be normalized to the BuildMark ItemInfo model. The mapper centralizes this translation + logic to ensure consistent type classification and state filtering across all connector + operations. + tests: + - WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem + - WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureItem + - WorkItemMapper_MapWorkItemToItemInfo_EpicType_ReturnsFeatureItem + - WorkItemMapper_MapWorkItemToItemInfo_TaskType_ReturnsTaskItem + - WorkItemMapper_IsWorkItemResolved_ResolvedState_ReturnsTrue + - WorkItemMapper_IsWorkItemResolved_ActiveState_ReturnsFalse + - WorkItemMapper_GetWorkItemTypeForRuleMatching_ReturnsWorkItemTypeName + + - id: BuildMark-AzureDevOps-CustomFields + title: >- + The WorkItemMapper class shall read Custom.Visibility and Custom.AffectedVersions + custom fields from Azure DevOps work items to control item visibility and version ranges. + justification: | + Azure DevOps work items support native custom fields for visibility and version range + metadata. These fields provide a more natural Azure DevOps-native alternative to + buildmark code blocks for teams that prefer not to embed YAML in descriptions. + Custom fields take precedence over buildmark blocks when both are present. + tests: + - WorkItemMapper_ExtractItemControls_CustomVisibilityField_ReturnsMappedControls + - WorkItemMapper_ExtractItemControls_CustomAffectedVersionsField_ReturnsMappedVersionSet + - WorkItemMapper_ExtractItemControls_CustomFieldsTakePrecedenceOverBuildmarkBlock diff --git a/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml b/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml new file mode 100644 index 00000000..56665154 --- /dev/null +++ b/docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml @@ -0,0 +1,101 @@ +--- +# Software Unit Requirements for the GitHubGraphQLClient Class and DescriptionBody +# +# GitHubGraphQLClient handles paginated HTTPS communication with the GitHub +# GraphQL endpoint. DescriptionBody requirements specify that pull request and +# issue description bodies are retrieved and made available for item controls +# parsing. + +sections: + - title: GitHubGraphQLClient Unit Requirements + requirements: + - id: BuildMark-GitHub-GraphQLClient + title: >- + The GitHubGraphQLClient shall retrieve commits, releases, tags, pull requests, + and issues from the GitHub repository, including support for paginated responses. + justification: | + The GitHub API returns large result sets in pages. The client must transparently + follow pagination cursors to retrieve all commits, releases, tags, pull requests, + issues, and linked issue IDs so that build information reflects the complete + repository history rather than only the first page of results. + tests: + - GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas + - GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits + - GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsReleaseTagNames + - GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAllReleases + - GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes + - GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllTags + - GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests + - GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_ReturnsAllPullRequests + - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssues + - GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAllIssues + - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds + - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues + children: + - BuildMark-GitHub-GetCommits + - BuildMark-GitHub-GetReleases + - BuildMark-GitHub-GetTags + - BuildMark-GitHub-GetPullRequests + - BuildMark-GitHub-GetIssues + + - id: BuildMark-GitHub-DescriptionBody + title: >- + The GitHubRepoConnector shall make the full description text of pull + requests and issues available for item controls parsing. + justification: | + ItemControlsParser requires the complete description text of each pull request + and issue to locate and extract a buildmark code block. The description text + must be retrieved as part of the repository data query so that item controls + can be applied to each item. + tests: + - GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody + - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody + + - id: BuildMark-GitHub-GetCommits + title: The GitHubGraphQLClient shall retrieve commits from the GitHub repository with pagination support. + justification: | + Commit retrieval is required to determine the set of changes between version tags. + Pagination ensures complete history is retrieved regardless of repository size. + tests: + - GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas + - GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits + + - id: BuildMark-GitHub-GetReleases + title: The GitHubGraphQLClient shall retrieve releases from the GitHub repository with pagination support. + justification: | + Release retrieval provides known-issues data based on published releases. + Pagination ensures all releases are fetched for complete known-issue analysis. + tests: + - GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsReleaseTagNames + - GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAllReleases + + - id: BuildMark-GitHub-GetTags + title: The GitHubGraphQLClient shall retrieve all tags from the GitHub repository with pagination support. + justification: | + Tag retrieval is required to identify version boundaries for build note generation. + All tags must be fetched to correctly determine previous version baselines. + tests: + - GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes + - GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllTags + + - id: BuildMark-GitHub-GetPullRequests + title: The GitHubGraphQLClient shall retrieve pull requests from the GitHub repository with pagination support. + justification: | + Pull request retrieval provides the change entries for build notes. Pagination + ensures all pull requests in the version range are included. + tests: + - GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests + - GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_ReturnsAllPullRequests + - GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody + + - id: BuildMark-GitHub-GetIssues + title: The GitHubGraphQLClient shall retrieve issues from the GitHub repository with pagination support. + justification: | + Issue retrieval provides bug fix entries and known-issue data. Pagination + ensures all issues associated with the version range are included. + tests: + - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssues + - GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAllIssues + - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody + - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds + - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues diff --git a/docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml b/docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml index 25d72e9c..4057bef5 100644 --- a/docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml +++ b/docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml @@ -4,6 +4,9 @@ # The GitHubRepoConnector class connects to GitHub using the GraphQL API to # retrieve commits, releases, tags, pull requests, and issues, assembling them # into structured BuildInformation for report generation. +# +# NOTE: GraphQL client and description body requirements have been extracted to +# github-graphql-client.yaml. sections: - title: GitHubRepoConnector Unit Requirements @@ -65,19 +68,6 @@ sections: - GitHubRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug - GitHubRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature - - id: BuildMark-GitHub-DescriptionBody - title: >- - The GitHubRepoConnector shall make the full description text of pull - requests and issues available for item controls parsing. - justification: | - ItemControlsParser requires the complete description text of each pull request - and issue to locate and extract a buildmark code block. The description text - must be retrieved as part of the repository data query so that item controls - can be applied to each item. - tests: - - GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody - - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody - - id: BuildMark-GitHub-Rules title: >- The GitHubRepoConnector class shall apply configured routing rules and sections @@ -91,26 +81,3 @@ sections: tests: - GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue - GitHubRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections - - - id: BuildMark-GitHub-GraphQLClient - title: >- - The GitHubGraphQLClient shall retrieve commits, releases, tags, pull requests, - and issues from the GitHub repository, including support for paginated responses. - justification: | - The GitHub API returns large result sets in pages. The client must transparently - follow pagination cursors to retrieve all commits, releases, tags, pull requests, - issues, and linked issue IDs so that build information reflects the complete - repository history rather than only the first page of results. - tests: - - GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas - - GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits - - GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsReleaseTagNames - - GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAllReleases - - GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes - - GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllTags - - GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests - - GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_ReturnsAllPullRequests - - GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssues - - GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAllIssues - - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds - - GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues diff --git a/docs/reqstream/build-mark/repo-connectors/github/github.yaml b/docs/reqstream/build-mark/repo-connectors/github/github.yaml new file mode 100644 index 00000000..9ca3ca15 --- /dev/null +++ b/docs/reqstream/build-mark/repo-connectors/github/github.yaml @@ -0,0 +1,37 @@ +--- +# GitHub Sub-Subsystem Requirements +# +# PURPOSE: +# - Define requirements for the BuildMark GitHub sub-subsystem +# - The GitHub sub-subsystem provides the production connector used when +# the repository host is GitHub or GitHub Enterprise +# - Sub-subsystem requirements describe the externally visible contract +# satisfied by the units beneath this sub-subsystem + +sections: + - title: GitHub Sub-Subsystem Requirements + requirements: + - id: BuildMark-GitHub-SubSystem + title: >- + The GitHub sub-subsystem shall provide a repository connector that implements + the IRepoConnector interface, retrieves build information from GitHub, supports + item controls, description body retrieval, and configurable routing rules. + justification: | + Generating build notes from GitHub repositories requires a dedicated sub-subsystem + that can authenticate to the GitHub API, retrieve commits, releases, tags, pull + requests, and issues, apply item-controls overrides, and assemble them into + structured build information. + tests: + - GitHub_ImplementsInterface_ReturnsTrue + - GitHub_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + - GitHub_GetBuildInformation_WithMultipleVersions_SelectsCorrectBaseline + - GitHub_GetBuildInformation_WithPullRequests_GathersChanges + - GitHub_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues + - GitHub_GetBuildInformation_ReleaseVersion_SkipsPreReleases + children: + - BuildMark-GitHub-ConnectorConfig + - BuildMark-GitHub-BuildInformation + - BuildMark-GitHub-ItemControls + - BuildMark-GitHub-DescriptionBody + - BuildMark-GitHub-GraphQLClient + - BuildMark-GitHub-Rules diff --git a/docs/reqstream/build-mark/repo-connectors/mock/mock.yaml b/docs/reqstream/build-mark/repo-connectors/mock/mock.yaml new file mode 100644 index 00000000..14d99e20 --- /dev/null +++ b/docs/reqstream/build-mark/repo-connectors/mock/mock.yaml @@ -0,0 +1,28 @@ +--- +# Mock Sub-Subsystem Requirements +# +# PURPOSE: +# - Define requirements for the BuildMark Mock sub-subsystem +# - The Mock sub-subsystem provides a deterministic in-memory connector used +# by the built-in --validate self-test and development unit tests +# - Sub-subsystem requirements describe the externally visible contract +# satisfied by the units beneath this sub-subsystem + +sections: + - title: Mock Sub-Subsystem Requirements + requirements: + - id: BuildMark-Mock-SubSystem + title: >- + The Mock sub-subsystem shall provide a deterministic repository connector + for self-test and development. + justification: | + A deterministic mock connector enables the built-in --validate self-test and + unit testing of build information processing logic without requiring an actual + repository connection, ensuring tests are fast, repeatable, and independent + of external services. + tests: + - Mock_ImplementsInterface_ReturnsTrue + - Mock_GetBuildInformation_ReturnsExpectedVersion + - Mock_GetBuildInformation_ReturnsCompleteInformation + children: + - BuildMark-MockRepoConnector-Deterministic diff --git a/docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml b/docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml index 9d13f922..5d847669 100644 --- a/docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml +++ b/docs/reqstream/build-mark/repo-connectors/repo-connectors.yaml @@ -99,12 +99,7 @@ sections: - RepoConnectors_GitHubConnector_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues - RepoConnectors_GitHubConnector_GetBuildInformation_ReleaseVersion_SkipsPreReleases children: - - BuildMark-GitHub-ConnectorConfig - - BuildMark-GitHub-BuildInformation - - BuildMark-GitHub-ItemControls - - BuildMark-GitHub-DescriptionBody - - BuildMark-GitHub-GraphQLClient - - BuildMark-GitHub-Rules + - BuildMark-GitHub-SubSystem - id: BuildMark-RepoConnectors-Mock title: >- @@ -121,7 +116,7 @@ sections: - RepoConnectors_MockConnector_GetBuildInformation_ReturnsExpectedVersion - RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation children: - - BuildMark-MockRepoConnector-Deterministic + - BuildMark-Mock-SubSystem - id: BuildMark-RepoConnectors-AzureDevOps title: >- @@ -138,11 +133,4 @@ sections: - RepoConnectors_AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues - RepoConnectors_AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases children: - - BuildMark-AzureDevOps-UrlParsing - - BuildMark-AzureDevOps-ConnectorConfig - - BuildMark-AzureDevOps-BuildInformation - - BuildMark-AzureDevOps-ItemControls - - BuildMark-AzureDevOps-CustomFields - - BuildMark-AzureDevOps-Rules - - BuildMark-AzureDevOps-RestClient - - BuildMark-AzureDevOps-WorkItemMapper + - BuildMark-AzureDevOps-SubSystem diff --git a/docs/reqstream/build-mark/version/version.yaml b/docs/reqstream/build-mark/version/version.yaml index 64d9fcb3..88aee7a3 100644 --- a/docs/reqstream/build-mark/version/version.yaml +++ b/docs/reqstream/build-mark/version/version.yaml @@ -39,7 +39,8 @@ sections: - id: BuildMark-Version-SemanticVersioning title: >- Version subsystem shall comply with Semantic Versioning 2.0.0 specification - for all version processing operations. + for all version processing operations, except that text pre-release identifiers + are compared case-insensitively as documented in BuildMark-VersionComparable-PreReleaseComparison. justification: | Compliance with SemVer 2.0.0 ensures consistent and predictable version comparison behavior that aligns with industry standards and developer diff --git a/docs/reqstream/ots/mstest.yaml b/docs/reqstream/ots/xunit.yaml similarity index 54% rename from docs/reqstream/ots/mstest.yaml rename to docs/reqstream/ots/xunit.yaml index c3b03ab8..f937ce67 100644 --- a/docs/reqstream/ots/mstest.yaml +++ b/docs/reqstream/ots/xunit.yaml @@ -1,19 +1,19 @@ --- -# 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: BuildMark-OTS-MSTest - title: MSTest shall execute unit tests and report results. + - id: BuildMark-OTS-xUnit + title: xUnit shall execute unit tests and report results. justification: | - MSTest (MSTest.TestFramework and MSTest.TestAdapter) 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 + xUnit v3 (xunit.v3) 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. tags: [ots] tests: diff --git a/docs/requirements_doc/definition.yaml b/docs/requirements_doc/definition.yaml index 0f4ccd22..628b7897 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 918a6454..9ee62a44 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/advanced-topics.md b/docs/user_guide/advanced-topics.md index 465657ee..55ce2821 100644 --- a/docs/user_guide/advanced-topics.md +++ b/docs/user_guide/advanced-topics.md @@ -17,7 +17,7 @@ Integrate BuildMark into your CI/CD pipeline to automatically generate build not --report docs/build-notes.md - name: Upload Build Notes - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v4 with: name: build-notes path: docs/build-notes.md @@ -142,7 +142,8 @@ changes from tools like Dependabot and Renovate are grouped separately: ## Known Issues -When `--include-known-issues` is specified, lists currently open bugs: +When `--include-known-issues` is specified, lists open bugs and any bugs (open or closed) +whose `affected-versions` field includes the current build version: ```markdown ## Known Issues diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index f0b6346e..0c50e202 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -172,7 +172,7 @@ Each rule may contain: | `label` | A label name or list of label names. Matches if the item carries any of the listed labels. | | `work-item-type` | A work-item type name or list of names (e.g., `Bug`, `Task`, `Epic`). | -Multiple criteria within a single `match` block are combined with AND logic — the item must satisfy +Multiple criteria within a single `match` block are combined with AND logic - the item must satisfy all specified criteria to match that rule. A rule with no `match` key is a **catch-all** and matches every item that has not already been diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 6d6cd584..3eb7e274 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -6,17 +6,33 @@ BuildMark is a .NET command-line tool that generates comprehensive markdown buil history and issue-tracking systems. It analyzes commits, pull requests, and issues to create human-readable build notes, making it easy to integrate release documentation into your CI/CD pipelines and documentation workflows. +## Purpose + +The purpose of this guide is to explain how to install, configure, and operate BuildMark to +generate markdown build notes from Git repository history and connected issue-tracking systems. + +## Scope + +This guide covers installation, basic usage, GitHub and Azure DevOps integration, configuration, +and common use cases for BuildMark. It is intended for developers and CI/CD engineers who want to +automate build notes generation as part of their release workflow. + +The following topics are out of scope: + +- Internal implementation details +- Contributing to BuildMark development + ## Key Features -- **Git Integration** — Analyze Git repository history, tags, and branches -- **Markdown Reports** — Generate structured build notes from repository data -- **Issue Tracking** — Extract changes and bug fixes from GitHub and Azure DevOps -- **Configurable Routing** — Route items into custom report sections by label or work-item type -- **Customizable Output** — Configure report depth, sections, and known-issue inclusion -- **CI/CD Ready** — Automate build notes generation in GitHub Actions and Azure Pipelines -- **Multi-Platform** — Windows, Linux, and macOS with .NET 8, 9, and 10 -- **Self-Validation** — Built-in qualification tests without external tooling -- **Dependency Updates** — Built-in tracking of dependency changes from Dependabot and Renovate +- **Git Integration** - Analyze Git repository history, tags, and branches +- **Markdown Reports** - Generate structured build notes from repository data +- **Issue Tracking** - Extract changes and bug fixes from GitHub and Azure DevOps +- **Configurable Routing** - Route items into custom report sections by label or work-item type +- **Customizable Output** - Configure report depth, sections, and known-issue inclusion +- **CI/CD Ready** - Automate build notes generation in GitHub Actions and Azure Pipelines +- **Multi-Platform** - Windows, Linux, and macOS with .NET 8, 9, and 10 +- **Self-Validation** - Built-in qualification tests without external tooling +- **Dependency Updates** - Built-in tracking of dependency changes from Dependabot and Renovate # Continuous Compliance diff --git a/docs/user_guide/item-controls.md b/docs/user_guide/item-controls.md index 3d67bb95..157f27a5 100644 --- a/docs/user_guide/item-controls.md +++ b/docs/user_guide/item-controls.md @@ -72,7 +72,11 @@ into the report. | `bug` | **Bugs Fixed** section | | `feature` | **Changes** section | -When `type` is absent, BuildMark infers the type from the GitHub issue or PR labels. +The `type` field sets the item classification. Routing rules (configured in `.buildmark.yaml`) govern which +report section each classified item is placed in. The section names in the table above reflect the default +configuration only. + +When `type` is absent, BuildMark infers the type from the GitHub issue or PR labels or from the Azure DevOps work-item type. ### Type Example @@ -99,8 +103,8 @@ affected-versions: (,1.0.1],[1.1.0,1.2.0),(1.2.5,2.0.0],[3.0.0,) | `(` | Exclusive lower bound | | `]` | Inclusive upper bound | | `)` | Exclusive upper bound | -| _(empty)_ lower bound | No minimum — all versions from the beginning | -| _(empty)_ upper bound | No maximum — all versions from the lower bound onward | +| _(empty)_ lower bound | No minimum - all versions from the beginning | +| _(empty)_ upper bound | No maximum - all versions from the lower bound onward | ### Affected Version Examples @@ -122,7 +126,27 @@ affected-versions: (,1.0.1],[1.1.0,1.2.0) This matches all versions up to and including `1.0.1`, and also versions from `1.1.0` up to (but not including) `1.2.0`. -## Azure DevOps Custom Fields +### Known-Issue Inclusion Rules + +When `--include-known-issues` is used to generate a report, BuildMark applies the following +four rules to determine whether each bug qualifies as a known issue for the current build +version: + +1. A **closed** bug with **no declared `affected-versions`** is **not** a known issue. +2. An **open** bug with **no declared `affected-versions`** **is** a known issue. +3. A bug in **any state** (open or closed) whose `affected-versions` **contains** the build + version **is** a known issue. +4. A bug in **any state** (open or closed) whose `affected-versions` **does not contain** the + build version is **not** a known issue. + +Rules 3 and 4 take precedence over the open/closed status whenever `affected-versions` is +declared. This matters for LTS branches: a bug may be closed in a later release but still +affects an older LTS branch. Setting `affected-versions` on that bug ensures it is correctly +reported as a known issue in LTS build notes even after it has been closed. + +**Example:** A bug is fixed in v2.0.0 but affects all v1.x releases. Setting +`affected-versions: (,2.0.0)` ensures it appears in build notes for any v1.x version +regardless of its open/closed state. Azure DevOps work items support the same visibility and version controls through native custom fields, as an alternative to embedding `buildmark` code blocks in descriptions. When both a custom diff --git a/docs/verification/build-mark/build-mark.md b/docs/verification/build-mark/build-mark.md new file mode 100644 index 00000000..98aceae3 --- /dev/null +++ b/docs/verification/build-mark/build-mark.md @@ -0,0 +1,118 @@ +# BuildMark + +## Verification Approach + +BuildMark is verified at the system level through a set of integration and end-to-end +tests that exercise the full pipeline from CLI invocation to build notes generation. +The `ProgramTests.cs` file exercises the entry point with all supported flags and +validates both exit codes and console output. The `RepoConnectorsTests.cs` file +exercises the full data pipeline, from connector factory creation through +`GetBuildInformationAsync`, using mock data to cover GitHub, Azure DevOps, and Mock +connector paths. + +Self-test (`--validate`) is covered by `Program_Run_ValidateFlag_OutputsValidationMessage` +and the self-test suite in `ValidationTests.cs`. The CI pipeline additionally runs +the full build notes generation chain with live GitHub metadata to confirm end-to-end +operation. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ------------------------------------------------------------- | +| `MockRepoConnector` | Provides deterministic repository data without live API calls | +| `MockHttpMessageHandler` | Used by GraphQL/REST client unit tests | +| Context output capture | Replaces `Console.Out` with `StringWriter` for assertion | + +## Test Scenarios (System-Level) + +### Program_Version_ReturnsValidVersion + +**Scenario**: `Program.Version` property is accessed. + +**Expected**: Returns a non-null, non-empty version string in semver format. + +**Requirement coverage**: `BuildMark-Program-Version` + +### Program_Run_VersionFlag_OutputsVersionToConsole + +**Scenario**: `Program.Run` is called with a context having `Version = true`. + +**Expected**: Version string is written to context output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Version` + +### Program_Run_HelpFlag_OutputsHelpMessage + +**Scenario**: `Program.Run` is called with a context having `Help = true`. + +**Expected**: Help text is written to context output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Program_Run_QuestionMarkFlag_OutputsHelpMessage + +**Scenario**: `Program.Run` is called with `?` flag. + +**Expected**: Help text is written to context output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Program_Run_LongHelpFlag_OutputsHelpMessage + +**Scenario**: `Program.Run` is called with `--help` flag. + +**Expected**: Help text is written to context output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Program_Run_ValidateFlag_OutputsValidationMessage + +**Scenario**: `Program.Run` is called with `Validate = true`. + +**Expected**: Validation output is written; self-test completes; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Validate` + +### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + +**Scenario**: `Program.Run` is called with report and include-known-issues flags set. + +**Expected**: Build notes report is generated including known issues section; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Report` + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `Program.Run` is called with `Lint = true` but no configuration file present. + +**Expected**: Exit code remains 0 (lint with no config is not an error). + +**Requirement coverage**: `BuildMark-Program-Lint` + +### Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode + +**Scenario**: `Program.Run` is called with an invalid build version string. + +**Expected**: Error message is written to stderr; exit code is 1. + +**Requirement coverage**: `BuildMark-Program-ErrorHandling` + +### Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode + +**Scenario**: `Program.Run` is called but the connector factory throws `InvalidOperationException`. + +**Expected**: Error message is written to stderr; exit code is 1. + +**Requirement coverage**: `BuildMark-Program-ErrorHandling` + +## Requirements Coverage + +- **BuildMark-Program-Version**: Program_Version_ReturnsValidVersion, + Program_Run_VersionFlag_OutputsVersionToConsole +- **BuildMark-Program-Help**: Program_Run_HelpFlag_OutputsHelpMessage, + Program_Run_QuestionMarkFlag_OutputsHelpMessage, Program_Run_LongHelpFlag_OutputsHelpMessage +- **BuildMark-Program-Validate**: Program_Run_ValidateFlag_OutputsValidationMessage +- **BuildMark-Program-Report**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues +- **BuildMark-Program-Lint**: Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero +- **BuildMark-Program-ErrorHandling**: Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode, + Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode diff --git a/docs/verification/build-mark/build-notes/build-information.md b/docs/verification/build-mark/build-notes/build-information.md new file mode 100644 index 00000000..eef2cc9c --- /dev/null +++ b/docs/verification/build-mark/build-notes/build-information.md @@ -0,0 +1,58 @@ +# BuildInformation + +## Verification Approach + +`BuildInformation` is a data model with no dedicated test class. It is verified +indirectly through integration tests in `ProgramTests.cs` and `RepoConnectorsTests.cs` +that exercise the full pipeline and assert on the structure and content of +`BuildInformation` instances returned by connectors. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------- | -------------------------------------------------- | +| `MockRepoConnector` | Returns deterministic `BuildInformation` instances | + +## Test Scenarios (Integration) + +### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + +**Scenario**: `Program.Run` generates a report using a mock connector. + +**Expected**: `BuildInformation.ToMarkdown` produces valid markdown output including +known issues section. + +**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation` + +### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation + +**Scenario**: Mock connector's `GetBuildInformationAsync` returns a complete result. + +**Expected**: `BuildInformation` instance contains version, baseline, changes, +known issues, and routed sections. + +**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: GitHub connector returns `BuildInformation` from mocked API responses. + +**Expected**: `BuildInformation` fields are correctly populated from the mock data. + +**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation` + +### RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: Azure DevOps connector returns `BuildInformation` from mocked API responses. + +**Expected**: `BuildInformation` fields are correctly populated from the mock data. + +**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation` + +## Requirements Coverage + +- **BuildMark-BuildNotes-BuildInformation**: + Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues, + RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation, + RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation diff --git a/docs/verification/build-mark/build-notes/build-notes.md b/docs/verification/build-mark/build-notes/build-notes.md new file mode 100644 index 00000000..7f913094 --- /dev/null +++ b/docs/verification/build-mark/build-notes/build-notes.md @@ -0,0 +1,52 @@ +# BuildNotes + +## Verification Approach + +The BuildNotes subsystem is verified at the integration level through `ProgramTests.cs` +and `RepoConnectorsTests.cs`. There is no dedicated `BuildNotesTests.cs` file; the +subsystem is exercised indirectly whenever `Program.Run` generates a report or when +a connector's `GetBuildInformationAsync` is exercised with mock data. + +`BuildInformation.ToMarkdown` is exercised by report generation tests. +`ItemInfo` and `WebLink` data models are exercised through the connector tests that +populate `BuildInformation` instances with change items and web links. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------- | --------------------------------------------------------- | +| `MockRepoConnector` | Returns deterministic `BuildInformation` with known items | +| `Context` | Provides output capture for markdown assertion | + +## Test Scenarios (Integration) + +The following integration tests in `ProgramTests.cs` exercise the BuildNotes subsystem: + +### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + +**Scenario**: A full pipeline run generates a report with known issues included. + +**Expected**: `BuildInformation` is populated; markdown report is written to file. + +**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`, +`BuildMark-BuildNotes-ItemInfo` + +The following tests in `RepoConnectorsTests.cs` exercise the subsystem through +connector output: + +### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation + +**Scenario**: Mock connector returns a fully populated `BuildInformation` instance. + +**Expected**: All fields including changes, known issues, and web links are present. + +**Requirement coverage**: `BuildMark-BuildNotes-BuildInformation`, +`BuildMark-BuildNotes-ItemInfo`, `BuildMark-BuildNotes-WebLink` + +## Requirements Coverage + +- **BuildMark-BuildNotes-BuildInformation**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues, + RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation +- **BuildMark-BuildNotes-ItemInfo**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues, + RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation +- **BuildMark-BuildNotes-WebLink**: RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation diff --git a/docs/verification/build-mark/build-notes/item-info.md b/docs/verification/build-mark/build-notes/item-info.md new file mode 100644 index 00000000..d45ea3dd --- /dev/null +++ b/docs/verification/build-mark/build-notes/item-info.md @@ -0,0 +1,48 @@ +# ItemInfo + +## Verification Approach + +`ItemInfo` is a data model with no dedicated test class. It is verified indirectly +through connector tests in `RepoConnectorsTests.cs`, `GitHubRepoConnectorTests.cs`, +`AzureDevOpsRepoConnectorTests.cs`, and `MockRepoConnectorTests.cs` that assert on +the items contained in `BuildInformation` instances returned by connectors. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------- | -------------------------------------------------------- | +| `MockRepoConnector` | Returns `BuildInformation` with known `ItemInfo` entries | + +## Test Scenarios (Integration) + +### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation + +**Scenario**: Mock connector returns complete build information with change items. + +**Expected**: `BuildInformation.Changes` contains `ItemInfo` entries with correct +title, type, visibility, and web link. + +**Requirement coverage**: `BuildMark-BuildNotes-ItemInfo` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithPullRequests_GathersChanges + +**Scenario**: GitHub connector processes pull requests into change items. + +**Expected**: Each pull request is represented as an `ItemInfo` in `BuildInformation.Changes`. + +**Requirement coverage**: `BuildMark-BuildNotes-ItemInfo` + +### RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges + +**Scenario**: Azure DevOps connector processes pull requests into change items. + +**Expected**: Each pull request is represented as an `ItemInfo` in `BuildInformation.Changes`. + +**Requirement coverage**: `BuildMark-BuildNotes-ItemInfo` + +## Requirements Coverage + +- **BuildMark-BuildNotes-ItemInfo**: + RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation, + RepoConnectors_GitHubConnector_GetBuildInformation_WithPullRequests_GathersChanges, + RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges diff --git a/docs/verification/build-mark/build-notes/web-link.md b/docs/verification/build-mark/build-notes/web-link.md new file mode 100644 index 00000000..08566035 --- /dev/null +++ b/docs/verification/build-mark/build-notes/web-link.md @@ -0,0 +1,47 @@ +# WebLink + +## Verification Approach + +`WebLink` is a utility data model with no dedicated test class. It is verified +indirectly through connector tests that assert on web link properties within +`ItemInfo` and `BuildInformation` instances. The `WebLink` type is used to attach +URLs to items, releases, and changelog links. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------- | ---------------------------------------------------------- | +| `MockRepoConnector` | Returns `BuildInformation` with `WebLink` entries on items | + +## Test Scenarios (Integration) + +### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation + +**Scenario**: Mock connector returns complete build information including items with web links. + +**Expected**: `ItemInfo` entries contain `WebLink` instances with non-null URL and label. + +**Requirement coverage**: `BuildMark-BuildNotes-WebLink` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: GitHub connector populates change items with links to GitHub pull request URLs. + +**Expected**: `ItemInfo.Link` is a `WebLink` with a valid GitHub URL. + +**Requirement coverage**: `BuildMark-BuildNotes-WebLink` + +### RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: Azure DevOps connector populates change items with links to Azure DevOps URLs. + +**Expected**: `ItemInfo.Link` is a `WebLink` with a valid Azure DevOps URL. + +**Requirement coverage**: `BuildMark-BuildNotes-WebLink` + +## Requirements Coverage + +- **BuildMark-BuildNotes-WebLink**: + RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation, + RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation diff --git a/docs/verification/build-mark/cli/cli.md b/docs/verification/build-mark/cli/cli.md new file mode 100644 index 00000000..d69f9de4 --- /dev/null +++ b/docs/verification/build-mark/cli/cli.md @@ -0,0 +1,189 @@ +# Cli + +## Verification Approach + +The Cli subsystem is verified through `CliTests.cs`, which exercises the `Context` +class directly by constructing instances with various argument combinations and +asserting on the resulting property values. Each test targets a specific flag or +argument combination and validates correct parsing behavior, including error conditions +and output behavior. + +## Dependencies + +| Mock / Stub | Reason | +| -------------------- | ------------------------------------------------------------------ | +| `StringWriter` | Captures context output for assertion without console side effects | +| In-process arguments | Passed directly to `Context` constructor instead of `args[]` | + +## Test Scenarios + +### Cli_Context_EmptyArguments_CreatesValidContext + +**Scenario**: A `Context` is created with no arguments. + +**Expected**: Context is created without error; all flags default to false. + +**Requirement coverage**: `BuildMark-Cli-Context` + +### Cli_VersionFlag_SetsProperty + +**Scenario**: Context is created with `--version` argument. + +**Expected**: `Version` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Version` + +### Cli_HelpFlag_SetsProperty + +**Scenario**: Context is created with `--help` argument. + +**Expected**: `Help` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Cli_SilentFlag_SetsProperty + +**Scenario**: Context is created with `--silent` argument. + +**Expected**: `Silent` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Silent` + +### Cli_SilentFlag_SuppressesConsoleOutput + +**Scenario**: Context is created with `--silent` argument and a write is performed. + +**Expected**: Output is suppressed; nothing is written to console. + +**Requirement coverage**: `BuildMark-Program-Silent` + +### Cli_BuildVersionFlag_SetsProperty + +**Scenario**: Context is created with `--build-version 1.2.3` argument. + +**Expected**: `BuildVersion` property equals `"1.2.3"`. + +**Requirement coverage**: `BuildMark-Program-BuildVersion` + +### Cli_ReportFlags_SetProperties + +**Scenario**: Context is created with `--report path.md` and `--report-depth 2` arguments. + +**Expected**: `ReportFile` and `Depth` properties are set to the provided values. + +**Requirement coverage**: `BuildMark-Program-Report`, `BuildMark-Program-Depth` + +### Cli_LogFlag_CreatesLogFile + +**Scenario**: Context is created with `--log path.log` argument. + +**Expected**: `Log` property is set; log file is created at the specified path. + +**Requirement coverage**: `BuildMark-Program-Log` + +### Cli_ValidateFlag_SetsProperty + +**Scenario**: Context is created with `--validate` argument. + +**Expected**: `Validate` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Validate` + +### Cli_ResultsFlag_SetsProperty + +**Scenario**: Context is created with `--results path.trx` argument. + +**Expected**: `Results` property equals the provided path. + +**Requirement coverage**: `BuildMark-Program-Results` + +### Cli_ResultFlag_SetsProperty + +**Scenario**: Context is created with `--result path.trx` argument (alias). + +**Expected**: `Results` property equals the provided path. + +**Requirement coverage**: `BuildMark-Program-Results` + +### Cli_ErrorOutput_WritesToStderr + +**Scenario**: `context.WriteError` is called with an error message. + +**Expected**: Message is written to the error stream; exit code is set. + +**Requirement coverage**: `BuildMark-Program-ErrorHandling` + +### Cli_InvalidArgument_ThrowsException + +**Scenario**: Context is created with an unrecognized flag. + +**Expected**: `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Cli-Context` + +### Cli_MissingArgumentValue_ThrowsException + +**Scenario**: Context is created with a flag that requires a value but no value is provided. + +**Expected**: `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Cli-Context` + +### Cli_ExitCode_DefaultsToZero + +**Scenario**: Context is created; `ExitCode` property is read without any errors. + +**Expected**: `ExitCode` is 0. + +**Requirement coverage**: `BuildMark-Cli-Context` + +### Cli_WriteError_SetsExitCodeToOne + +**Scenario**: `context.WriteError` is called. + +**Expected**: `ExitCode` is 1 after the call. + +**Requirement coverage**: `BuildMark-Program-ErrorHandling` + +### Cli_VersionShortFlag_SetsProperty + +**Scenario**: Context is created with `-v` short argument. + +**Expected**: `Version` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Version` + +### Cli_HelpShortFlags_SetProperty + +**Scenario**: Context is created with `-h` or `-?` short arguments. + +**Expected**: `Help` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Cli_LintFlag_SetsProperty + +**Scenario**: Context is created with `--lint` argument. + +**Expected**: `Lint` property is `true`. + +**Requirement coverage**: `BuildMark-Program-Lint` + +## Requirements Coverage + +- **BuildMark-Cli-Context**: Cli_Context_EmptyArguments_CreatesValidContext, + Cli_InvalidArgument_ThrowsException, Cli_MissingArgumentValue_ThrowsException, + Cli_ExitCode_DefaultsToZero +- **BuildMark-Program-Version**: Cli_VersionFlag_SetsProperty, Cli_VersionShortFlag_SetsProperty +- **BuildMark-Program-Help**: Cli_HelpFlag_SetsProperty, Cli_HelpShortFlags_SetProperty +- **BuildMark-Program-Silent**: Cli_SilentFlag_SetsProperty, + Cli_SilentFlag_SuppressesConsoleOutput +- **BuildMark-Program-BuildVersion**: Cli_BuildVersionFlag_SetsProperty +- **BuildMark-Program-Report**: Cli_ReportFlags_SetProperties +- **BuildMark-Program-Depth**: Cli_ReportFlags_SetProperties +- **BuildMark-Program-Log**: Cli_LogFlag_CreatesLogFile +- **BuildMark-Program-Validate**: Cli_ValidateFlag_SetsProperty +- **BuildMark-Program-Results**: Cli_ResultsFlag_SetsProperty, Cli_ResultFlag_SetsProperty +- **BuildMark-Program-Lint**: Cli_LintFlag_SetsProperty +- **BuildMark-Program-ErrorHandling**: Cli_ErrorOutput_WritesToStderr, + Cli_WriteError_SetsExitCodeToOne diff --git a/docs/verification/build-mark/cli/context.md b/docs/verification/build-mark/cli/context.md new file mode 100644 index 00000000..8c0305bb --- /dev/null +++ b/docs/verification/build-mark/cli/context.md @@ -0,0 +1,385 @@ +# Context Verification + +This document describes the unit-level verification design for the `Context` unit. It defines the +test scenarios, dependency usage, and requirement coverage for `Cli/Context.cs`. + +## Verification Approach + +`Context` is verified with unit tests defined in `ContextTests.cs`. Because `Context` depends only +on .NET base class library types (`Console`, `StreamWriter`, `Path`), no mocking or test doubles +are required. Tests call `Context.Create` with controlled argument arrays, inspect the resulting +properties and exit codes, and verify output written to captured streams. + +## Dependencies + +`Context` has no dependencies on other tool units. All dependencies are real .NET BCL types; +no mocking is needed at this level. + +## Test Scenarios + +### Context_Create_EmptyArguments_CreatesValidContext + +**Scenario**: `Context.Create` is called with an empty argument array. + +**Expected**: All boolean flags are false; `ResultsFile` is null; `HeadingDepth` is 1; +exit code is 0. + +**Requirement coverage**: `BuildMark-Context-DefaultConstruction`. + +### Context_Create_ShortVersionFlag_SetsVersionProperty + +**Scenario**: `Context.Create` is called with `["-v"]`. + +**Expected**: `Version` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_LongVersionFlag_SetsVersionProperty + +**Scenario**: `Context.Create` is called with `["--version"]`. + +**Expected**: `Version` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_QuestionMarkHelpFlag_SetsHelpProperty + +**Scenario**: `Context.Create` is called with `["-?"]`. + +**Expected**: `Help` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_ShortHelpFlag_SetsHelpProperty + +**Scenario**: `Context.Create` is called with `["-h"]`. + +**Expected**: `Help` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_LongHelpFlag_SetsHelpProperty + +**Scenario**: `Context.Create` is called with `["--help"]`. + +**Expected**: `Help` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_SilentFlag_SetsSilentProperty + +**Scenario**: `Context.Create` is called with `["--silent"]`. + +**Expected**: `Silent` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_ValidateFlag_SetsValidateProperty + +**Scenario**: `Context.Create` is called with `["--validate"]`. + +**Expected**: `Validate` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_LintFlag_SetsLintProperty + +**Scenario**: `Context.Create` is called with `["--lint"]`. + +**Expected**: `Lint` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_BuildVersionArgument_SetsBuildVersionProperty + +**Scenario**: `Context.Create` is called with `["--build-version", "1.2.3"]`. + +**Expected**: `BuildVersion` property equals `"1.2.3"`. + +**Requirement coverage**: `BuildMark-Context-ArgumentParsing`. + +### Context_Create_ReportArgument_SetsReportFileProperty + +**Scenario**: `Context.Create` is called with `["--report", "output.md"]`. + +**Expected**: `ReportFile` property equals `"output.md"`. + +**Requirement coverage**: `BuildMark-Context-ArgumentParsing`. + +### Context_Create_DepthArgument_SetsDepthProperty + +**Scenario**: `Context.Create` is called with `["--depth", "3"]`. + +**Expected**: `HeadingDepth` property equals 3. + +**Requirement coverage**: `BuildMark-Context-ArgumentParsing`. + +### Context_Create_LegacyReportDepthArgument_SetsDepthProperty + +**Scenario**: `Context.Create` is called with the legacy depth argument form. + +**Expected**: `HeadingDepth` property is set to the specified value. + +**Requirement coverage**: `BuildMark-Context-ArgumentParsing`. + +### Context_Create_IncludeKnownIssuesFlag_SetsIncludeKnownIssuesProperty + +**Scenario**: `Context.Create` is called with `["--include-known-issues"]`. + +**Expected**: `IncludeKnownIssues` property is true. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`. + +### Context_Create_ResultsArgument_SetsResultsFileProperty + +**Scenario**: `Context.Create` is called with `["--results", "output.trx"]`. + +**Expected**: `ResultsFile` property equals `"output.trx"`. + +**Requirement coverage**: `BuildMark-Context-ArgumentParsing`. + +### Context_Create_ResultArgument_SetsResultsFileProperty + +**Scenario**: `Context.Create` is called with `["--result", "output.trx"]` (alias). + +**Expected**: `ResultsFile` property equals `"output.trx"`. + +**Requirement coverage**: `BuildMark-Context-ArgumentParsing`. + +### Context_Create_LogArgument_CreatesLogFile + +**Scenario**: `Context.Create` is called with `["--log", ".log"]`; `WriteLine` is then called +with a test message. + +**Expected**: The log file is created; the test message is written to it. + +**Requirement coverage**: `BuildMark-Context-LogFile`. + +### Context_Create_MultipleArguments_SetsAllPropertiesCorrectly + +**Scenario**: `Context.Create` is called with multiple flags together. + +**Expected**: All corresponding properties are set correctly; no exception is thrown. + +**Requirement coverage**: `BuildMark-Context-FlagParsing`, `BuildMark-Context-ArgumentParsing`. + +### Context_Create_UnsupportedArgument_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with an unrecognized argument (e.g., `["--unknown"]`). + +**Expected**: An `ArgumentException` is thrown containing the text "Unsupported argument". + +**Boundary / error path**: Unknown argument rejection. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_BuildVersionWithoutValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--build-version"]` (value missing). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_ReportWithoutValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--report"]` (value missing). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_DepthWithoutValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--depth"]` (value missing). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_DepthWithNonIntegerValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--depth", "abc"]`. + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_DepthWithZeroValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--depth", "0"]` (below minimum of 1). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_DepthWithNegativeValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--depth", "-1"]` (negative value). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_DepthExceedingMaximum_ThrowsArgumentOutOfRangeException + +**Scenario**: `Context.Create` is called with `["--depth", "7"]` (above the maximum of 6). + +**Expected**: An `ArgumentOutOfRangeException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_ResultsWithoutValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--results"]` (value missing). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_ResultWithoutValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--result"]` (value missing). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_LogWithoutValue_ThrowsArgumentException + +**Scenario**: `Context.Create` is called with `["--log"]` (value missing). + +**Expected**: An `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_Create_InvalidLogFilePath_ThrowsInvalidOperationException + +**Scenario**: `Context.Create` is called with `["--log", ""]`. + +**Expected**: An `InvalidOperationException` is thrown when the log file cannot be created. + +**Requirement coverage**: `BuildMark-Context-ErrorHandling`. + +### Context_WriteLine_NotSilent_WritesToConsole + +**Scenario**: A non-silent `Context` is created and `WriteLine` is called with a test message. + +**Expected**: The test message appears on standard output. + +**Requirement coverage**: `BuildMark-Context-Output`. + +### Context_WriteLine_Silent_DoesNotWriteToConsole + +**Scenario**: A silent `Context` (created with `["--silent"]`) calls `WriteLine`. + +**Expected**: Standard output receives nothing. + +**Requirement coverage**: `BuildMark-Context-SilentMode`. + +### Context_WriteLine_WithLogFile_WritesToLogFile + +**Scenario**: A `Context` created with a log file calls `WriteLine` with a test message. + +**Expected**: The test message appears in the log file. + +**Requirement coverage**: `BuildMark-Context-LogFile`. + +### Context_WriteError_NotSilent_WritesToConsole + +**Scenario**: A non-silent `Context` calls `WriteError` with a test message. + +**Expected**: The test message appears on standard error. + +**Requirement coverage**: `BuildMark-Context-Output`. + +### Context_WriteError_Silent_DoesNotWriteToConsole + +**Scenario**: A silent `Context` calls `WriteError`. + +**Expected**: Standard error receives nothing. + +**Requirement coverage**: `BuildMark-Context-SilentMode`. + +### Context_WriteError_WithLogFile_WritesToLogFile + +**Scenario**: A `Context` created with a log file calls `WriteError` with a test message. + +**Expected**: The test message appears in the log file. + +**Requirement coverage**: `BuildMark-Context-LogFile`. + +### Context_WriteError_SetsExitCodeToOne + +**Scenario**: A `Context` calls `WriteError`. + +**Expected**: `ExitCode` is 1 after the call. + +**Requirement coverage**: `BuildMark-Context-ExitCode`. + +### Context_ExitCode_NoErrors_RemainsZero + +**Scenario**: A `Context` is created and no errors are written. + +**Expected**: `ExitCode` remains 0. + +**Requirement coverage**: `BuildMark-Context-ExitCode`. + +### Context_Dispose_ClosesLogFileProperly + +**Scenario**: A `Context` with a log file is disposed. + +**Expected**: The log file is properly closed without error. + +**Requirement coverage**: `BuildMark-Context-LogFile`. + +## Requirements Coverage + +- **`BuildMark-Context-DefaultConstruction`**: + - Context_Create_EmptyArguments_CreatesValidContext +- **`BuildMark-Context-FlagParsing`**: + - Context_Create_ShortVersionFlag_SetsVersionProperty + - Context_Create_LongVersionFlag_SetsVersionProperty + - Context_Create_QuestionMarkHelpFlag_SetsHelpProperty + - Context_Create_ShortHelpFlag_SetsHelpProperty + - Context_Create_LongHelpFlag_SetsHelpProperty + - Context_Create_SilentFlag_SetsSilentProperty + - Context_Create_ValidateFlag_SetsValidateProperty + - Context_Create_LintFlag_SetsLintProperty + - Context_Create_IncludeKnownIssuesFlag_SetsIncludeKnownIssuesProperty + - Context_Create_MultipleArguments_SetsAllPropertiesCorrectly +- **`BuildMark-Context-ArgumentParsing`**: + - Context_Create_BuildVersionArgument_SetsBuildVersionProperty + - Context_Create_ReportArgument_SetsReportFileProperty + - Context_Create_DepthArgument_SetsDepthProperty + - Context_Create_LegacyReportDepthArgument_SetsDepthProperty + - Context_Create_ResultsArgument_SetsResultsFileProperty + - Context_Create_ResultArgument_SetsResultsFileProperty + - Context_Create_MultipleArguments_SetsAllPropertiesCorrectly +- **`BuildMark-Context-LogFile`**: + - Context_Create_LogArgument_CreatesLogFile + - Context_WriteLine_WithLogFile_WritesToLogFile + - Context_WriteError_WithLogFile_WritesToLogFile + - Context_Dispose_ClosesLogFileProperly +- **`BuildMark-Context-ErrorHandling`**: + - Context_Create_UnsupportedArgument_ThrowsArgumentException + - Context_Create_BuildVersionWithoutValue_ThrowsArgumentException + - Context_Create_ReportWithoutValue_ThrowsArgumentException + - Context_Create_DepthWithoutValue_ThrowsArgumentException + - Context_Create_DepthWithNonIntegerValue_ThrowsArgumentException + - Context_Create_DepthWithZeroValue_ThrowsArgumentException + - Context_Create_DepthWithNegativeValue_ThrowsArgumentException + - Context_Create_DepthExceedingMaximum_ThrowsArgumentOutOfRangeException + - Context_Create_ResultsWithoutValue_ThrowsArgumentException + - Context_Create_ResultWithoutValue_ThrowsArgumentException + - Context_Create_LogWithoutValue_ThrowsArgumentException + - Context_Create_InvalidLogFilePath_ThrowsInvalidOperationException +- **`BuildMark-Context-Output`**: + - Context_WriteLine_NotSilent_WritesToConsole + - Context_WriteError_NotSilent_WritesToConsole +- **`BuildMark-Context-SilentMode`**: + - Context_WriteLine_Silent_DoesNotWriteToConsole + - Context_WriteError_Silent_DoesNotWriteToConsole +- **`BuildMark-Context-ExitCode`**: + - Context_WriteError_SetsExitCodeToOne + - Context_ExitCode_NoErrors_RemainsZero diff --git a/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md b/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md new file mode 100644 index 00000000..00ddb8f5 --- /dev/null +++ b/docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md @@ -0,0 +1,31 @@ +# AzureDevOpsConnectorConfig + +## Verification Approach + +`AzureDevOpsConnectorConfig` is a data model class verified indirectly through +`RepoConnectorFactoryTests.cs`. Tests that supply Azure DevOps-specific configuration +within a `ConnectorConfig` confirm that the settings are forwarded to the created +`AzureDevOpsRepoConnector`. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration + +**Scenario**: A `ConnectorConfig` with an `AzureDevOpsConnectorConfig` is passed to +the factory. + +**Expected**: The resulting connector incorporates the Azure DevOps-specific +configuration. + +**Requirement coverage**: `BuildMark-Configuration-AzureDevOpsConnectorConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-AzureDevOpsConnectorConfig**: + RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration diff --git a/docs/verification/build-mark/configuration/build-mark-config-reader.md b/docs/verification/build-mark/configuration/build-mark-config-reader.md new file mode 100644 index 00000000..8ba1b2dd --- /dev/null +++ b/docs/verification/build-mark/configuration/build-mark-config-reader.md @@ -0,0 +1,42 @@ +# BuildMarkConfigReader + +## Verification Approach + +`BuildMarkConfigReader` is verified indirectly through `ProgramTests.cs`. The lint +and report tests exercise `BuildMarkConfigReader.ReadAsync` as part of the program +execution pipeline. When `Lint = true`, `ReadAsync` is called and the result is +reported to the context. When a report is generated, `ReadAsync` is called to load +optional configuration before connector creation. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ------------------------------------------------------- | +| File system | `ReadAsync` attempts to read `.buildmark.yaml` from the | +| | current directory; tests control file presence | + +## Test Scenarios (Integration) + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `Program.Run` calls `BuildMarkConfigReader.ReadAsync` when no +`.buildmark.yaml` is present. + +**Expected**: Reader returns a result with no errors; exit code is 0. + +**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfigReader` + +### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + +**Scenario**: `Program.Run` calls `BuildMarkConfigReader.ReadAsync` before report +generation; configuration may or may not be present. + +**Expected**: Reader returns without error; report generation proceeds. + +**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfigReader` + +## Requirements Coverage + +- **BuildMark-Configuration-BuildMarkConfigReader**: + Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero, + Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues diff --git a/docs/verification/build-mark/configuration/build-mark-config.md b/docs/verification/build-mark/configuration/build-mark-config.md new file mode 100644 index 00000000..359f5509 --- /dev/null +++ b/docs/verification/build-mark/configuration/build-mark-config.md @@ -0,0 +1,41 @@ +# BuildMarkConfig + +## Verification Approach + +`BuildMarkConfig` is a data model class with no dedicated test class. It is verified +indirectly through program-level and connector factory tests that exercise the +configuration pipeline. When `BuildMarkConfigReader.ReadAsync` returns a +`ConfigurationLoadResult`, the embedded `BuildMarkConfig` instance drives connector +creation and report configuration. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------------------------------------------------------- | +| File system | Integration tests may create temporary configuration files | + +## Test Scenarios (Integration) + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `Program.Run` processes a lint request; `BuildMarkConfig` is loaded +as an empty/default instance when no file is present. + +**Expected**: Default `BuildMarkConfig` is handled gracefully; exit code is 0. + +**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfig` + +### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration + +**Scenario**: `BuildMarkConfig.Connector` field is used by the factory to create +a connector with GitHub settings. + +**Expected**: Connector reflects the `BuildMarkConfig` connector settings. + +**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-BuildMarkConfig**: + Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero, + RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration diff --git a/docs/verification/build-mark/configuration/configuration-issue.md b/docs/verification/build-mark/configuration/configuration-issue.md new file mode 100644 index 00000000..045b3992 --- /dev/null +++ b/docs/verification/build-mark/configuration/configuration-issue.md @@ -0,0 +1,31 @@ +# ConfigurationIssue + +## Verification Approach + +`ConfigurationIssue` is a data class with no dedicated test class. It is verified +indirectly through `ProgramTests.cs` via the `ConfigurationLoadResult` pipeline. +When configuration parsing produces issues, they are stored as `ConfigurationIssue` +instances and reported to the context. No dedicated test exercises this path in +isolation; it is covered by integration paths where configuration errors surface. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `ConfigurationLoadResult` contains no `ConfigurationIssue` entries +when no config file is present; `ReportTo` is a no-op. + +**Expected**: No issues reported; exit code is 0. + +**Requirement coverage**: `BuildMark-Configuration-ConfigurationIssue` + +## Requirements Coverage + +- **BuildMark-Configuration-ConfigurationIssue**: + Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero diff --git a/docs/verification/build-mark/configuration/configuration-load-result.md b/docs/verification/build-mark/configuration/configuration-load-result.md new file mode 100644 index 00000000..057ae83c --- /dev/null +++ b/docs/verification/build-mark/configuration/configuration-load-result.md @@ -0,0 +1,31 @@ +# ConfigurationLoadResult + +## Verification Approach + +`ConfigurationLoadResult` is a data class with no dedicated test class. It is verified +indirectly through `ProgramTests.cs`, where the result of `BuildMarkConfigReader.ReadAsync` +is used to report issues and extract the active configuration. Tests that exercise +the lint path confirm that `ConfigurationLoadResult` is handled correctly when no +issues are present. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `ConfigurationLoadResult` is returned from `BuildMarkConfigReader` with +no issues when no configuration file is present. + +**Expected**: `result.ReportTo(context)` completes without errors; exit code is 0. + +**Requirement coverage**: `BuildMark-Configuration-ConfigurationLoadResult` + +## Requirements Coverage + +- **BuildMark-Configuration-ConfigurationLoadResult**: + Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero diff --git a/docs/verification/build-mark/configuration/configuration.md b/docs/verification/build-mark/configuration/configuration.md new file mode 100644 index 00000000..e6a21972 --- /dev/null +++ b/docs/verification/build-mark/configuration/configuration.md @@ -0,0 +1,58 @@ +# Configuration + +## Verification Approach + +The Configuration subsystem is verified at the integration level through +`ProgramTests.cs` and `RepoConnectorFactoryTests.cs`. There is no dedicated +`ConfigurationTests.cs` file. The lint-related tests in `ProgramTests.cs` exercise +`BuildMarkConfigReader` indirectly when `Program.Run` with `Lint = true` calls +`BuildMarkConfigReader.ReadAsync`. The factory tests in `RepoConnectorFactoryTests.cs` +exercise connector configuration forwarding. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | --------------------------------------------------------- | +| File system | Some tests create temporary `.buildmark.yaml` files | +| `Context` | Provides output capture for configuration issue reporting | + +## Test Scenarios (Integration) + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `Program.Run` is called with `Lint = true` and no `.buildmark.yaml` present. + +**Expected**: `BuildMarkConfigReader` returns a default/empty result; exit code is 0. + +**Requirement coverage**: `BuildMark-Configuration-BuildMarkConfigReader` + +### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration + +**Scenario**: `RepoConnectorFactory.Create` is called with a `ConnectorConfig` that +specifies GitHub settings. + +**Expected**: The returned connector has the GitHub configuration applied. + +**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`, +`BuildMark-Configuration-GitHubConnectorConfig` + +### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration + +**Scenario**: `RepoConnectorFactory.Create` is called with Azure DevOps connector config. + +**Expected**: The returned connector has the Azure DevOps configuration applied. + +**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig`, +`BuildMark-Configuration-AzureDevOpsConnectorConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-BuildMarkConfigReader**: + Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero +- **BuildMark-Configuration-ConnectorConfig**: + RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration, + RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration +- **BuildMark-Configuration-GitHubConnectorConfig**: + RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration +- **BuildMark-Configuration-AzureDevOpsConnectorConfig**: + RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration diff --git a/docs/verification/build-mark/configuration/connector-config.md b/docs/verification/build-mark/configuration/connector-config.md new file mode 100644 index 00000000..24f6fc2c --- /dev/null +++ b/docs/verification/build-mark/configuration/connector-config.md @@ -0,0 +1,47 @@ +# ConnectorConfig + +## Verification Approach + +`ConnectorConfig` is a data model class verified indirectly through +`RepoConnectorFactoryTests.cs`. Tests that pass a `ConnectorConfig` to +`RepoConnectorFactory.Create` exercise the configuration forwarding path and confirm +that the connector type and sub-configuration are interpreted correctly. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration + +**Scenario**: `ConnectorConfig` with GitHub settings is passed to the factory. + +**Expected**: Factory creates a GitHub connector with the supplied settings applied. + +**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig` + +### RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsConnector + +**Scenario**: `ConnectorConfig` with Azure DevOps type is passed to the factory. + +**Expected**: Factory creates an Azure DevOps connector. + +**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig` + +### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration + +**Scenario**: `ConnectorConfig` with Azure DevOps settings is passed to the factory. + +**Expected**: Factory creates Azure DevOps connector with the supplied settings applied. + +**Requirement coverage**: `BuildMark-Configuration-ConnectorConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-ConnectorConfig**: + RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration, + RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsConnector, + RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration diff --git a/docs/verification/build-mark/configuration/git-hub-connector-config.md b/docs/verification/build-mark/configuration/git-hub-connector-config.md new file mode 100644 index 00000000..64541b7a --- /dev/null +++ b/docs/verification/build-mark/configuration/git-hub-connector-config.md @@ -0,0 +1,29 @@ +# GitHubConnectorConfig + +## Verification Approach + +`GitHubConnectorConfig` is a data model class verified indirectly through +`RepoConnectorFactoryTests.cs`. Tests that supply GitHub-specific configuration +within a `ConnectorConfig` confirm that the settings are forwarded to the created +`GitHubRepoConnector`. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration + +**Scenario**: A `ConnectorConfig` with a `GitHubConnectorConfig` is passed to the factory. + +**Expected**: The resulting connector incorporates the GitHub-specific configuration. + +**Requirement coverage**: `BuildMark-Configuration-GitHubConnectorConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-GitHubConnectorConfig**: + RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration diff --git a/docs/verification/build-mark/configuration/report-config.md b/docs/verification/build-mark/configuration/report-config.md new file mode 100644 index 00000000..e11a51e5 --- /dev/null +++ b/docs/verification/build-mark/configuration/report-config.md @@ -0,0 +1,31 @@ +# ReportConfig + +## Verification Approach + +`ReportConfig` is a data model class verified indirectly through `ProgramTests.cs`. +Report configuration fields (`File`, `Depth`, `IncludeKnownIssues`) influence the +behavior of `Program.ProcessBuildNotes`. The test +`Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues` +exercises the `IncludeKnownIssues` field path. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + +**Scenario**: `ReportConfig.IncludeKnownIssues` is set; report generation includes +known issues. + +**Expected**: Generated report contains a known issues section. + +**Requirement coverage**: `BuildMark-Configuration-ReportConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-ReportConfig**: + Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues diff --git a/docs/verification/build-mark/configuration/rule-config.md b/docs/verification/build-mark/configuration/rule-config.md new file mode 100644 index 00000000..e6b3b692 --- /dev/null +++ b/docs/verification/build-mark/configuration/rule-config.md @@ -0,0 +1,48 @@ +# RuleConfig + +## Verification Approach + +`RuleConfig` is a data model class verified indirectly through `ItemRouterTests.cs` +and connector configuration tests. Tests that provide `RuleConfig` instances to +connectors or to `ItemRouter` exercise the routing logic and confirm that items +are routed to the correct sections. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection + +**Scenario**: A `RuleConfig` with a matching condition routes an item to the +configured section. + +**Expected**: Item appears in the correct section of the output. + +**Requirement coverage**: `BuildMark-Configuration-RuleConfig` + +### ItemRouter_Route_SuppressedRouteOmitsMatchingItem + +**Scenario**: A `RuleConfig` with suppress set routes a matching item to be omitted. + +**Expected**: Item does not appear in any section of the output. + +**Requirement coverage**: `BuildMark-Configuration-RuleConfig` + +### MockRepoConnector_Configure_StoresRulesAndSections + +**Scenario**: `MockRepoConnector.Configure` is called with `RuleConfig` entries. + +**Expected**: Rules are stored; `HasRules` returns `true`. + +**Requirement coverage**: `BuildMark-Configuration-RuleConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-RuleConfig**: + ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection, + ItemRouter_Route_SuppressedRouteOmitsMatchingItem, + MockRepoConnector_Configure_StoresRulesAndSections diff --git a/docs/verification/build-mark/configuration/rule-match-config.md b/docs/verification/build-mark/configuration/rule-match-config.md new file mode 100644 index 00000000..76f93074 --- /dev/null +++ b/docs/verification/build-mark/configuration/rule-match-config.md @@ -0,0 +1,49 @@ +# RuleMatchConfig + +## Verification Approach + +`RuleMatchConfig` is a data model class verified indirectly through `ItemRouterTests.cs`. +Tests that exercise label-based and type-based matching use `RuleMatchConfig` instances +within `RuleConfig` to control item routing. Case-insensitive matching tests confirm +the match comparison behavior. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### ItemRouter_Route_WithWorkItemTypeMatch_RoutesMatchingItem + +**Scenario**: A `RuleMatchConfig` specifying a work item type is used; item with +matching type is routed. + +**Expected**: Item appears in the correct section. + +**Requirement coverage**: `BuildMark-Configuration-RuleMatchConfig` + +### ItemRouter_Route_WithCaseInsensitiveLabelMatch_RoutesItem + +**Scenario**: A `RuleMatchConfig` specifying a label is used; item with matching +label (case-insensitive) is routed. + +**Expected**: Item appears in the correct section regardless of label case. + +**Requirement coverage**: `BuildMark-Configuration-RuleMatchConfig` + +### ItemRouter_Route_WithCaseInsensitiveSuppressedRoute_OmitsMatchingItem + +**Scenario**: A `RuleMatchConfig` on a suppressed rule matches an item case-insensitively. + +**Expected**: Item is omitted from all sections. + +**Requirement coverage**: `BuildMark-Configuration-RuleMatchConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-RuleMatchConfig**: + ItemRouter_Route_WithWorkItemTypeMatch_RoutesMatchingItem, + ItemRouter_Route_WithCaseInsensitiveLabelMatch_RoutesItem, + ItemRouter_Route_WithCaseInsensitiveSuppressedRoute_OmitsMatchingItem diff --git a/docs/verification/build-mark/configuration/section-config.md b/docs/verification/build-mark/configuration/section-config.md new file mode 100644 index 00000000..c1b49a24 --- /dev/null +++ b/docs/verification/build-mark/configuration/section-config.md @@ -0,0 +1,41 @@ +# SectionConfig + +## Verification Approach + +`SectionConfig` is a data model class verified indirectly through +`RepoConnectorsTests.cs` and connector-specific tests. When `Configure` is called +on a connector with `SectionConfig` entries, the connector creates named sections in +the `BuildInformation.RoutedSections` output. Tests that assert on routed sections +exercise `SectionConfig` indirectly. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (Integration) + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: A connector configured with `SectionConfig` entries processes items and +populates `RoutedSections`. + +**Expected**: Routed sections are present in the result as configured. + +**Requirement coverage**: `BuildMark-Configuration-SectionConfig` + +### MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRoutedSections + +**Scenario**: `MockRepoConnector` is configured with sections; `GetBuildInformationAsync` +populates routed sections. + +**Expected**: `RoutedSections` contains the configured section names. + +**Requirement coverage**: `BuildMark-Configuration-SectionConfig` + +## Requirements Coverage + +- **BuildMark-Configuration-SectionConfig**: + RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRoutedSections diff --git a/docs/verification/build-mark/program.md b/docs/verification/build-mark/program.md new file mode 100644 index 00000000..8322304a --- /dev/null +++ b/docs/verification/build-mark/program.md @@ -0,0 +1,109 @@ +# Program + +## Verification Approach + +`Program` unit tests are in `ProgramTests.cs`. Each test constructs a `Context` object +with controlled arguments and output capture, calls `Program.Run`, then asserts on the +context output and exit code. The connector factory is injected via a context override +to avoid live API calls where needed. + +## Dependencies + +| Mock / Stub | Reason | +| ---------------------- | -------------------------------------------------------- | +| `Context` | Constructed with controlled arguments and output capture | +| Connector factory mock | Injected to avoid live API calls | + +## Test Scenarios + +### Program_Version_ReturnsValidVersion + +**Scenario**: `Program.Version` is accessed directly. + +**Expected**: Returns a non-null semver string. + +**Requirement coverage**: `BuildMark-Program-Version` + +### Program_Run_VersionFlag_OutputsVersionToConsole + +**Scenario**: `Program.Run` is called with `Version = true` in context. + +**Expected**: Version string appears in output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Version` + +### Program_Run_HelpFlag_OutputsHelpMessage + +**Scenario**: `Program.Run` is called with `Help = true` in context. + +**Expected**: Help text appears in output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Program_Run_QuestionMarkFlag_OutputsHelpMessage + +**Scenario**: `Program.Run` is called with `?` argument. + +**Expected**: Help text appears in output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Program_Run_LongHelpFlag_OutputsHelpMessage + +**Scenario**: `Program.Run` is called with `--help` argument. + +**Expected**: Help text appears in output; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Help` + +### Program_Run_ValidateFlag_OutputsValidationMessage + +**Scenario**: `Program.Run` is called with `Validate = true`. + +**Expected**: Validation output appears; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Validate` + +### Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues + +**Scenario**: `Program.Run` is called with report flags and `IncludeKnownIssues = true`. + +**Expected**: Report is generated including known issues; exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Report` + +### Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero + +**Scenario**: `Program.Run` is called with `Lint = true` but no config present. + +**Expected**: Exit code is 0. + +**Requirement coverage**: `BuildMark-Program-Lint` + +### Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode + +**Scenario**: `Program.Run` is called with an invalid build version. + +**Expected**: Error is written to stderr; exit code is 1. + +**Requirement coverage**: `BuildMark-Program-ErrorHandling` + +### Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode + +**Scenario**: `Program.Run` is called but connector factory throws `InvalidOperationException`. + +**Expected**: Error is written to stderr; exit code is 1. + +**Requirement coverage**: `BuildMark-Program-ErrorHandling` + +## Requirements Coverage + +- **BuildMark-Program-Version**: Program_Version_ReturnsValidVersion, + Program_Run_VersionFlag_OutputsVersionToConsole +- **BuildMark-Program-Help**: Program_Run_HelpFlag_OutputsHelpMessage, + Program_Run_QuestionMarkFlag_OutputsHelpMessage, Program_Run_LongHelpFlag_OutputsHelpMessage +- **BuildMark-Program-Validate**: Program_Run_ValidateFlag_OutputsValidationMessage +- **BuildMark-Program-Report**: Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues +- **BuildMark-Program-Lint**: Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero +- **BuildMark-Program-ErrorHandling**: Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode, + Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode diff --git a/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-api-types.md b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-api-types.md new file mode 100644 index 00000000..5cd39a2b --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-api-types.md @@ -0,0 +1,55 @@ +# AzureDevOpsApiTypes + +## Verification Approach + +`AzureDevOpsApiTypes` contains the DTO types used to deserialize Azure DevOps REST API +responses. These types have no dedicated test class; they are verified indirectly +through `AzureDevOpsRestClientTests.cs` tests that exercise JSON deserialization of +mocked API responses and through `WorkItemMapperTests.cs` tests that consume the +deserialized data. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ------------------------------------------------------------ | +| `MockHttpMessageHandler` | Provides JSON payloads whose structure matches the DTO types | + +## Test Scenarios (via AzureDevOpsRestClientTests.cs) + +### AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags + +**Scenario**: Tag REST response is deserialized into tag DTOs. + +**Expected**: Tag DTO fields are populated correctly. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` + +### AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits + +**Scenario**: Commits REST response is deserialized into commit DTOs. + +**Expected**: Commit DTO fields are populated correctly. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` + +### AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems + +**Scenario**: Work items REST response is deserialized into work item DTOs. + +**Expected**: Work item DTO fields are populated correctly. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` + +### AzureDevOpsRestClient_GetWorkItemLinksAsync_ValidResponse_ReturnsWorkItemLinks + +**Scenario**: Work item links REST response is deserialized into work item link DTOs. + +**Expected**: Work item link DTO fields are populated correctly. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsApiTypes` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-AzureDevOpsApiTypes**: Verified indirectly through all + 8 tests in `AzureDevOpsRestClientTests.cs` and all 10 tests in + `WorkItemMapperTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-repo-connector.md b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-repo-connector.md new file mode 100644 index 00000000..0a471bec --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-repo-connector.md @@ -0,0 +1,226 @@ +# AzureDevOpsRepoConnector + +## Verification Approach + +`AzureDevOpsRepoConnector` is tested through `AzureDevOpsRepoConnectorTests.cs`, +which contains 25 unit tests. The tests exercise constructor behavior, the full +`GetBuildInformationAsync` pipeline, visibility overrides, type overrides, routing +configuration, known issues filtering by affected versions, and edge cases including +work item deduplication and version tag handling. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ----------------------------------------------------------- | +| `MockHttpMessageHandler` | Intercepts all HTTP calls to the Azure DevOps REST endpoint | + +## Test Scenarios + +### AzureDevOpsRepoConnector_Constructor_CreatesInstance + +**Scenario**: Connector is constructed with no configuration. + +**Expected**: Instance is created without error. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_Constructor_WithConfig_StoresConfigurationOverrides + +**Scenario**: Connector is constructed with an `AzureDevOpsConnectorConfig`. + +**Expected**: Configuration overrides are stored. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: Connector is checked against `IRepoConnector`. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: `GetBuildInformationAsync` processes mocked REST API responses. + +**Expected**: Returns a `BuildInformation` instance with correct version, baseline, +changes, and known issues. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithMultipleVersions_SelectsCorrectBaseline + +**Scenario**: Multiple version tags exist in the mocked response. + +**Expected**: Selects the correct previous release as baseline. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithWorkItems_GathersChangesCorrectly + +**Scenario**: Mocked data includes work items linked to commits. + +**Expected**: Work items appear as change items. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithOpenBugs_IdentifiesKnownIssues + +**Scenario**: Mocked data includes open bug work items. + +**Expected**: Open bugs appear in `KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_PreReleaseWithSameCommitHash_SkipsToNextDifferentHash + +**Scenario**: A pre-release tag shares the same commit hash as the build version. + +**Expected**: Connector skips to the next tag with a different commit hash. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_ReleaseVersion_SkipsAllPreReleases + +**Scenario**: Build version is a release; prior history contains pre-release tags. + +**Expected**: Pre-release tags are skipped; baseline is the previous release. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_PreReleaseNotInHistory_UsesLatestDifferentHash + +**Scenario**: Pre-release tag is not found in commit history; connector falls back. + +**Expected**: Uses the latest tag with a different commit hash as baseline. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_PreReleaseAllPreviousSameHash_ReturnsNullBaseline + +**Scenario**: All previous tags share the same commit hash. + +**Expected**: `BuildInformation.BaselineVersion` is `null`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithDuplicateWorkItem_DeduplicatesChanges + +**Scenario**: Mocked data contains the same work item linked to multiple commits. + +**Expected**: Work item appears only once in `Changes`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_VisibilityInternal_ExcludesItem + +**Scenario**: A work item has `visibility: internal` in its buildmark block. + +**Expected**: Work item is excluded from the public output. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_VisibilityPublic_IncludesItem + +**Scenario**: A work item has `visibility: public` in its buildmark block. + +**Expected**: Work item is included in the output. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug + +**Scenario**: A work item has `type: bug` in its buildmark block. + +**Expected**: Work item is classified as a bug. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature + +**Scenario**: A work item has `type: feature` in its buildmark block. + +**Expected**: Work item is classified as a feature. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_Configure_WithRules_HasRulesReturnsTrue + +**Scenario**: `Configure` is called with routing rules. + +**Expected**: `HasRules` returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections + +**Scenario**: Connector is configured with routing rules and run with mock data. + +**Expected**: `BuildInformation.RoutedSections` is populated correctly. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_KnownIssues_FilteredByAffectedVersions + +**Scenario**: Known issues have `affected-versions` set; build version is outside the +range. + +**Expected**: Issues outside the range are excluded. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchingAffectedVersions_IsKnownIssue + +**Scenario**: A closed bug has `affected-versions` that includes the build version. + +**Expected**: Closed bug appears in `KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithNoTags_ReturnsEmptyBuildInformation + +**Scenario**: Repository has no version tags. + +**Expected**: Returns `BuildInformation` with null baseline and empty change lists. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithNoCommitsBetweenVersions_ReturnsEmptyChanges + +**Scenario**: No commits exist between the build version and the baseline. + +**Expected**: `Changes` is empty. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WorkItemWithNoBugType_NotKnownIssue + +**Scenario**: Open work item is not of type bug. + +**Expected**: Work item is not added to `KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WorkItemWithCompletedState_NotKnownIssue + +**Scenario**: Bug work item is in a completed state. + +**Expected**: Completed bug is not added to `KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOpsRepoConnector_GetBuildInformationAsync_WithAzureDevOpsUrl_GeneratesChangelogLink + +**Scenario**: Repository URL is an Azure DevOps URL. + +**Expected**: Generated changelog link uses the Azure DevOps compare format. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: + AzureDevOpsRepoConnector_ImplementsInterface_ReturnsTrue +- **BuildMark-RepoConnectors-AzureDevOps**: All remaining 24 tests in + `AzureDevOpsRepoConnectorTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-rest-client.md b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-rest-client.md new file mode 100644 index 00000000..a3f37335 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-rest-client.md @@ -0,0 +1,85 @@ +# AzureDevOpsRestClient + +## Verification Approach + +`AzureDevOpsRestClient` is tested through `AzureDevOpsRestClientTests.cs`, which +contains 8 unit tests. The tests cover successful data retrieval for tags, commits, +work items, and work item links, as well as HTTP error handling and invalid JSON +handling. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ------------------------------------------------------------ | +| `MockHttpMessageHandler` | Intercepts all HTTP calls to the Azure DevOps REST endpoints | + +## Test Scenarios + +### AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags + +**Scenario**: Valid REST API response for the tags endpoint. + +**Expected**: Returns the list of tags from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetTagsAsync_HttpError_ReturnsEmptyList + +**Scenario**: Tags endpoint returns a non-success status code. + +**Expected**: Returns an empty list without throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits + +**Scenario**: Valid REST API response for the commits endpoint. + +**Expected**: Returns the list of commits from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetCommitsAsync_HttpError_ReturnsEmptyList + +**Scenario**: Commits endpoint returns a non-success status code. + +**Expected**: Returns an empty list without throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems + +**Scenario**: Valid REST API response for the work items endpoint. + +**Expected**: Returns the list of work items from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetWorkItemsAsync_HttpError_ReturnsEmptyList + +**Scenario**: Work items endpoint returns a non-success status code. + +**Expected**: Returns an empty list without throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetWorkItemLinksAsync_ValidResponse_ReturnsWorkItemLinks + +**Scenario**: Valid REST API response for the work item links endpoint. + +**Expected**: Returns the list of work item links from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +### AzureDevOpsRestClient_GetWorkItemLinksAsync_HttpError_ReturnsEmptyList + +**Scenario**: Work item links endpoint returns a non-success status code. + +**Expected**: Returns an empty list without throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOpsRestClient` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-AzureDevOpsRestClient**: All 8 tests in + `AzureDevOpsRestClientTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops.md b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops.md new file mode 100644 index 00000000..ca3e2697 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops.md @@ -0,0 +1,65 @@ +# Azure DevOps + +## Verification Approach + +The Azure DevOps sub-subsystem is verified through `AzureDevOpsTests.cs` (5 subsystem- +level tests), `AzureDevOpsRepoConnectorTests.cs` (25 unit tests), +`AzureDevOpsRestClientTests.cs` (8 unit tests), and `WorkItemMapperTests.cs` (10 unit +tests). The subsystem tests exercise the full Azure DevOps data pipeline through mock +HTTP responses. The unit tests are described in the individual unit chapters. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | -------------------------------------------------- | +| `MockHttpMessageHandler` | Intercepts HTTP calls to the Azure DevOps REST API | + +## Test Scenarios (Subsystem-Level, AzureDevOpsTests.cs) + +### AzureDevOps_ImplementsInterface_ReturnsTrue + +**Scenario**: `AzureDevOpsRepoConnector` is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: Azure DevOps connector receives mocked REST API data. + +**Expected**: Returns valid `BuildInformation` with correct fields. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOps_GetBuildInformation_WithWorkItems_GathersChanges + +**Scenario**: Mock data includes work items linked to commits. + +**Expected**: Work items appear in `BuildInformation.Changes`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOps_GetBuildInformation_WithOpenBugs_IdentifiesKnownIssues + +**Scenario**: Mock data includes open bug work items. + +**Expected**: Bugs appear in `BuildInformation.KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases + +**Scenario**: Build version is a release; pre-release tags in history are skipped. + +**Expected**: Baseline is the previous release tag. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: AzureDevOps_ImplementsInterface_ReturnsTrue +- **BuildMark-RepoConnectors-AzureDevOps**: AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + AzureDevOps_GetBuildInformation_WithWorkItems_GathersChanges, + AzureDevOps_GetBuildInformation_WithOpenBugs_IdentifiesKnownIssues, + AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases diff --git a/docs/verification/build-mark/repo-connectors/azure-dev-ops/work-item-mapper.md b/docs/verification/build-mark/repo-connectors/azure-dev-ops/work-item-mapper.md new file mode 100644 index 00000000..357b701f --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/azure-dev-ops/work-item-mapper.md @@ -0,0 +1,101 @@ +# WorkItemMapper + +## Verification Approach + +`WorkItemMapper` is tested through `WorkItemMapperTests.cs`, which contains 10 unit +tests. The tests verify mapping of Azure DevOps work items to the BuildMark model - +classification of features and bugs, title and description extraction, change link +generation, and handling of known issue identification based on work item type and +state. + +## Dependencies + +| Mock / Stub | Reason | +| -------------------- | ----------------------------------------------------------- | +| `WorkItem` test data | Constructed in-line with specific types, states, and fields | + +## Test Scenarios + +### WorkItemMapper_MapToItemInfo_Bug_ReturnsBugType + +**Scenario**: A work item with type `"Bug"` is mapped. + +**Expected**: `ItemInfo.Type` is bug. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_MapToItemInfo_Feature_ReturnsFeatureType + +**Scenario**: A work item with type `"Feature"` is mapped. + +**Expected**: `ItemInfo.Type` is feature. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_MapToItemInfo_UserStory_ReturnsFeatureType + +**Scenario**: A work item with type `"User Story"` is mapped. + +**Expected**: `ItemInfo.Type` is feature. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_MapToItemInfo_Task_ReturnsOtherType + +**Scenario**: A work item with type `"Task"` is mapped. + +**Expected**: `ItemInfo.Type` is other (or a non-bug, non-feature classification). + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_MapToItemInfo_ExtractsTitle + +**Scenario**: A work item has a title. + +**Expected**: Mapped `ItemInfo.Title` matches the work item title. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_MapToItemInfo_ExtractsDescription + +**Scenario**: A work item has a description. + +**Expected**: Mapped `ItemInfo.Description` matches the work item description. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_MapToItemInfo_GeneratesWebLink + +**Scenario**: A work item has an ID and a valid organization URL. + +**Expected**: `ItemInfo.WebLink` contains the Azure DevOps work item URL. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_IsKnownIssue_OpenBug_ReturnsTrue + +**Scenario**: Work item is an open bug. + +**Expected**: `IsKnownIssue` returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_IsKnownIssue_ClosedBug_ReturnsFalse + +**Scenario**: Work item is a closed bug. + +**Expected**: `IsKnownIssue` returns `false`. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +### WorkItemMapper_IsKnownIssue_OpenFeature_ReturnsFalse + +**Scenario**: Work item is an open feature (not a bug). + +**Expected**: `IsKnownIssue` returns `false`. + +**Requirement coverage**: `BuildMark-RepoConnectors-WorkItemMapper` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-WorkItemMapper**: All 10 tests in `WorkItemMapperTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md new file mode 100644 index 00000000..bf45b53b --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md @@ -0,0 +1,423 @@ +# GitHubGraphQLClient + +## Verification Approach + +`GitHubGraphQLClient` is tested through five dedicated test files, each covering one +query method. All tests use `MockHttpMessageHandler` to intercept HTTP requests and +return controlled JSON responses. Tests cover successful responses, empty data, +missing required properties, HTTP errors, invalid JSON, single-item responses, and +pagination. + +| Test File | Method Tested | Test Count | +| -------------------------------------------- | -------------------------------------- | ---------- | +| `GitHubGraphQLClientFindIssueIdsTests.cs` | `FindIssueIdsLinkedToPullRequestAsync` | 8 | +| `GitHubGraphQLClientGetAllIssuesTests.cs` | `GetAllIssuesAsync` | 8 | +| `GitHubGraphQLClientGetAllTagsTests.cs` | `GetAllTagsAsync` | 8 | +| `GitHubGraphQLClientGetCommitsTests.cs` | `GetCommitsAsync` | 8 | +| `GitHubGraphQLClientGetPullRequestsTests.cs` | `GetPullRequestsAsync` | 9 | +| `GitHubGraphQLClientGetReleasesTests.cs` | `GetReleasesAsync` | 8 | + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ------------------------------------------------------- | +| `MockHttpMessageHandler` | Intercepts HTTP calls; returns controlled JSON payloads | + +## Test Scenarios + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds + +**Scenario**: Valid GraphQL response is returned for a linked-issues query. + +**Expected**: Returns the list of issue IDs extracted from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_NoIssues_ReturnsEmptyList + +**Scenario**: Response contains no linked issues. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_MissingData_ReturnsEmptyList + +**Scenario**: Response JSON is missing the data structure. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_HttpError_ReturnsEmptyList + +**Scenario**: HTTP request returns a non-success status code. + +**Expected**: Returns an empty list without throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_InvalidJson_ReturnsEmptyList + +**Scenario**: Response body is not valid JSON. + +**Expected**: Returns an empty list without throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_SingleIssue_ReturnsOneIssueId + +**Scenario**: Response contains exactly one linked issue. + +**Expected**: Returns a list with one issue ID. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_MissingNumberProperty_SkipsInvalidNodes + +**Scenario**: One node in the response is missing the `number` property. + +**Expected**: Invalid node is skipped; valid nodes are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues + +**Scenario**: Response uses pagination (multiple pages of linked issues). + +**Expected**: All pages are fetched and all issue IDs are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssues + +**Scenario**: Valid response for GetAllIssues query. + +**Expected**: Returns all issues from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_NoIssues_ReturnsEmptyList + +**Scenario**: Response contains no issues. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_MissingData_ReturnsEmptyList + +**Scenario**: Response is missing the data structure. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_NullNodes_ReturnsEmptyList + +**Scenario**: Response has null nodes array. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_InvalidIssues_FiltersThemOut + +**Scenario**: Response contains some invalid issue objects. + +**Expected**: Invalid issues are filtered out; valid issues are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAllIssues + +**Scenario**: Issues span multiple pages. + +**Expected**: All pages are fetched and all issues are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_Exception_ReturnsEmptyList + +**Scenario**: An exception is thrown during the HTTP request. + +**Expected**: Returns an empty list without re-throwing. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody + +**Scenario**: Valid response includes issues with a body field. + +**Expected**: Returned issues include the body content. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes + +**Scenario**: Valid response for GetAllTags query. + +**Expected**: Returns all tag nodes from the response. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_NoTags_ReturnsEmptyList + +**Scenario**: Response contains no tags. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_MissingData_ReturnsEmptyList + +**Scenario**: Response is missing the data structure. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_HttpError_ReturnsEmptyList + +**Scenario**: HTTP request returns a non-success status code. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_InvalidJson_ReturnsEmptyList + +**Scenario**: Response body is not valid JSON. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_SingleTag_ReturnsOneTagNode + +**Scenario**: Response contains exactly one tag. + +**Expected**: Returns a list with one tag node. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_MissingNameProperty_SkipsInvalidNodes + +**Scenario**: One tag node is missing the `name` property. + +**Expected**: Invalid node is skipped. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllTags + +**Scenario**: Tags span multiple pages. + +**Expected**: All pages are fetched and all tags are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas + +**Scenario**: Valid response for GetCommits query. + +**Expected**: Returns all commit SHAs. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_NoCommits_ReturnsEmptyList + +**Scenario**: Response contains no commits. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_MissingData_ReturnsEmptyList + +**Scenario**: Response is missing the data structure. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_HttpError_ReturnsEmptyList + +**Scenario**: HTTP error response received. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_InvalidJson_ReturnsEmptyList + +**Scenario**: Invalid JSON in response body. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_SingleCommit_ReturnsOneCommitSha + +**Scenario**: Response contains exactly one commit. + +**Expected**: Returns a list with one commit SHA. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_MissingOidProperty_SkipsInvalidNodes + +**Scenario**: One commit node is missing the `oid` property. + +**Expected**: Invalid node is skipped. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits + +**Scenario**: Commits span multiple pages. + +**Expected**: All pages are fetched and all commit SHAs are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests + +**Scenario**: Valid response for GetPullRequests query. + +**Expected**: Returns all pull requests. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_NoPullRequests_ReturnsEmptyList + +**Scenario**: Response contains no pull requests. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_MissingData_ReturnsEmptyList + +**Scenario**: Response is missing the data structure. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_HttpError_ReturnsEmptyList + +**Scenario**: HTTP error response received. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_InvalidJson_ReturnsEmptyList + +**Scenario**: Invalid JSON in response body. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_SinglePullRequest_ReturnsOnePullRequest + +**Scenario**: Response contains exactly one pull request. + +**Expected**: Returns a list with one pull request. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_MissingNumberOrTitle_SkipsInvalidNodes + +**Scenario**: One pull request node is missing `number` or `title`. + +**Expected**: Invalid node is skipped. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_ReturnsAllPullRequests + +**Scenario**: Pull requests span multiple pages. + +**Expected**: All pages are fetched and all pull requests are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody + +**Scenario**: Valid response includes pull requests with a body field. + +**Expected**: Returned pull requests include the body content. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsReleaseTagNames + +**Scenario**: Valid response for GetReleases query. + +**Expected**: Returns all release tag names. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_NoReleases_ReturnsEmptyList + +**Scenario**: Response contains no releases. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_MissingData_ReturnsEmptyList + +**Scenario**: Response is missing the data structure. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_HttpError_ReturnsEmptyList + +**Scenario**: HTTP error response received. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_InvalidJson_ReturnsEmptyList + +**Scenario**: Invalid JSON in response body. + +**Expected**: Returns an empty list. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_SingleRelease_ReturnsOneTagName + +**Scenario**: Response contains exactly one release. + +**Expected**: Returns a list with one tag name. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_MissingTagNameProperty_SkipsInvalidNodes + +**Scenario**: One release node is missing the `tagName` property. + +**Expected**: Invalid node is skipped. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +### GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAllReleases + +**Scenario**: Releases span multiple pages. + +**Expected**: All pages are fetched and all release tag names are returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLClient` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-GitHubGraphQLClient**: All 41 tests across the five + `GitHubGraphQLClient*Tests.cs` files diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md new file mode 100644 index 00000000..e8a03f85 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md @@ -0,0 +1,45 @@ +# GitHubGraphQLTypes + +## Verification Approach + +`GitHubGraphQLTypes` contains the data transfer object (DTO) types used to deserialize +GitHub GraphQL API responses. These types have no dedicated test class; they are +verified indirectly through all `GitHubGraphQLClient*Tests.cs` tests that exercise +JSON deserialization of mocked API responses. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ------------------------------------------------------------ | +| `MockHttpMessageHandler` | Provides JSON payloads whose structure matches the DTO types | + +## Test Scenarios (via GitHubGraphQLClient*Tests.cs) + +### GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody + +**Scenario**: GraphQL response for issues is deserialized into issue DTOs. + +**Expected**: Issue DTOs contain the expected fields including `body`. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLTypes` + +### GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody + +**Scenario**: GraphQL response for pull requests is deserialized into pull request DTOs. + +**Expected**: Pull request DTOs contain the expected fields including `body`. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLTypes` + +### GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes + +**Scenario**: GraphQL response for tags is deserialized into tag node DTOs. + +**Expected**: Tag node DTOs contain `name` and target commit hash fields. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHubGraphQLTypes` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-GitHubGraphQLTypes**: Verified indirectly through all + 41 tests in the `GitHubGraphQLClient*Tests.cs` files diff --git a/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md b/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md new file mode 100644 index 00000000..0ac83eae --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md @@ -0,0 +1,200 @@ +# GitHubRepoConnector + +## Verification Approach + +`GitHubRepoConnector` is tested through `GitHubRepoConnectorTests.cs`, which contains +22 unit tests. The tests exercise constructor behavior (with and without config), +the full `GetBuildInformationAsync` pipeline with various scenarios, visibility and +type overrides, routing configuration, known issues filtering by affected versions, +and edge cases such as duplicate commit SHAs and substring label matching. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | -------------------------------------------------------- | +| `MockHttpMessageHandler` | Intercepts all HTTP calls to the GitHub GraphQL endpoint | + +## Test Scenarios + +### GitHubRepoConnector_Constructor_CreatesInstance + +**Scenario**: `GitHubRepoConnector` is constructed with no configuration. + +**Expected**: Instance is created without error. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_Constructor_WithConfig_StoresConfigurationOverrides + +**Scenario**: `GitHubRepoConnector` is constructed with a `GitHubConnectorConfig`. + +**Expected**: Configuration overrides (e.g., owner, repo) are stored. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: Connector is checked against `IRepoConnector`. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### GitHubRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: `GetBuildInformationAsync` processes mocked GraphQL responses. + +**Expected**: Returns a `BuildInformation` instance with correct version, baseline, +changes, and known issues. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_WithMultipleVersions_SelectsCorrectPreviousVersionAndGeneratesChangelogLink + +**Scenario**: Multiple version tags exist in the mocked response. + +**Expected**: Selects the correct previous release and generates a GitHub changelog link. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_WithPullRequests_GathersChangesCorrectly + +**Scenario**: Mocked data includes pull requests with labels. + +**Expected**: Each pull request is represented as a change item with correct type +classification. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_WithOpenIssues_IdentifiesKnownIssues + +**Scenario**: Mocked data includes open issues. + +**Expected**: Open issues appear in `BuildInformation.KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_PreReleaseWithSameCommitHash_SkipsToNextDifferentHash + +**Scenario**: A pre-release tag shares the same commit hash as the build version. + +**Expected**: Connector skips to the next tag with a different commit hash. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_ReleaseVersion_SkipsAllPreReleases + +**Scenario**: Build version is a release; prior history contains pre-release tags. + +**Expected**: Pre-release tags are skipped; baseline is the previous release. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_PreReleaseNotInHistory_UsesLatestDifferentHash + +**Scenario**: Pre-release tag is not found in commit history; connector falls back. + +**Expected**: Uses the latest tag with a different commit hash as baseline. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_PreReleaseAllPreviousSameHash_ReturnsNullBaseline + +**Scenario**: All previous tags share the same commit hash as the build version. + +**Expected**: `BuildInformation.BaselineVersion` is `null`. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_WithDuplicateMergeCommitSha_DoesNotThrow + +**Scenario**: Mocked data contains pull requests with duplicate merge commit SHAs. + +**Expected**: No exception is thrown; result is returned normally. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_PrWithSubstringMatchLabel_NotClassifiedAsBug + +**Scenario**: A pull request has a label that is a substring of `"bug"` (e.g., `"b"`). + +**Expected**: Pull request is not classified as a bug. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_IssueWithSubstringMatchLabel_NotClassifiedAsKnownIssue + +**Scenario**: An issue has a label that is a substring of a known issue label. + +**Expected**: Issue is not classified as a known issue. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_VisibilityInternal_ExcludesItem + +**Scenario**: An item has `visibility: internal` in its buildmark block. + +**Expected**: Item is excluded from the public output. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_VisibilityPublic_IncludesItem + +**Scenario**: An item has `visibility: public` in its buildmark block. + +**Expected**: Item is included in the output. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug + +**Scenario**: An item has `type: bug` in its buildmark block. + +**Expected**: Item is classified as a bug regardless of labels. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature + +**Scenario**: An item has `type: feature` in its buildmark block. + +**Expected**: Item is classified as a feature regardless of labels. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue + +**Scenario**: `Configure` is called with routing rules. + +**Expected**: `HasRules` returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections + +**Scenario**: Connector is configured with routing rules and run with mock data. + +**Expected**: `BuildInformation.RoutedSections` is populated with items routed per rules. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_KnownIssues_FilteredByAffectedVersions + +**Scenario**: Known issues have `affected-versions` set; build version is outside the range. + +**Expected**: Issues outside the affected version range are excluded. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHubRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchingAffectedVersions_IsKnownIssue + +**Scenario**: A closed bug has `affected-versions` that includes the build version. + +**Expected**: Closed bug appears in `KnownIssues`. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: GitHubRepoConnector_ImplementsInterface_ReturnsTrue +- **BuildMark-RepoConnectors-GitHub**: All remaining 21 tests in `GitHubRepoConnectorTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/github/github.md b/docs/verification/build-mark/repo-connectors/github/github.md new file mode 100644 index 00000000..0111f8d1 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/github/github.md @@ -0,0 +1,73 @@ +# GitHub + +## Verification Approach + +The GitHub sub-subsystem is verified through `GitHubTests.cs` (6 subsystem-level +tests), `GitHubRepoConnectorTests.cs` (22 unit tests), and 5 `GitHubGraphQLClient*Tests.cs` +files (41 tests). The subsystem tests exercise the full GitHub data pipeline through +mock HTTP responses. The unit tests are described in the individual unit chapters. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ----------------------------------------------- | +| `MockHttpMessageHandler` | Intercepts HTTP calls to the GitHub GraphQL API | + +## Test Scenarios (Subsystem-Level, GitHubTests.cs) + +### GitHub_ImplementsInterface_ReturnsTrue + +**Scenario**: `GitHubRepoConnector` is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### GitHub_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: GitHub connector receives mocked GraphQL data. + +**Expected**: Returns valid `BuildInformation` with correct fields. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHub_GetBuildInformation_WithMultipleVersions_SelectsCorrectBaseline + +**Scenario**: Multiple tags exist; connector selects the correct prior release. + +**Expected**: Baseline version is the most recent release before the build version. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHub_GetBuildInformation_WithPullRequests_GathersChanges + +**Scenario**: Mock data contains pull requests merged since the baseline. + +**Expected**: `BuildInformation.Changes` contains entries for each pull request. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHub_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues + +**Scenario**: Mock data contains open issues. + +**Expected**: `BuildInformation.KnownIssues` contains entries for each open issue. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### GitHub_GetBuildInformation_ReleaseVersion_SkipsPreReleases + +**Scenario**: Build version is a release; pre-release tags in the history are skipped. + +**Expected**: Baseline is the previous release tag. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: GitHub_ImplementsInterface_ReturnsTrue +- **BuildMark-RepoConnectors-GitHub**: GitHub_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + GitHub_GetBuildInformation_WithMultipleVersions_SelectsCorrectBaseline, + GitHub_GetBuildInformation_WithPullRequests_GathersChanges, + GitHub_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues, + GitHub_GetBuildInformation_ReleaseVersion_SkipsPreReleases diff --git a/docs/verification/build-mark/repo-connectors/i-repo-connector.md b/docs/verification/build-mark/repo-connectors/i-repo-connector.md new file mode 100644 index 00000000..778db671 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/i-repo-connector.md @@ -0,0 +1,76 @@ +# IRepoConnector + +## Verification Approach + +`IRepoConnector` is an interface with no dedicated test class. Its contract is +verified through all tests that exercise concrete implementations: `GitHubRepoConnector`, +`AzureDevOpsRepoConnector`, and `MockRepoConnector`. Each implementation is checked +against the interface via cast or type-assertion tests that confirm the concrete class +implements `IRepoConnector`. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | --------- | +| None | Interface | + +## Test Scenarios (Integration via Implementations) + +### RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: `GitHubRepoConnector` instance is checked for `IRepoConnector` implementation. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### RepoConnectors_MockConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: `MockRepoConnector` instance is checked for `IRepoConnector` implementation. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### RepoConnectors_AzureDevOps_ImplementsInterface_ReturnsTrue + +**Scenario**: `AzureDevOpsRepoConnector` instance is checked for `IRepoConnector` +implementation. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### GitHubRepoConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: `GitHubRepoConnector` type check against `IRepoConnector`. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### MockRepoConnector_ImplementsInterface + +**Scenario**: `MockRepoConnector` type check against `IRepoConnector`. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### AzureDevOpsRepoConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: `AzureDevOpsRepoConnector` type check against `IRepoConnector`. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: + RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue, + RepoConnectors_MockConnector_ImplementsInterface_ReturnsTrue, + RepoConnectors_AzureDevOps_ImplementsInterface_ReturnsTrue, + GitHubRepoConnector_ImplementsInterface_ReturnsTrue, + MockRepoConnector_ImplementsInterface, + AzureDevOpsRepoConnector_ImplementsInterface_ReturnsTrue diff --git a/docs/verification/build-mark/repo-connectors/item-controls-info.md b/docs/verification/build-mark/repo-connectors/item-controls-info.md new file mode 100644 index 00000000..70644b75 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/item-controls-info.md @@ -0,0 +1,76 @@ +# ItemControlsInfo + +## Verification Approach + +`ItemControlsInfo` is a data model class with no dedicated test class. It is verified +indirectly through `ItemControlsTests.cs` and `ItemControlsParserTests.cs`. The parser +tests assert on the resulting `ItemControlsInfo` instances, confirming that visibility, +type, and affected versions fields are populated correctly. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios (via ItemControlsTests.cs - 13 tests) + +### ItemControls_Parse_WithVisibilityPublic_ReturnsPublicVisibility + +**Scenario**: A buildmark block with `visibility: public` is parsed. + +**Expected**: `ItemControlsInfo.Visibility` is public. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +### ItemControls_Parse_WithVisibilityInternal_ReturnsInternalVisibility + +**Scenario**: A buildmark block with `visibility: internal` is parsed. + +**Expected**: `ItemControlsInfo.Visibility` is internal. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +### ItemControls_Parse_WithTypeBug_ReturnsBugType + +**Scenario**: A buildmark block with `type: bug` is parsed. + +**Expected**: `ItemControlsInfo.Type` is bug. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +### ItemControls_Parse_WithTypeFeature_ReturnsFeatureType + +**Scenario**: A buildmark block with `type: feature` is parsed. + +**Expected**: `ItemControlsInfo.Type` is feature. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +### ItemControls_Parse_WithAffectedVersions_ReturnsIntervalSet + +**Scenario**: A buildmark block with `affected-versions` is parsed. + +**Expected**: `ItemControlsInfo.AffectedVersions` contains the interval set. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +### ItemControls_Parse_WithHiddenBlock_ReturnsControls + +**Scenario**: A hidden buildmark block is parsed. + +**Expected**: `ItemControlsInfo` is non-null. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +### ItemControls_Parse_WithNoBlock_ReturnsNull + +**Scenario**: Text with no buildmark block is parsed. + +**Expected**: Returns `null` (no `ItemControlsInfo`). + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsInfo` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-ItemControlsInfo**: All 13 tests in `ItemControlsTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/item-controls-parser.md b/docs/verification/build-mark/repo-connectors/item-controls-parser.md new file mode 100644 index 00000000..6e331bbe --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/item-controls-parser.md @@ -0,0 +1,143 @@ +# ItemControlsParser + +## Verification Approach + +`ItemControlsParser` is tested through `ItemControlsParserTests.cs`, which contains +15 unit tests. The tests cover parsing `null` and empty descriptions, descriptions +with no block, and descriptions containing a buildmark block with various field +combinations (visibility, type, affected-versions, hidden). Unknown keys and +unrecognized values are tested for graceful ignorance. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### ItemControlsParser_Parse_WithNullDescription_ReturnsNull + +**Scenario**: `ItemControlsParser.Parse` is called with `null`. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithEmptyDescription_ReturnsNull + +**Scenario**: `ItemControlsParser.Parse` is called with an empty string. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithNoBlock_ReturnsNull + +**Scenario**: `ItemControlsParser.Parse` is called with text that contains no +buildmark block. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithVisibilityPublic_ReturnsPublicVisibility + +**Scenario**: Description contains a buildmark block with `visibility: public`. + +**Expected**: Returns `ItemControlsInfo` with public visibility. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithVisibilityInternal_ReturnsInternalVisibility + +**Scenario**: Description contains a buildmark block with `visibility: internal`. + +**Expected**: Returns `ItemControlsInfo` with internal visibility. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithTypeBug_ReturnsBugType + +**Scenario**: Description contains a buildmark block with `type: bug`. + +**Expected**: Returns `ItemControlsInfo` with bug type. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithTypeFeature_ReturnsFeatureType + +**Scenario**: Description contains a buildmark block with `type: feature`. + +**Expected**: Returns `ItemControlsInfo` with feature type. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithAffectedVersions_ReturnsIntervalSet + +**Scenario**: Description contains a buildmark block with `affected-versions`. + +**Expected**: Returns `ItemControlsInfo` with a non-null `AffectedVersions`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithHiddenBlock_ReturnsControls + +**Scenario**: Description contains a hidden (but valid) buildmark block. + +**Expected**: Returns non-null `ItemControlsInfo`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithHiddenBlockVisibilityInternal_ReturnsInternalVisibility + +**Scenario**: Hidden block contains `visibility: internal`. + +**Expected**: Returns `ItemControlsInfo` with internal visibility. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithUnknownKey_IgnoresKey + +**Scenario**: Buildmark block contains an unrecognized key. + +**Expected**: Unknown key is ignored; other fields are parsed normally. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithUnrecognizedVisibilityValue_IgnoresValue + +**Scenario**: Buildmark block contains `visibility: unknown-value`. + +**Expected**: Visibility is not set; `ItemControlsInfo` has default visibility. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithUnrecognizedTypeValue_IgnoresValue + +**Scenario**: Buildmark block contains `type: unknown-value`. + +**Expected**: Type is not set; `ItemControlsInfo` has default type. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_AllFields_ReturnsCompleteInfo + +**Scenario**: Buildmark block contains visibility, type, and affected-versions. + +**Expected**: Returns `ItemControlsInfo` with all three fields populated. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### ItemControlsParser_Parse_WithUnrecognizedAffectedVersionsValue_IgnoresValue + +**Scenario**: Buildmark block contains an invalid `affected-versions` value. + +**Expected**: `AffectedVersions` is not set or is empty; no exception thrown. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-ItemControlsParser**: All 15 tests in + `ItemControlsParserTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/item-router.md b/docs/verification/build-mark/repo-connectors/item-router.md new file mode 100644 index 00000000..ac44f419 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/item-router.md @@ -0,0 +1,86 @@ +# ItemRouter + +## Verification Approach + +`ItemRouter` is tested through `ItemRouterTests.cs`, which contains 8 unit tests. +The tests cover matching rules (with and without a match block), suppression rules, +type-based matching, label-based matching (case-insensitive), routing to new sections, +and default section fallback. + +## Dependencies + +| Mock / Stub | Reason | +| ---------------- | ------------------------------------------------------------ | +| `ItemInfo` stubs | Constructed with specific labels and types for routing tests | +| `SectionConfig` | Provided as test input to define available sections | +| `RuleConfig` | Provided as test input to define routing rules | + +## Test Scenarios + +### ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection + +**Scenario**: An item matching a configured rule's label is routed. + +**Expected**: Item appears in the section specified by the rule. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_SuppressedRouteOmitsMatchingItem + +**Scenario**: An item matching a suppressed rule is processed. + +**Expected**: Item is not placed in any section. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_WithNullMatchBlock_MatchesAllItems + +**Scenario**: A rule with a `null` match block is configured; all items are tested. + +**Expected**: All items match the rule (null match = match all). + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_WithWorkItemTypeMatch_RoutesMatchingItem + +**Scenario**: A rule matching a specific work item type is applied. + +**Expected**: Items of the matching type are routed to the configured section. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_WithNoMatchingRule_RoutesToDefaultSection + +**Scenario**: An item that does not match any rule is processed. + +**Expected**: Item is placed in the default section. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_ItemNotInConfiguredSections_CreatesNewSection + +**Scenario**: A rule routes an item to a section name not in `SectionConfig`. + +**Expected**: A new section is created dynamically for the item. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_WithCaseInsensitiveLabelMatch_RoutesItem + +**Scenario**: Rule matches a label `"bug"` and item has label `"Bug"`. + +**Expected**: Case-insensitive match routes the item correctly. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### ItemRouter_Route_WithCaseInsensitiveSuppressedRoute_OmitsMatchingItem + +**Scenario**: Suppressed rule matches a label case-insensitively. + +**Expected**: Item is omitted from all sections. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-ItemRouter**: All 8 tests in `ItemRouterTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md b/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md new file mode 100644 index 00000000..14680475 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md @@ -0,0 +1,110 @@ +# MockRepoConnector + +## Verification Approach + +`MockRepoConnector` is tested through `MockRepoConnectorTests.cs`, which contains +11 unit tests. The tests verify that the connector correctly returns in-memory data +supplied via its configuration API, handles all routing and `HasRules` logic, and +correctly implements all members of `IRepoConnector`. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------------------------------- | +| None | Self-contained in-memory connector | + +## Test Scenarios + +### MockRepoConnector_ImplementsInterface + +**Scenario**: `MockRepoConnector` is checked against `IRepoConnector`. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### MockRepoConnector_Constructor_CreatesInstance + +**Scenario**: `MockRepoConnector` is constructed with no arguments. + +**Expected**: Instance is created without error. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_GetBuildInformationAsync_NoData_ReturnsEmptyBuildInformation + +**Scenario**: `GetBuildInformationAsync` is called on an unconfigured connector. + +**Expected**: Returns an empty `BuildInformation` instance. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_SetBuildVersion_StoresVersion + +**Scenario**: `SetBuildVersion` is called with a version string. + +**Expected**: `BuildInformation.Version` equals the supplied version. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_SetBaselineVersion_StoresBaseline + +**Scenario**: `SetBaselineVersion` is called with a version string. + +**Expected**: `BuildInformation.BaselineVersion` equals the supplied version. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_AddChange_AddsItemToChanges + +**Scenario**: `AddChange` is called with a change item. + +**Expected**: `BuildInformation.Changes` contains the added item. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_AddKnownIssue_AddsItemToKnownIssues + +**Scenario**: `AddKnownIssue` is called with a known issue item. + +**Expected**: `BuildInformation.KnownIssues` contains the added item. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_Configure_WithRules_HasRulesReturnsTrue + +**Scenario**: `Configure` is called with a non-empty rules list. + +**Expected**: `HasRules` returns `true`. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_Configure_EmptyRules_HasRulesReturnsFalse + +**Scenario**: `Configure` is called with an empty rules list. + +**Expected**: `HasRules` returns `false`. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections + +**Scenario**: Connector is configured with routing rules; changes and known issues are +added; `GetBuildInformationAsync` is called. + +**Expected**: `BuildInformation.RoutedSections` is populated according to the rules. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### MockRepoConnector_GetBuildInformationAsync_WithChangelogLink_StoresLink + +**Scenario**: A changelog link is set on the connector. + +**Expected**: `BuildInformation.ChangelogLink` equals the supplied link. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: MockRepoConnector_ImplementsInterface +- **BuildMark-RepoConnectors-Mock**: All remaining 10 tests in `MockRepoConnectorTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/mock/mock.md b/docs/verification/build-mark/repo-connectors/mock/mock.md new file mode 100644 index 00000000..a73d033f --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/mock/mock.md @@ -0,0 +1,47 @@ +# Mock + +## Verification Approach + +The Mock sub-subsystem is verified through `MockTests.cs` (3 subsystem-level tests) +and `MockRepoConnectorTests.cs` (11 unit tests). The subsystem tests confirm +integration of the mock connector within the broader pipeline. The unit tests are +described in the individual unit chapter. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | --------- | +| None | Pure mock | + +## Test Scenarios (Subsystem-Level, MockTests.cs) + +### Mock_ImplementsInterface_ReturnsTrue + +**Scenario**: `MockRepoConnector` is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### Mock_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: Mock connector is configured with direct data and `GetBuildInformationAsync` +is called. + +**Expected**: Returns the configured `BuildInformation` directly. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### Mock_GetBuildInformation_WithNoData_ReturnsEmptyBuildInformation + +**Scenario**: Mock connector is not configured; `GetBuildInformationAsync` is called. + +**Expected**: Returns an empty `BuildInformation`. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: Mock_ImplementsInterface_ReturnsTrue +- **BuildMark-RepoConnectors-Mock**: Mock_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation, + Mock_GetBuildInformation_WithNoData_ReturnsEmptyBuildInformation diff --git a/docs/verification/build-mark/repo-connectors/repo-connector-base.md b/docs/verification/build-mark/repo-connectors/repo-connector-base.md new file mode 100644 index 00000000..5b2183e8 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/repo-connector-base.md @@ -0,0 +1,61 @@ +# RepoConnectorBase + +## Verification Approach + +`RepoConnectorBase` is tested through `RepoConnectorBaseTests.cs`, which contains +5 unit tests. The tests exercise `Configure` (storing rules and sections and setting +`HasRules`), `ApplyRules` (routing items to sections), and `FindVersionIndex` (locating +a version tag in a list, including cross-prefix equality). + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------- | -------------------------------------------------------------- | +| Concrete subclass fixture | Tests instantiate a minimal concrete subclass for testing base | + +## Test Scenarios + +### RepoConnectorBase_Configure_StoresRulesAndSections_HasRulesReturnsTrue + +**Scenario**: `Configure` is called with a non-empty rules list. + +**Expected**: `HasRules` returns `true`; rules are stored for use in `ApplyRules`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorBase` + +### RepoConnectorBase_Configure_EmptyRules_HasRulesReturnsFalse + +**Scenario**: `Configure` is called with an empty rules list. + +**Expected**: `HasRules` returns `false`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorBase` + +### RepoConnectorBase_ApplyRules_RoutesItemsToConfiguredSections + +**Scenario**: `ApplyRules` is called with items and configured routing rules. + +**Expected**: Each item is placed in the correct section as specified by the rules. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorBase` + +### RepoConnectorBase_FindVersionIndex_DifferentPrefixSameVersion_ReturnsCorrectIndex + +**Scenario**: `FindVersionIndex` is called with a tag list containing a tag with a +different prefix but the same version as the search target. + +**Expected**: The index of the matching tag is returned. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorBase` + +### RepoConnectorBase_FindVersionIndex_VersionNotInList_ReturnsMinusOne + +**Scenario**: `FindVersionIndex` is called with a version that is not in the list. + +**Expected**: Returns `-1`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorBase` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-RepoConnectorBase**: All 5 tests in `RepoConnectorBaseTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/repo-connector-factory.md b/docs/verification/build-mark/repo-connectors/repo-connector-factory.md new file mode 100644 index 00000000..f51ba6f9 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/repo-connector-factory.md @@ -0,0 +1,110 @@ +# RepoConnectorFactory + +## Verification Approach + +`RepoConnectorFactory` is tested through `RepoConnectorFactoryTests.cs`, which contains +11 unit tests. The tests cover connector creation from default settings, from explicit +connector type configuration, from environment variables (GitHub Actions, Azure DevOps), +and from remote URL detection. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | -------------------------------------------------------- | +| Environment variables | Tests set/clear `GITHUB_ACTIONS` and `TF_BUILD` env vars | +| Git remote URL (process) | Factory may invoke Git to detect the remote URL | + +## Test Scenarios + +### RepoConnectorFactory_Create_ReturnsConnector + +**Scenario**: `RepoConnectorFactory.Create` is called with `null` configuration. + +**Expected**: Returns a non-null `IRepoConnector` instance. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_ReturnsGitHubConnectorForThisRepo + +**Scenario**: Factory is invoked in the GitHub Actions CI environment. + +**Expected**: Returns a `GitHubRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration + +**Scenario**: Factory is called with a `ConnectorConfig` specifying GitHub settings. + +**Expected**: Returns a connector with GitHub settings applied. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsConnector + +**Scenario**: Factory is called with `ConnectorConfig.Type = "azure-devops"`. + +**Expected**: Returns an `AzureDevOpsRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration + +**Scenario**: Factory is called with Azure DevOps connector configuration. + +**Expected**: Returns an `AzureDevOpsRepoConnector` with the supplied settings. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithTfBuildEnv_ReturnsAzureDevOpsConnector + +**Scenario**: `TF_BUILD` environment variable is set to `"True"`. + +**Expected**: Factory returns an `AzureDevOpsRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithGitHubActionsEnv_ReturnsGitHubConnector + +**Scenario**: `GITHUB_ACTIONS` environment variable is set to `"true"`. + +**Expected**: Factory returns a `GitHubRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithAzureDevOpsRemoteUrl_ReturnsAzureDevOpsConnector + +**Scenario**: Git remote URL matches an Azure DevOps `dev.azure.com` pattern. + +**Expected**: Factory returns an `AzureDevOpsRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithVisualStudioRemoteUrl_ReturnsAzureDevOpsConnector + +**Scenario**: Git remote URL matches a `visualstudio.com` pattern. + +**Expected**: Factory returns an `AzureDevOpsRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithGitHubRemoteUrl_ReturnsGitHubConnector + +**Scenario**: Git remote URL matches a `github.com` pattern. + +**Expected**: Factory returns a `GitHubRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectorFactory_Create_WithNullRemoteUrl_DefaultsToGitHubConnector + +**Scenario**: Git remote URL cannot be determined (null/empty). + +**Expected**: Factory defaults to returning a `GitHubRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-RepoConnectorFactory**: All 11 tests in + `RepoConnectorFactoryTests.cs` diff --git a/docs/verification/build-mark/repo-connectors/repo-connectors.md b/docs/verification/build-mark/repo-connectors/repo-connectors.md new file mode 100644 index 00000000..7716b028 --- /dev/null +++ b/docs/verification/build-mark/repo-connectors/repo-connectors.md @@ -0,0 +1,307 @@ +# RepoConnectors + +## Verification Approach + +The RepoConnectors subsystem is verified through `RepoConnectorsTests.cs`, which +contains 33 subsystem-level integration tests. These tests exercise the connector +factory, the GitHub connector, the Azure DevOps connector, and the Mock connector +through the full `GetBuildInformationAsync` pipeline using mock HTTP data. Individual +unit tests for sub-components are described in the unit-level chapters. + +## Dependencies + +| Mock / Stub | Reason | +| ------------------------ | ------------------------------------------------------------- | +| `MockHttpMessageHandler` | Intercepts HTTP calls to GitHub GraphQL and Azure DevOps REST | +| `MockRepoConnector` | Used directly for factory and base class tests | +| `ProcessRunner` (real) | Used by ProcessRunner tests with actual OS commands | + +## Test Scenarios + +### RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: The GitHub connector instance is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: GitHub connector receives mocked GraphQL responses. + +**Expected**: Returns a `BuildInformation` with correct version, changes, and known issues. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithMultipleVersions_SelectsCorrectBaseline + +**Scenario**: Multiple version tags exist; connector selects the correct baseline. + +**Expected**: Baseline version is the most recent release prior to the build version. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithPullRequests_GathersChanges + +**Scenario**: Mock data includes pull requests; connector gathers them as changes. + +**Expected**: `BuildInformation.Changes` contains entries for each pull request. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### RepoConnectors_GitHubConnector_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues + +**Scenario**: Mock data includes open issues; connector identifies them as known issues. + +**Expected**: `BuildInformation.KnownIssues` contains entries for open issues. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### RepoConnectors_GitHubConnector_GetBuildInformation_ReleaseVersion_SkipsPreReleases + +**Scenario**: Build version is a release; pre-release tags in history are skipped. + +**Expected**: Baseline is a previous release tag, not a pre-release. + +**Requirement coverage**: `BuildMark-RepoConnectors-GitHub` + +### RepoConnectors_ConnectorBase_MockConnector_ImplementsInterface + +**Scenario**: Mock connector is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### RepoConnectors_ConnectorBase_GitHubConnector_ImplementsInterface + +**Scenario**: GitHub connector class is checked for `RepoConnectorBase` inheritance. + +**Expected**: GitHub connector extends `RepoConnectorBase`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorBase` + +### RepoConnectors_MockConnector_Constructor_CreatesInstance + +**Scenario**: `MockRepoConnector` is constructed. + +**Expected**: Instance is created without error. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### RepoConnectors_MockConnector_ImplementsInterface_ReturnsTrue + +**Scenario**: Mock connector is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### RepoConnectors_MockConnector_GetBuildInformation_ReturnsExpectedVersion + +**Scenario**: Mock connector's `GetBuildInformationAsync` is called. + +**Expected**: Returns `BuildInformation` with the expected version. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation + +**Scenario**: Mock connector returns complete build information. + +**Expected**: All `BuildInformation` fields are populated. + +**Requirement coverage**: `BuildMark-RepoConnectors-Mock` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput + +**Scenario**: `ProcessRunner.TryRunAsync` with a valid OS command. + +**Expected**: Returns non-null output. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull + +**Scenario**: `ProcessRunner.TryRunAsync` with an invalid command. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull + +**Scenario**: `ProcessRunner.TryRunAsync` with a command that exits non-zero. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput + +**Scenario**: `ProcessRunner.RunAsync` with a valid command. + +**Expected**: Returns output string. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException + +**Scenario**: `ProcessRunner.RunAsync` with a failing command. + +**Expected**: Throws exception. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_Factory_Create_ReturnsConnector + +**Scenario**: `RepoConnectorFactory.Create` is called with no configuration. + +**Expected**: Returns a non-null connector instance. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectors_Factory_Create_ReturnsGitHubConnectorForThisRepo + +**Scenario**: Factory detects GitHub Actions environment or remote URL. + +**Expected**: Returns a `GitHubRepoConnector`. + +**Requirement coverage**: `BuildMark-RepoConnectors-RepoConnectorFactory` + +### RepoConnectors_ItemControls_VisibilityPublic_ReturnsPublicVisibility + +**Scenario**: `ItemControlsParser.Parse` processes a block with `visibility: public`. + +**Expected**: Returns controls with public visibility. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemControls_VisibilityInternal_ReturnsInternalVisibility + +**Scenario**: `ItemControlsParser.Parse` processes a block with `visibility: internal`. + +**Expected**: Returns controls with internal visibility. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemControls_TypeBug_ReturnsBugType + +**Scenario**: `ItemControlsParser.Parse` processes a block with `type: bug`. + +**Expected**: Returns controls with bug type. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemControls_TypeFeature_ReturnsFeatureType + +**Scenario**: `ItemControlsParser.Parse` processes a block with `type: feature`. + +**Expected**: Returns controls with feature type. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemControls_AffectedVersions_ReturnsIntervalSet + +**Scenario**: `ItemControlsParser.Parse` processes a block with `affected-versions`. + +**Expected**: Returns controls with a non-empty `VersionIntervalSet`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemControls_HiddenBlock_ReturnsControls + +**Scenario**: `ItemControlsParser.Parse` processes a hidden buildmark block. + +**Expected**: Returns non-null controls. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemControls_NoBlock_ReturnsNull + +**Scenario**: `ItemControlsParser.Parse` is called with text containing no buildmark block. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemControlsParser` + +### RepoConnectors_ItemRouter_MatchingRule_RoutesToSection + +**Scenario**: `ItemRouter.Route` routes an item matching a configured rule. + +**Expected**: Item appears in the correct routed section. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### RepoConnectors_ItemRouter_SuppressedRoute_OmitsItem + +**Scenario**: `ItemRouter.Route` suppresses an item matching a suppressed rule. + +**Expected**: Item is not present in any section. + +**Requirement coverage**: `BuildMark-RepoConnectors-ItemRouter` + +### RepoConnectors_AzureDevOps_ImplementsInterface_ReturnsTrue + +**Scenario**: Azure DevOps connector is checked against `IRepoConnector`. + +**Expected**: Implements the interface. + +**Requirement coverage**: `BuildMark-RepoConnectors-IRepoConnector` + +### RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation + +**Scenario**: Azure DevOps connector receives mocked REST responses. + +**Expected**: Returns valid `BuildInformation`. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges + +**Scenario**: Mock data includes Azure DevOps pull requests. + +**Expected**: `BuildInformation.Changes` contains the pull requests as change items. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### RepoConnectors_AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues + +**Scenario**: Mock data includes open work items. + +**Expected**: `BuildInformation.KnownIssues` contains entries for open bugs. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +### RepoConnectors_AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases + +**Scenario**: Release build; pre-release tags in history are skipped. + +**Expected**: Baseline is a previous release tag. + +**Requirement coverage**: `BuildMark-RepoConnectors-AzureDevOps` + +## Requirements Coverage + +- **BuildMark-RepoConnectors-IRepoConnector**: RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue, + RepoConnectors_ConnectorBase_MockConnector_ImplementsInterface, + RepoConnectors_MockConnector_ImplementsInterface_ReturnsTrue, + RepoConnectors_AzureDevOps_ImplementsInterface_ReturnsTrue +- **BuildMark-RepoConnectors-RepoConnectorBase**: RepoConnectors_ConnectorBase_GitHubConnector_ImplementsInterface +- **BuildMark-RepoConnectors-RepoConnectorFactory**: RepoConnectors_Factory_Create_ReturnsConnector, + RepoConnectors_Factory_Create_ReturnsGitHubConnectorForThisRepo +- **BuildMark-RepoConnectors-ItemRouter**: RepoConnectors_ItemRouter_MatchingRule_RoutesToSection, + RepoConnectors_ItemRouter_SuppressedRoute_OmitsItem +- **BuildMark-RepoConnectors-ItemControlsParser**: RepoConnectors_ItemControls_VisibilityPublic_ReturnsPublicVisibility, + RepoConnectors_ItemControls_VisibilityInternal_ReturnsInternalVisibility, + RepoConnectors_ItemControls_TypeBug_ReturnsBugType, + RepoConnectors_ItemControls_TypeFeature_ReturnsFeatureType, + RepoConnectors_ItemControls_AffectedVersions_ReturnsIntervalSet, + RepoConnectors_ItemControls_HiddenBlock_ReturnsControls, + RepoConnectors_ItemControls_NoBlock_ReturnsNull +- **BuildMark-RepoConnectors-GitHub**: Multiple GitHub connector tests +- **BuildMark-RepoConnectors-AzureDevOps**: Multiple Azure DevOps connector tests +- **BuildMark-RepoConnectors-Mock**: RepoConnectors_MockConnector_Constructor_CreatesInstance, + RepoConnectors_MockConnector_GetBuildInformation_ReturnsExpectedVersion, + RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation diff --git a/docs/verification/build-mark/self-test/self-test.md b/docs/verification/build-mark/self-test/self-test.md new file mode 100644 index 00000000..b4165e30 --- /dev/null +++ b/docs/verification/build-mark/self-test/self-test.md @@ -0,0 +1,30 @@ +# SelfTest + +## Verification Approach + +The SelfTest subsystem is verified through the `Program_Run_ValidateFlag_OutputsValidationMessage` +test in `ProgramTests.cs` and through the CI pipeline integration test step +`Run self-validation`. Both confirm that the `Validation.Run` method can be invoked +without errors and produces output, providing evidence that all internal self-checks +pass in the test environment. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | -------------------------------------------------------- | +| `Context` | Provides output capture for validation message assertion | + +## Test Scenarios (Integration) + +### Program_Run_ValidateFlag_OutputsValidationMessage + +**Scenario**: `Program.Run` is called with `Validate = true`. + +**Expected**: Validation framework runs; output is written; exit code is 0. + +**Requirement coverage**: `BuildMark-SelfTest-Validation`, `BuildMark-Program-Validate` + +## Requirements Coverage + +- **BuildMark-SelfTest-Validation**: Program_Run_ValidateFlag_OutputsValidationMessage +- **BuildMark-Program-Validate**: Program_Run_ValidateFlag_OutputsValidationMessage diff --git a/docs/verification/build-mark/self-test/validation.md b/docs/verification/build-mark/self-test/validation.md new file mode 100644 index 00000000..c6f53bd4 --- /dev/null +++ b/docs/verification/build-mark/self-test/validation.md @@ -0,0 +1,34 @@ +# Validation + +## Verification Approach + +`Validation` is tested indirectly through `ProgramTests.cs`. The test +`Program_Run_ValidateFlag_OutputsValidationMessage` invokes `Program.Run` with +`Validate = true`, which delegates to `Validation.Run`. Successful completion without +exception or non-zero exit code constitutes evidence that the validation framework +is operational. + +The CI pipeline integration test `Run self-validation` also runs +`buildmark --validate --results artifacts/validation-*.trx` on each operating +system and .NET runtime combination, providing platform-level evidence. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ----------------------------------------------------- | +| `Context` | Provides output stream for validation result messages | + +## Test Scenarios + +### Program_Run_ValidateFlag_OutputsValidationMessage + +**Scenario**: `Program.Run` is called with `Validate = true`; control reaches +`Validation.Run`. + +**Expected**: Validation output is written to the context output; exit code is 0. + +**Requirement coverage**: `BuildMark-SelfTest-Validation` + +## Requirements Coverage + +- **BuildMark-SelfTest-Validation**: Program_Run_ValidateFlag_OutputsValidationMessage diff --git a/docs/verification/build-mark/utilities/path-helpers.md b/docs/verification/build-mark/utilities/path-helpers.md new file mode 100644 index 00000000..521740c7 --- /dev/null +++ b/docs/verification/build-mark/utilities/path-helpers.md @@ -0,0 +1,43 @@ +# PathHelpers + +## Verification Approach + +`PathHelpers` is a pure utility class with no dedicated test class. It is verified +indirectly through CLI and program tests that exercise path-related flag handling +(`--log`, `--report`, `--results`) and through the overall build pipeline where +paths are resolved during document generation. + +No direct unit tests exist for `PathHelpers` because the class provides straightforward +path combination logic with no branching that requires isolated testing. Its behavior +is validated through the integration tests that consume it. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios (Integration) + +### Cli_LogFlag_CreatesLogFile + +**Scenario**: `Context` is created with a log file path; `PathHelpers` is used to +resolve the file path. + +**Expected**: Log file is created at the resolved path. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` + +### Cli_ReportFlags_SetProperties + +**Scenario**: `Context` is created with `--report` flag; path is processed by the +context/utilities layer. + +**Expected**: `ReportFile` property contains the resolved path. + +**Requirement coverage**: `BuildMark-Utilities-PathHelpers` + +## Requirements Coverage + +- **BuildMark-Utilities-PathHelpers**: Cli_LogFlag_CreatesLogFile, + Cli_ReportFlags_SetProperties diff --git a/docs/verification/build-mark/utilities/process-runner.md b/docs/verification/build-mark/utilities/process-runner.md new file mode 100644 index 00000000..87c69e96 --- /dev/null +++ b/docs/verification/build-mark/utilities/process-runner.md @@ -0,0 +1,67 @@ +# ProcessRunner + +## Verification Approach + +`ProcessRunner` is tested through `RepoConnectorsTests.cs`. The five ProcessRunner +tests exercise `TryRunAsync` (which returns `null` on failure) and `RunAsync` (which +throws on failure) using real OS processes to confirm the process execution logic is +correct on the target operating system. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ----------------------------------------------------------- | +| None | Real OS processes are used to test actual process execution | + +## Test Scenarios + +### RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput + +**Scenario**: `ProcessRunner.TryRunAsync` is called with a valid system command. + +**Expected**: Returns a non-null output string. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull + +**Scenario**: `ProcessRunner.TryRunAsync` is called with an invalid command name. + +**Expected**: Returns `null` without throwing. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull + +**Scenario**: `ProcessRunner.TryRunAsync` is called with a command that exits with +a non-zero code. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput + +**Scenario**: `ProcessRunner.RunAsync` is called with a valid command. + +**Expected**: Returns the process standard output as a string. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException + +**Scenario**: `ProcessRunner.RunAsync` is called with a command that returns a +non-zero exit code. + +**Expected**: An exception is thrown. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +## Requirements Coverage + +- **BuildMark-Utilities-ProcessRunner**: + RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput, + RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull, + RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull, + RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput, + RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException diff --git a/docs/verification/build-mark/utilities/utilities.md b/docs/verification/build-mark/utilities/utilities.md new file mode 100644 index 00000000..47892ab3 --- /dev/null +++ b/docs/verification/build-mark/utilities/utilities.md @@ -0,0 +1,68 @@ +# Utilities + +## Verification Approach + +The Utilities subsystem is verified through `RepoConnectorsTests.cs` (for +`ProcessRunner`) and indirectly through `CliTests.cs` (for `PathHelpers` via +path-related flag handling). There is no dedicated `UtilitiesTests.cs` file; +coverage is provided by integration-level tests that exercise the utility classes +as they are used by other units. + +## Dependencies + +| Mock / Stub | Reason | +| ------------- | --------------------------------------------------------------------- | +| None required | `ProcessRunner` tests use real processes; `PathHelpers` is pure logic | + +## Test Scenarios (Integration) + +The following integration tests in `RepoConnectorsTests.cs` exercise `ProcessRunner`: + +### RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput + +**Scenario**: `ProcessRunner.TryRunAsync` is called with a valid system command. + +**Expected**: Returns non-null output string from the process. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull + +**Scenario**: `ProcessRunner.TryRunAsync` is called with an invalid/nonexistent command. + +**Expected**: Returns `null` rather than throwing. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull + +**Scenario**: `ProcessRunner.TryRunAsync` is called with a command that exits non-zero. + +**Expected**: Returns `null` due to non-zero exit code. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput + +**Scenario**: `ProcessRunner.RunAsync` is called with a valid command. + +**Expected**: Returns process output string. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +### RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException + +**Scenario**: `ProcessRunner.RunAsync` is called with a command that fails. + +**Expected**: Throws an exception. + +**Requirement coverage**: `BuildMark-Utilities-ProcessRunner` + +## Requirements Coverage + +- **BuildMark-Utilities-ProcessRunner**: + RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput, + RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull, + RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull, + RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput, + RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException diff --git a/docs/verification/build-mark/version/version-commit-tag.md b/docs/verification/build-mark/version/version-commit-tag.md new file mode 100644 index 00000000..6877f777 --- /dev/null +++ b/docs/verification/build-mark/version/version-commit-tag.md @@ -0,0 +1,29 @@ +# VersionCommitTag + +## Verification Approach + +`VersionCommitTag` is a simple data class with no dedicated test class. It is verified +through `VersionTests.cs` via the test +`VersionCommitTag_Constructor_ValidParameters_CreatesInstance`, which constructs an +instance and asserts that the tag and commit-hash properties are stored correctly. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Data class | + +## Test Scenarios + +### VersionCommitTag_Constructor_ValidParameters_CreatesInstance + +**Scenario**: `VersionCommitTag` is constructed with a `VersionTag` and a commit hash +string. + +**Expected**: The `Tag` and `CommitHash` properties return the supplied values. + +**Requirement coverage**: `BuildMark-Version-VersionCommitTag` + +## Requirements Coverage + +- **BuildMark-Version-VersionCommitTag**: VersionCommitTag_Constructor_ValidParameters_CreatesInstance diff --git a/docs/verification/build-mark/version/version-comparable.md b/docs/verification/build-mark/version/version-comparable.md new file mode 100644 index 00000000..b142e7c6 --- /dev/null +++ b/docs/verification/build-mark/version/version-comparable.md @@ -0,0 +1,228 @@ +# VersionComparable + +## Verification Approach + +`VersionComparable` is tested through `VersionComparableTests.cs`, which contains +26 unit tests. The tests cover creation (valid, invalid, null, empty), comparison +operators, and numeric-vs-lexicographic pre-release ordering rules that follow the +Semantic Versioning specification. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### VersionComparable_Create_ValidVersion_ReturnsInstance + +**Scenario**: `VersionComparable.Create` is called with a valid version string. + +**Expected**: Returns a non-null instance. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Create_SimpleVersion_ParsesVersion + +**Scenario**: `VersionComparable.Create` is called with a simple `major.minor.patch` string. + +**Expected**: Major, minor, and patch properties reflect parsed values. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Create_PreReleaseVersion_ParsesVersion + +**Scenario**: `VersionComparable.Create` is called with a pre-release version string. + +**Expected**: Pre-release identifiers are parsed correctly. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_TryCreate_InvalidVersion_ReturnsNull + +**Scenario**: `VersionComparable.TryCreate` is called with an invalid version string. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_TryCreate_NullInput_ReturnsNull + +**Scenario**: `VersionComparable.TryCreate` is called with a `null` argument. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_TryCreate_EmptyInput_ReturnsNull + +**Scenario**: `VersionComparable.TryCreate` is called with an empty string. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Create_InvalidVersion_ThrowsArgumentException + +**Scenario**: `VersionComparable.Create` is called with an invalid version string. + +**Expected**: `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_SameMajorMinorPatch_ReturnsZero + +**Scenario**: Two instances with identical major.minor.patch are compared. + +**Expected**: `CompareTo` returns 0. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_DifferentMajor_ReturnsCorrectOrder + +**Scenario**: Two instances with different major versions are compared. + +**Expected**: Higher major version compares as greater. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_DifferentMinor_ReturnsCorrectOrder + +**Scenario**: Two instances with different minor versions are compared. + +**Expected**: Higher minor version compares as greater. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_DifferentPatch_ReturnsCorrectOrder + +**Scenario**: Two instances with different patch versions are compared. + +**Expected**: Higher patch version compares as greater. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_PreReleaseVsRelease_ReturnsCorrectOrder + +**Scenario**: A pre-release version is compared to its release counterpart. + +**Expected**: Pre-release is less than release per semver rules. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_PreReleaseVersions_ReturnsLexicographicOrder + +**Scenario**: Two pre-release versions with non-numeric identifiers are compared. + +**Expected**: Comparison follows lexicographic order. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Operators_LessThan_WorksCorrectly + +**Scenario**: The `<` operator is applied to two version instances. + +**Expected**: Returns the correct boolean result. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Operators_GreaterThan_WorksCorrectly + +**Scenario**: The `>` operator is applied to two version instances. + +**Expected**: Returns the correct boolean result. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Operators_LessThanOrEqual_WorksCorrectly + +**Scenario**: The `<=` operator is applied to two version instances. + +**Expected**: Returns the correct boolean result. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_Operators_GreaterThanOrEqual_WorksCorrectly + +**Scenario**: The `>=` operator is applied to two version instances. + +**Expected**: Returns the correct boolean result. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_SemanticVersions_ReturnsCorrectOrder + +**Scenario**: A series of semver-compliant versions is compared. + +**Expected**: Ordering matches the semver specification. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_NumericComparison_CorrectOrdering + +**Scenario**: Numeric pre-release identifiers are compared. + +**Expected**: Numeric comparison is used (11 > 9). + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_ReleaseGreaterThanPreRelease_CorrectOrdering + +**Scenario**: Release version `1.0.0` is compared to `1.0.0-alpha`. + +**Expected**: Release is greater than pre-release. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_PreReleaseLexicographic_CorrectOrdering + +**Scenario**: Pre-release identifiers with alphabetic content are compared. + +**Expected**: Lexicographic ordering is applied. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_PreReleaseNumeric_ComparesNumerically + +**Scenario**: Pre-release identifiers that are purely numeric are compared. + +**Expected**: Numeric comparison is used. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_PreReleaseSemVerRules_CorrectOrdering + +**Scenario**: Pre-release versions are compared following all semver rules. + +**Expected**: Ordering matches semver 2.0.0 specification section 11. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_NumericVsNonNumeric_NumericIsLess + +**Scenario**: A numeric pre-release identifier is compared to a non-numeric one. + +**Expected**: Numeric identifier has lower precedence. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_ShorterPreRelease_IsLess + +**Scenario**: Two pre-release versions with different numbers of identifiers are compared. + +**Expected**: Shorter pre-release is less than longer when all common fields are equal. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionComparable_CompareTo_ComplexPreRelease_CorrectOrdering + +**Scenario**: Complex multi-identifier pre-release versions are compared. + +**Expected**: Correct ordering following semver field-by-field rules. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +## Requirements Coverage + +- **BuildMark-Version-VersionComparable**: All 26 tests in `VersionComparableTests.cs` diff --git a/docs/verification/build-mark/version/version-interval-set.md b/docs/verification/build-mark/version/version-interval-set.md new file mode 100644 index 00000000..6670f407 --- /dev/null +++ b/docs/verification/build-mark/version/version-interval-set.md @@ -0,0 +1,128 @@ +# VersionIntervalSet + +## Verification Approach + +`VersionIntervalSet` is tested through `VersionIntervalSetTests.cs`, which contains +13 unit tests. The tests cover parsing single and multiple intervals, handling of +internal commas in interval strings, empty input, discarding invalid tokens, and +`Contains` checks for strings, `VersionTag` instances, pre-release versions, and +`VersionComparable` instances. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### VersionIntervalSet_Parse_SingleInterval_ReturnsOneInterval + +**Scenario**: `VersionIntervalSet.Parse` is called with a single interval string. + +**Expected**: Returns a set containing one interval. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Parse_TwoIntervals_ReturnsTwoIntervals + +**Scenario**: `VersionIntervalSet.Parse` is called with two comma-separated intervals. + +**Expected**: Returns a set containing two intervals. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Parse_IntervalsWithInternalComma_ParsedCorrectly + +**Scenario**: `VersionIntervalSet.Parse` is called with an interval string that +contains a comma as the separator between lower and upper bound. + +**Expected**: Each interval is parsed as a unit; internal commas do not split intervals. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Parse_EmptyString_ReturnsEmptySet + +**Scenario**: `VersionIntervalSet.Parse` is called with an empty string. + +**Expected**: Returns an empty set. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Parse_InvalidToken_DiscardedSilently + +**Scenario**: `VersionIntervalSet.Parse` is called with a string containing invalid +tokens mixed with valid intervals. + +**Expected**: Invalid tokens are silently discarded; valid intervals are retained. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_StringInsideFirstInterval_ReturnsTrue + +**Scenario**: `Contains` is called with a string version inside the first interval. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_StringInsideLaterInterval_ReturnsTrue + +**Scenario**: `Contains` is called with a string version inside a later interval. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_StringOutsideAllIntervals_ReturnsFalse + +**Scenario**: `Contains` is called with a string version outside all intervals. + +**Expected**: Returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_EmptySet_ReturnsFalse + +**Scenario**: `Contains` is called on an empty set. + +**Expected**: Returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_VersionTag_DelegatesToSemanticVersion + +**Scenario**: `Contains` is called with a `VersionTag` argument. + +**Expected**: Delegates to the tag's semantic version for comparison. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_PreReleaseVersions_HandlesCorrectly + +**Scenario**: `Contains` is called with pre-release version strings. + +**Expected**: Correctly applies semver pre-release ordering rules. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Contains_VersionComparable_HandlesPreRelease + +**Scenario**: `Contains` is called with a `VersionComparable` that has pre-release. + +**Expected**: Pre-release ordering rules are applied correctly. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +### VersionIntervalSet_Parse_PreReleaseBounds_ParsesCorrectly + +**Scenario**: `VersionIntervalSet.Parse` is called with intervals using pre-release +version bounds. + +**Expected**: Pre-release bounds are parsed and stored correctly. + +**Requirement coverage**: `BuildMark-Version-VersionIntervalSet` + +## Requirements Coverage + +- **BuildMark-Version-VersionIntervalSet**: All 13 tests in `VersionIntervalSetTests.cs` diff --git a/docs/verification/build-mark/version/version-interval.md b/docs/verification/build-mark/version/version-interval.md new file mode 100644 index 00000000..d4c6226e --- /dev/null +++ b/docs/verification/build-mark/version/version-interval.md @@ -0,0 +1,192 @@ +# VersionInterval + +## Verification Approach + +`VersionInterval` is tested through `VersionIntervalTests.cs`, which contains 21 unit +tests. The tests cover parsing of inclusive and exclusive lower/upper bounds, unbounded +intervals, invalid input handling, and `Contains` checks for string versions, semantic +versions, and pre-release versions. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### VersionInterval_Parse_InclusiveLower_IsInclusive + +**Scenario**: `VersionInterval.Parse` is called with `[1.0.0,2.0.0]`. + +**Expected**: Lower bound is inclusive; `Contains("1.0.0")` returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_ExclusiveLower_IsExclusive + +**Scenario**: `VersionInterval.Parse` is called with `(1.0.0,2.0.0)`. + +**Expected**: Lower bound is exclusive; `Contains("1.0.0")` returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_InclusiveUpper_IsInclusive + +**Scenario**: `VersionInterval.Parse` is called with an interval using `]` upper bracket. + +**Expected**: Upper bound is inclusive; `Contains` at the upper bound returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_ExclusiveUpper_IsExclusive + +**Scenario**: `VersionInterval.Parse` is called with an interval using `)` upper bracket. + +**Expected**: Upper bound is exclusive; `Contains` at the upper bound returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_UnboundedLower_HasNullLowerBound + +**Scenario**: `VersionInterval.Parse` is called with `(,2.0.0)` (no lower bound). + +**Expected**: Lower bound property is `null`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_UnboundedUpper_HasNullUpperBound + +**Scenario**: `VersionInterval.Parse` is called with `[1.0.0,)` (no upper bound). + +**Expected**: Upper bound property is `null`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_BothBoundsPresent_ReturnsInterval + +**Scenario**: `VersionInterval.Parse` is called with a fully bounded interval. + +**Expected**: Both bound properties are non-null. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_InvalidFormat_ReturnsNull + +**Scenario**: `VersionInterval.Parse` is called with a string that does not match +the interval format. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_NullInput_ReturnsNull + +**Scenario**: `VersionInterval.Parse` is called with `null`. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Parse_EmptyString_ReturnsNull + +**Scenario**: `VersionInterval.Parse` is called with an empty string. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_StringEqualToInclusiveLower_ReturnsTrue + +**Scenario**: `Contains` is called with a string equal to the inclusive lower bound. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_StringEqualToExclusiveLower_ReturnsFalse + +**Scenario**: `Contains` is called with a string equal to the exclusive lower bound. + +**Expected**: Returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_StringEqualToInclusiveUpper_ReturnsTrue + +**Scenario**: `Contains` is called with a string equal to the inclusive upper bound. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_StringEqualToExclusiveUpper_ReturnsFalse + +**Scenario**: `Contains` is called with a string equal to the exclusive upper bound. + +**Expected**: Returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_StringInsideUnboundedInterval_ReturnsTrue + +**Scenario**: `Contains` is called with a version inside an unbounded interval. + +**Expected**: Returns `true`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_StringOutsideInterval_ReturnsFalse + +**Scenario**: `Contains` is called with a version outside the interval bounds. + +**Expected**: Returns `false`. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_Version_DelegatesToSemanticVersion + +**Scenario**: `Contains` is called with a `VersionSemantic` argument. + +**Expected**: Comparison delegates to the semantic version correctly. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_PreReleaseBounds_HandlesCorrectly + +**Scenario**: An interval with pre-release bounds is created; `Contains` is called +with a pre-release version. + +**Expected**: Correctly determines membership using semver pre-release ordering. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_PreReleaseToPreRelease_HandlesCorrectly + +**Scenario**: An interval spanning two pre-release versions; `Contains` checks a +version between them. + +**Expected**: Returns `true` for versions in range, `false` outside. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_PreReleaseOrdering_UsesNumericComparison + +**Scenario**: Interval bounds use numeric pre-release identifiers; `Contains` is +called with intermediate numeric pre-releases. + +**Expected**: Numeric comparison is applied correctly. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionInterval_Contains_VersionComparable_HandlesPreRelease + +**Scenario**: `Contains` is called with a `VersionComparable` that has a pre-release. + +**Expected**: Comparison uses semver pre-release ordering rules. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +## Requirements Coverage + +- **BuildMark-Version-VersionInterval**: All 21 tests in `VersionIntervalTests.cs` diff --git a/docs/verification/build-mark/version/version-semantic.md b/docs/verification/build-mark/version/version-semantic.md new file mode 100644 index 00000000..62bd147f --- /dev/null +++ b/docs/verification/build-mark/version/version-semantic.md @@ -0,0 +1,118 @@ +# VersionSemantic + +## Verification Approach + +`VersionSemantic` is tested through `VersionSemanticTests.cs`, which contains 12 unit +tests. The tests cover creation with and without build metadata, property delegation +to the underlying `VersionComparable`, string formatting, and comparison. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### VersionSemantic_Create_WithBuildMetadata_ReturnsInstance + +**Scenario**: `VersionSemantic.Create` is called with a version string that includes +build metadata (the `+` suffix). + +**Expected**: Returns a non-null instance with metadata set. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Create_WithoutBuildMetadata_ReturnsInstance + +**Scenario**: `VersionSemantic.Create` is called with a version string that has no +build metadata. + +**Expected**: Returns a non-null instance with metadata as empty string. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Properties_DelegateToComparable_Correctly + +**Scenario**: Major, minor, patch, and pre-release properties are accessed on a +`VersionSemantic` instance. + +**Expected**: Values match those of the underlying `VersionComparable`. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_ToString_FormatsCompletely_WithAllComponents + +**Scenario**: `ToString` is called on a `VersionSemantic` with pre-release and metadata. + +**Expected**: Returns a string in `major.minor.patch-pre+meta` format. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_PreRelease_ReturnsEmptyStringForRelease + +**Scenario**: `PreRelease` property is accessed on a release version (no `-` suffix). + +**Expected**: Returns empty string. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Parse_ValidSemanticVersions_ParsesCorrectly + +**Scenario**: A series of standard semver strings are parsed. + +**Expected**: Each parses without error with correct field values. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Create_SimpleVersion_ParsesVersion + +**Scenario**: `VersionSemantic.Create` is called with a simple `major.minor.patch`. + +**Expected**: Instance created with zero pre-release and metadata. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Create_VersionWithMetadata_ParsesVersion + +**Scenario**: `VersionSemantic.Create` is called with version plus build metadata. + +**Expected**: Metadata property returns the metadata string. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Create_PreReleaseWithMetadata_ParsesVersion + +**Scenario**: `VersionSemantic.Create` is called with pre-release and metadata. + +**Expected**: Both pre-release and metadata properties are set correctly. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_TryCreate_InvalidVersion_ReturnsNull + +**Scenario**: `VersionSemantic.TryCreate` is called with an invalid string. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Create_InvalidVersion_ThrowsArgumentException + +**Scenario**: `VersionSemantic.Create` is called with an invalid string. + +**Expected**: `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionSemantic_Comparable_AllowsComparison + +**Scenario**: Two `VersionSemantic` instances are compared using comparison operators. + +**Expected**: Comparison delegates to the underlying `VersionComparable` correctly. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +## Requirements Coverage + +- **BuildMark-Version-VersionSemantic**: All 12 tests in `VersionSemanticTests.cs` diff --git a/docs/verification/build-mark/version/version-tag.md b/docs/verification/build-mark/version/version-tag.md new file mode 100644 index 00000000..54919803 --- /dev/null +++ b/docs/verification/build-mark/version/version-tag.md @@ -0,0 +1,177 @@ +# VersionTag + +## Verification Approach + +`VersionTag` is tested through `VersionTagTests.cs`, which contains 19 unit tests. +The tests cover parsing of standard tags, prefixed tags (e.g., `v1.2.3`), path-prefix +tags (e.g., `mylib/1.2.3`), pre-release normalization (dots converted to hyphens), +and error handling for invalid tags. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### VersionTag_Create_ValidTag_ReturnsVersionTag + +**Scenario**: `VersionTag.Create` is called with a valid tag string. + +**Expected**: Returns a non-null `VersionTag` instance. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_StandardTag_ParsesCorrectly + +**Scenario**: `VersionTag.Create` is called with a standard `v1.2.3` tag. + +**Expected**: Version components are parsed correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_PrefixedTag_ParsesCorrectly + +**Scenario**: `VersionTag.Create` is called with a prefixed tag like `v1.2.3`. + +**Expected**: `v` prefix is stripped; version parses correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_DotSeparatedPreRelease_NormalizesToHyphen + +**Scenario**: `VersionTag.Create` is called with a tag using dots in the pre-release +segment. + +**Expected**: Dots in the pre-release are normalized to hyphens. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_ComplexTag_ExtractsVersionCorrectly + +**Scenario**: `VersionTag.Create` is called with a complex tag containing prefix, +version, pre-release, and build metadata. + +**Expected**: All components are extracted correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Properties_ExposeOriginalAndParsed_Correctly + +**Scenario**: `Tag` and `Semantic` properties are accessed after creating a tag. + +**Expected**: `Tag` returns the original string; `Semantic` returns the parsed version. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_ToString_ReturnsOriginalTag + +**Scenario**: `ToString` is called on a `VersionTag` instance. + +**Expected**: Returns the original tag string unchanged. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_SimpleVPrefix_ParsesVersion + +**Scenario**: `VersionTag.Create` is called with `v1.0.0`. + +**Expected**: Instance created with version `1.0.0`. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_ComplexVersionWithMetadata_ParsesVersion + +**Scenario**: `VersionTag.Create` is called with a tag including build metadata. + +**Expected**: Metadata is preserved in the `Semantic` property. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_TryCreate_InvalidTag_ReturnsNull + +**Scenario**: `VersionTag.TryCreate` is called with an unparseable tag string. + +**Expected**: Returns `null`. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_InvalidTag_ThrowsArgumentException + +**Scenario**: `VersionTag.Create` is called with an unparseable tag string. + +**Expected**: `ArgumentException` is thrown. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_NoPrefix_ParsesVersion + +**Scenario**: `VersionTag.Create` is called with a tag that has no alphabetic prefix. + +**Expected**: Version is parsed directly from the string. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_HyphenPreReleaseWithMetadata_ParsesVersion + +**Scenario**: `VersionTag.Create` is called with a tag using hyphen pre-release and +build metadata. + +**Expected**: Both pre-release and metadata are parsed correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Semantic_AllowsComparison + +**Scenario**: `Semantic` property is used to compare two tags. + +**Expected**: Comparison behaves correctly via the underlying `VersionSemantic`. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionComparable_Equals_DifferentPrefixesSameVersion_ReturnsTrue + +**Scenario**: Two tags with different prefixes but the same version are compared +via `VersionComparable`. + +**Expected**: Comparable values are equal. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_GetVersionComparable_SemanticTags_ReturnsCorrectComparison + +**Scenario**: `VersionComparable` extracted from two semantic tags is compared. + +**Expected**: Returns the correct ordering. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_PathSeparatorPrefix_ParsesCorrectly + +**Scenario**: `VersionTag.Create` is called with a tag in `prefix/1.2.3` format. + +**Expected**: Path prefix is stripped; version is parsed correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_PathSeparatorPrefixWithPreRelease_ParsesCorrectly + +**Scenario**: `VersionTag.Create` is called with `prefix/1.2.3-alpha`. + +**Expected**: Path prefix is stripped; pre-release is parsed correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionTag_Create_MultiLevelPathPrefix_ParsesCorrectly + +**Scenario**: `VersionTag.Create` is called with a multi-level path prefix like +`a/b/1.2.3`. + +**Expected**: All prefix segments are stripped; version parses correctly. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +## Requirements Coverage + +- **BuildMark-Version-VersionTag**: All 19 tests in `VersionTagTests.cs` diff --git a/docs/verification/build-mark/version/version.md b/docs/verification/build-mark/version/version.md new file mode 100644 index 00000000..bd5d0696 --- /dev/null +++ b/docs/verification/build-mark/version/version.md @@ -0,0 +1,97 @@ +# Version + +## Verification Approach + +The Version subsystem is verified through `VersionTests.cs` (subsystem integration +tests), plus dedicated unit test files for each version class. The subsystem tests +exercise the interaction between version types - creating `VersionTag` instances, +extracting `VersionComparable`, and comparing via `VersionInterval`. The unit tests +are described in the individual unit chapters. + +## Dependencies + +| Mock / Stub | Reason | +| ----------- | ---------- | +| None | Pure logic | + +## Test Scenarios + +### VersionComparable_Create_ValidVersions_ReturnsVersionComparable + +**Scenario**: `VersionComparable.Create` is called with a valid version string. + +**Expected**: Returns a non-null `VersionComparable` instance. + +**Requirement coverage**: `BuildMark-Version-VersionComparable` + +### VersionSemantic_Create_ValidSemanticVersion_ReturnsVersionSemantic + +**Scenario**: `VersionSemantic.Create` is called with a valid semantic version string. + +**Expected**: Returns a non-null `VersionSemantic` instance. + +**Requirement coverage**: `BuildMark-Version-VersionSemantic` + +### VersionTag_Create_ValidTag_ReturnsVersionTag + +**Scenario**: `VersionTag.Create` is called with a valid tag string. + +**Expected**: Returns a non-null `VersionTag` instance. + +**Requirement coverage**: `BuildMark-Version-VersionTag` + +### VersionInterval_Create_ValidInterval_ReturnsVersionInterval + +**Scenario**: `VersionInterval.Parse` is called with a valid interval string. + +**Expected**: Returns a non-null `VersionInterval` instance. + +**Requirement coverage**: `BuildMark-Version-VersionInterval` + +### VersionCommitTag_Constructor_ValidParameters_CreatesInstance + +**Scenario**: `VersionCommitTag` is constructed with a valid tag and commit hash. + +**Expected**: Instance is created with the provided tag and commit hash properties set. + +**Requirement coverage**: `BuildMark-Version-VersionCommitTag` + +### Version_Subsystem_CreateAllVersionTypes_WorksCorrectly + +**Scenario**: All version type factory methods are invoked in sequence. + +**Expected**: All instances are created without error. + +**Requirement coverage**: `BuildMark-Version-VersionComparable`, `BuildMark-Version-VersionSemantic`, +`BuildMark-Version-VersionTag` + +### Version_Subsystem_SemanticVersioningCompliance_WorksCorrectly + +**Scenario**: Version strings from the semver specification are parsed and compared. + +**Expected**: Ordering follows the semver specification. + +**Requirement coverage**: `BuildMark-Version-VersionComparable`, `BuildMark-Version-VersionSemantic` + +### Version_Subsystem_TagToComparableIntegration_WorksCorrectly + +**Scenario**: A `VersionTag` is created and its comparable representation is extracted. + +**Expected**: The `VersionComparable` extracted from the tag matches the expected version. + +**Requirement coverage**: `BuildMark-Version-VersionTag`, `BuildMark-Version-VersionComparable` + +## Requirements Coverage + +- **BuildMark-Version-VersionComparable**: VersionComparable_Create_ValidVersions_ReturnsVersionComparable, + Version_Subsystem_CreateAllVersionTypes_WorksCorrectly, + Version_Subsystem_SemanticVersioningCompliance_WorksCorrectly, + Version_Subsystem_TagToComparableIntegration_WorksCorrectly +- **BuildMark-Version-VersionSemantic**: VersionSemantic_Create_ValidSemanticVersion_ReturnsVersionSemantic, + Version_Subsystem_CreateAllVersionTypes_WorksCorrectly, + Version_Subsystem_SemanticVersioningCompliance_WorksCorrectly +- **BuildMark-Version-VersionTag**: VersionTag_Create_ValidTag_ReturnsVersionTag, + Version_Subsystem_CreateAllVersionTypes_WorksCorrectly, + Version_Subsystem_TagToComparableIntegration_WorksCorrectly +- **BuildMark-Version-VersionInterval**: VersionInterval_Create_ValidInterval_ReturnsVersionInterval +- **BuildMark-Version-VersionCommitTag**: VersionCommitTag_Constructor_ValidParameters_CreatesInstance diff --git a/docs/verification/definition.yaml b/docs/verification/definition.yaml new file mode 100644 index 00000000..be106081 --- /dev/null +++ b/docs/verification/definition.yaml @@ -0,0 +1,83 @@ +--- +resource-path: + - docs/verification + - docs/verification/build-mark + - docs/verification/build-mark/cli + - docs/verification/build-mark/build-notes + - docs/verification/build-mark/self-test + - docs/verification/build-mark/utilities + - docs/verification/build-mark/version + - docs/verification/build-mark/configuration + - docs/verification/build-mark/repo-connectors + - docs/verification/build-mark/repo-connectors/github + - docs/verification/build-mark/repo-connectors/azure-dev-ops + - docs/verification/build-mark/repo-connectors/mock + - docs/verification/ots + - docs/template + +input-files: + - docs/verification/title.txt + - docs/verification/introduction.md + - docs/verification/build-mark/build-mark.md + - docs/verification/build-mark/program.md + - docs/verification/build-mark/cli/cli.md + - docs/verification/build-mark/cli/context.md + - docs/verification/build-mark/build-notes/build-notes.md + - docs/verification/build-mark/build-notes/build-information.md + - docs/verification/build-mark/build-notes/item-info.md + - docs/verification/build-mark/build-notes/web-link.md + - docs/verification/build-mark/self-test/self-test.md + - docs/verification/build-mark/self-test/validation.md + - docs/verification/build-mark/utilities/utilities.md + - docs/verification/build-mark/utilities/path-helpers.md + - docs/verification/build-mark/utilities/process-runner.md + - docs/verification/build-mark/version/version.md + - docs/verification/build-mark/version/version-comparable.md + - docs/verification/build-mark/version/version-semantic.md + - docs/verification/build-mark/version/version-tag.md + - docs/verification/build-mark/version/version-interval.md + - docs/verification/build-mark/version/version-interval-set.md + - docs/verification/build-mark/version/version-commit-tag.md + - docs/verification/build-mark/configuration/configuration.md + - docs/verification/build-mark/configuration/build-mark-config.md + - docs/verification/build-mark/configuration/build-mark-config-reader.md + - docs/verification/build-mark/configuration/configuration-load-result.md + - docs/verification/build-mark/configuration/configuration-issue.md + - docs/verification/build-mark/configuration/connector-config.md + - docs/verification/build-mark/configuration/git-hub-connector-config.md + - docs/verification/build-mark/configuration/azure-dev-ops-connector-config.md + - docs/verification/build-mark/configuration/report-config.md + - docs/verification/build-mark/configuration/section-config.md + - docs/verification/build-mark/configuration/rule-config.md + - docs/verification/build-mark/configuration/rule-match-config.md + - docs/verification/build-mark/repo-connectors/repo-connectors.md + - docs/verification/build-mark/repo-connectors/i-repo-connector.md + - docs/verification/build-mark/repo-connectors/repo-connector-base.md + - docs/verification/build-mark/repo-connectors/repo-connector-factory.md + - docs/verification/build-mark/repo-connectors/item-router.md + - docs/verification/build-mark/repo-connectors/item-controls-info.md + - docs/verification/build-mark/repo-connectors/item-controls-parser.md + - docs/verification/build-mark/repo-connectors/github/github.md + - docs/verification/build-mark/repo-connectors/github/git-hub-repo-connector.md + - docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-client.md + - docs/verification/build-mark/repo-connectors/github/git-hub-graph-ql-types.md + - docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops.md + - docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-repo-connector.md + - docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-rest-client.md + - docs/verification/build-mark/repo-connectors/azure-dev-ops/azure-dev-ops-api-types.md + - docs/verification/build-mark/repo-connectors/azure-dev-ops/work-item-mapper.md + - docs/verification/build-mark/repo-connectors/mock/mock.md + - docs/verification/build-mark/repo-connectors/mock/mock-repo-connector.md + - docs/verification/ots/buildmark.md + - docs/verification/ots/fileassert.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/versionmark.md + - docs/verification/ots/weasyprint.md + - docs/verification/ots/xunit.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 00000000..ddc07bea --- /dev/null +++ b/docs/verification/introduction.md @@ -0,0 +1,142 @@ +# Introduction + +This document provides the verification design for BuildMark, a .NET command-line tool +that generates markdown build notes from Git repository metadata, including GitHub +and Azure DevOps repositories. + +## Purpose + +The purpose of this document is to describe how each software requirement for BuildMark +is verified. For each unit, subsystem, and OTS component it identifies the test class, +test methods, mock or stub dependencies, and the requirement identifiers that each test +satisfies. The document provides a traceable record of verification coverage that +supports formal code review, compliance audit, and ongoing maintenance. + +## Scope + +This document covers the verification design for the complete BuildMark system, +including all in-house subsystems and units and all Off-The-Shelf (OTS) components. + +In-house software items verified in this document: + +- **Program** - entry point and execution orchestrator +- **Cli** subsystem - `Context` unit (command-line argument parser and I/O owner) +- **BuildNotes** subsystem - `BuildInformation`, `ItemInfo`, and `WebLink` units +- **SelfTest** subsystem - `Validation` unit +- **Utilities** subsystem - `PathHelpers` and `ProcessRunner` units +- **Version** subsystem - `VersionComparable`, `VersionSemantic`, `VersionTag`, + `VersionInterval`, `VersionIntervalSet`, and `VersionCommitTag` units +- **Configuration** subsystem - `BuildMarkConfig`, `BuildMarkConfigReader`, + `ConfigurationLoadResult`, `ConfigurationIssue`, `ConnectorConfig`, + `GitHubConnectorConfig`, `AzureDevOpsConnectorConfig`, `ReportConfig`, + `SectionConfig`, `RuleConfig`, and `RuleMatchConfig` units +- **RepoConnectors** subsystem - `IRepoConnector`, `RepoConnectorBase`, + `RepoConnectorFactory`, `ItemRouter`, `ItemControlsInfo`, and `ItemControlsParser` + units, plus the following sub-subsystems: + - **GitHub** sub-subsystem - `GitHubRepoConnector`, `GitHubGraphQLClient`, + and `GitHubGraphQLTypes` units + - **AzureDevOps** sub-subsystem - `AzureDevOpsRepoConnector`, + `AzureDevOpsRestClient`, `AzureDevOpsApiTypes`, and `WorkItemMapper` units + - **Mock** sub-subsystem - `MockRepoConnector` unit + +OTS components verified in this document: + +- **BuildMark** - build notes generation tool (self-referential) +- **FileAssert** - file content assertion tool +- **Pandoc** - document conversion tool +- **ReqStream** - requirements traceability tool +- **ReviewMark** - code review enforcement tool +- **SarifMark** - SARIF report generation tool +- **SonarMark** - SonarCloud report generation tool +- **VersionMark** - tool version capture tool +- **WeasyPrint** - HTML-to-PDF renderer +- **xUnit** - unit testing framework + +The following topics are out of scope: + +- External library internals not listed above +- Build pipeline configuration beyond the steps referenced as evidence +- Deployment and packaging + +## Software Structure + +The following tree shows how the BuildMark software items are organized across the +system, subsystem, and unit levels: + +```text +BuildMark (System) +├── Program (Unit) +├── Cli (Subsystem) +│ └── Context (Unit) +├── BuildNotes (Subsystem) +│ ├── BuildInformation (Unit) +│ ├── ItemInfo (Unit) +│ └── WebLink (Unit) +├── SelfTest (Subsystem) +│ └── Validation (Unit) +├── Utilities (Subsystem) +│ ├── PathHelpers (Unit) +│ └── ProcessRunner (Unit) +├── Version (Subsystem) +│ ├── VersionComparable (Unit) +│ ├── VersionSemantic (Unit) +│ ├── VersionTag (Unit) +│ ├── VersionInterval (Unit) +│ ├── VersionIntervalSet (Unit) +│ └── VersionCommitTag (Unit) +├── Configuration (Subsystem) +│ ├── BuildMarkConfig (Unit) +│ ├── BuildMarkConfigReader (Unit) +│ ├── ConfigurationLoadResult (Unit) +│ ├── ConfigurationIssue (Unit) +│ ├── ConnectorConfig (Unit) +│ ├── GitHubConnectorConfig (Unit) +│ ├── AzureDevOpsConnectorConfig (Unit) +│ ├── ReportConfig (Unit) +│ ├── SectionConfig (Unit) +│ ├── RuleConfig (Unit) +│ └── RuleMatchConfig (Unit) +└── RepoConnectors (Subsystem) + ├── IRepoConnector (Unit) + ├── RepoConnectorBase (Unit) + ├── RepoConnectorFactory (Unit) + ├── ItemRouter (Unit) + ├── ItemControlsInfo (Unit) + ├── ItemControlsParser (Unit) + ├── GitHub (Subsystem) + │ ├── GitHubRepoConnector (Unit) + │ ├── GitHubGraphQLClient (Unit) + │ └── GitHubGraphQLTypes (Unit) + ├── AzureDevOps (Subsystem) + │ ├── AzureDevOpsRepoConnector (Unit) + │ ├── AzureDevOpsRestClient (Unit) + │ ├── AzureDevOpsApiTypes (Unit) + │ └── WorkItemMapper (Unit) + └── Mock (Subsystem) + └── MockRepoConnector (Unit) +``` + +## Companion Artifact Structure + +Verification design documents are companion artifacts to requirements, design, source +code, and tests. The parallel tree below shows how each artifact type maps to the same +software structure: + +```text +docs/requirements_doc/ - compiled requirements document (generated) +docs/reqstream/ - requirements source YAML files +docs/design/ - software design document source +docs/verification/ - this document (verification design source) +src/DemaConsulting.BuildMark/ - implementation source +test/DemaConsulting.BuildMark.Tests/ - test source +``` + +Each chapter in this verification document corresponds to a unit or subsystem chapter +in the design document. Requirement IDs referenced in the Requirements Coverage sections +match identifiers defined in the ReqStream YAML files under `docs/reqstream/`. + +## References + +- See the *BuildMark Software Design* document for implementation details of each unit. +- See the *BuildMark Requirements* document for the full requirements specification. +- [BuildMark Repository](https://github.com/demaconsulting/BuildMark) diff --git a/docs/verification/ots/buildmark.md b/docs/verification/ots/buildmark.md new file mode 100644 index 00000000..4420770d --- /dev/null +++ b/docs/verification/ots/buildmark.md @@ -0,0 +1,24 @@ +# BuildMark + +## Verification Approach + +BuildMark is an OTS tool. Verification is achieved through the tool's built-in +self-validation (`--validate`) executed in the CI pipeline. The self-validation +runs all internal checks and writes results to a TRX file. Successful completion +of the self-validation step constitutes evidence that the tool is functioning +correctly in the build environment. + +## Evidence + +The CI pipeline step `Run BuildMark self-validation` executes: + +```bash +dotnet buildmark --validate --results artifacts/buildmark-self-validation.trx +``` + +The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-BuildMark**: CI pipeline self-validation TRX evidence from + `artifacts/buildmark-self-validation.trx` diff --git a/docs/verification/ots/fileassert.md b/docs/verification/ots/fileassert.md new file mode 100644 index 00000000..7dae649c --- /dev/null +++ b/docs/verification/ots/fileassert.md @@ -0,0 +1,29 @@ +# FileAssert + +## Verification Approach + +FileAssert is an OTS tool. Verification is achieved through the tool's built-in +self-validation (`--validate`) executed in the CI pipeline. The self-validation +runs all internal checks and writes results to a TRX file. Successful completion +of the self-validation step constitutes evidence that the tool is functioning +correctly in the build environment. + +Additionally, FileAssert is exercised indirectly by asserting the content of each +generated document (Build Notes, Code Quality, Review Plan, Review Report, Design, +User Guide, and Verification). Each successful assertion confirms that FileAssert +is able to inspect and validate file content as required. + +## Evidence + +The CI pipeline step `Run FileAssert self-validation` executes: + +```bash +dotnet fileassert --validate --results artifacts/fileassert-self-validation.trx +``` + +The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-FileAssert**: CI pipeline self-validation TRX evidence from + `artifacts/fileassert-self-validation.trx` diff --git a/docs/verification/ots/pandoc.md b/docs/verification/ots/pandoc.md new file mode 100644 index 00000000..567279be --- /dev/null +++ b/docs/verification/ots/pandoc.md @@ -0,0 +1,29 @@ +# Pandoc + +## Verification Approach + +Pandoc is an OTS document conversion tool. Verification is achieved through +repeated document generation in the CI pipeline. Pandoc converts markdown source +files to HTML for each document collection (Build Notes, Code Quality, Review Plan, +Review Report, Design, User Guide, and Verification). FileAssert then validates that +each generated HTML file contains expected content, providing evidence of correct +Pandoc operation. + +## Evidence + +The CI pipeline generates HTML output using Pandoc for each document section, and +FileAssert validates the output. For example, for the Design document: + +```bash +dotnet pandoc --defaults docs/design/definition.yaml ... --output docs/design/generated/design.html +dotnet fileassert --results artifacts/fileassert-design.trx design +``` + +FileAssert TRX files (`fileassert-build-notes.trx`, `fileassert-code-quality.trx`, +`fileassert-code-review.trx`, `fileassert-design.trx`, `fileassert-user-guide.trx`, +`fileassert-verification.trx`) are consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-Pandoc**: CI pipeline document generation evidence from multiple + FileAssert TRX results confirming successful HTML conversion diff --git a/docs/verification/ots/reqstream.md b/docs/verification/ots/reqstream.md new file mode 100644 index 00000000..5e958fcd --- /dev/null +++ b/docs/verification/ots/reqstream.md @@ -0,0 +1,29 @@ +# ReqStream + +## Verification Approach + +ReqStream is an OTS requirements traceability tool. Verification is achieved through +the tool's built-in self-validation (`--validate`) executed in the CI pipeline. The +self-validation runs all internal checks and writes results to a TRX file. Successful +completion of the self-validation step constitutes evidence that the tool is functioning +correctly in the build environment. + +Additionally, ReqStream is exercised operationally when it processes the project's +requirements YAML files against TRX evidence and enforces that all requirements are +satisfied (`--enforce`). Successful execution of ReqStream in enforcement mode provides +further evidence of correct operation. + +## Evidence + +The CI pipeline step `Run ReqStream self-validation` executes: + +```bash +dotnet reqstream --validate --results artifacts/reqstream-self-validation.trx +``` + +The resulting TRX file is consumed by ReqStream itself to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-ReqStream**: CI pipeline self-validation TRX evidence from + `artifacts/reqstream-self-validation.trx` diff --git a/docs/verification/ots/reviewmark.md b/docs/verification/ots/reviewmark.md new file mode 100644 index 00000000..d65c470d --- /dev/null +++ b/docs/verification/ots/reviewmark.md @@ -0,0 +1,28 @@ +# ReviewMark + +## Verification Approach + +ReviewMark is an OTS code review enforcement tool. Verification is achieved through +the tool's built-in self-validation (`--validate`) executed in the CI pipeline. The +self-validation runs all internal checks and writes results to a TRX file. Successful +completion of the self-validation step constitutes evidence that the tool is functioning +correctly in the build environment. + +Additionally, ReviewMark is exercised operationally when it generates the Review Plan +and Review Report documents from the `.reviewmark.yaml` configuration. Successful +document generation provides further evidence of correct operation. + +## Evidence + +The CI pipeline step `Run ReviewMark self-validation` executes: + +```bash +dotnet reviewmark --validate --results artifacts/reviewmark-self-validation.trx +``` + +The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-ReviewMark**: CI pipeline self-validation TRX evidence from + `artifacts/reviewmark-self-validation.trx` diff --git a/docs/verification/ots/sarifmark.md b/docs/verification/ots/sarifmark.md new file mode 100644 index 00000000..ef55ba5f --- /dev/null +++ b/docs/verification/ots/sarifmark.md @@ -0,0 +1,28 @@ +# SarifMark + +## Verification Approach + +SarifMark is an OTS SARIF report generation tool. Verification is achieved through +the tool's built-in self-validation (`--validate`) executed in the CI pipeline. The +self-validation runs all internal checks and writes results to a TRX file. Successful +completion of the self-validation step constitutes evidence that the tool is functioning +correctly in the build environment. + +Additionally, SarifMark is exercised operationally when it processes the CodeQL SARIF +output and generates the CodeQL quality report markdown. Successful report generation +provides further evidence of correct operation. + +## Evidence + +The CI pipeline step `Run SarifMark self-validation` executes: + +```bash +dotnet sarifmark --validate --results artifacts/sarifmark-self-validation.trx +``` + +The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-SarifMark**: CI pipeline self-validation TRX evidence from + `artifacts/sarifmark-self-validation.trx` diff --git a/docs/verification/ots/sonarmark.md b/docs/verification/ots/sonarmark.md new file mode 100644 index 00000000..3f88abba --- /dev/null +++ b/docs/verification/ots/sonarmark.md @@ -0,0 +1,28 @@ +# SonarMark + +## Verification Approach + +SonarMark is an OTS SonarCloud quality report generation tool. Verification is achieved +through the tool's built-in self-validation (`--validate`) executed in the CI pipeline. +The self-validation runs all internal checks and writes results to a TRX file. Successful +completion of the self-validation step constitutes evidence that the tool is functioning +correctly in the build environment. + +Additionally, SonarMark is exercised operationally when it queries the SonarCloud API +and generates the SonarCloud quality report markdown. Successful report generation +provides further evidence of correct operation. + +## Evidence + +The CI pipeline step `Run SonarMark self-validation` executes: + +```bash +dotnet sonarmark --validate --results artifacts/sonarmark-self-validation.trx +``` + +The resulting TRX file is consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-SonarMark**: CI pipeline self-validation TRX evidence from + `artifacts/sonarmark-self-validation.trx` diff --git a/docs/verification/ots/versionmark.md b/docs/verification/ots/versionmark.md new file mode 100644 index 00000000..e9500be2 --- /dev/null +++ b/docs/verification/ots/versionmark.md @@ -0,0 +1,30 @@ +# VersionMark + +## Verification Approach + +VersionMark is an OTS tool version capture tool. Verification is achieved through the +tool's built-in self-validation (`--validate`) executed in the CI pipeline across +multiple jobs. The self-validation runs all internal checks and writes results to TRX +files. Successful completion of the self-validation steps constitutes evidence that the +tool is functioning correctly across all build environments. + +Additionally, VersionMark is exercised operationally in every job to capture and publish +tool version information. Successful version capture and publication provides further +evidence of correct operation. + +## Evidence + +The CI pipeline runs VersionMark self-validation in multiple jobs: + +```bash +dotnet versionmark --validate --results artifacts/versionmark-self-validation-quality.trx +dotnet versionmark --validate --results artifacts/versionmark-self-validation.trx +``` + +The resulting TRX files are consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-VersionMark**: CI pipeline self-validation TRX evidence from + `artifacts/versionmark-self-validation.trx` and + `artifacts/versionmark-self-validation-quality.trx` diff --git a/docs/verification/ots/weasyprint.md b/docs/verification/ots/weasyprint.md new file mode 100644 index 00000000..be4eba56 --- /dev/null +++ b/docs/verification/ots/weasyprint.md @@ -0,0 +1,30 @@ +# WeasyPrint + +## Verification Approach + +WeasyPrint is an OTS HTML-to-PDF rendering tool. Verification is achieved through +repeated document generation in the CI pipeline. WeasyPrint converts HTML files to PDF +for each document collection (Build Notes, Code Quality, Review Plan, Review Report, +Design, User Guide, and Verification). FileAssert then validates that each generated +PDF file contains expected content and metadata, providing evidence of correct WeasyPrint +operation. + +## Evidence + +The CI pipeline generates PDF output using WeasyPrint for each document section, and +FileAssert validates the output. For example, for the Design document: + +```bash +dotnet weasyprint --pdf-variant pdf/a-3u docs/design/generated/design.html \ + "docs/generated/BuildMark Software Design.pdf" +dotnet fileassert --results artifacts/fileassert-design.trx design +``` + +FileAssert TRX files (`fileassert-build-notes.trx`, `fileassert-code-quality.trx`, +`fileassert-code-review.trx`, `fileassert-design.trx`, `fileassert-user-guide.trx`, +`fileassert-verification.trx`) are consumed by ReqStream to satisfy the OTS requirement. + +## Requirements Coverage + +- **BuildMark-OTS-WeasyPrint**: CI pipeline document generation evidence from multiple + FileAssert TRX results confirming successful PDF rendering diff --git a/docs/verification/ots/xunit.md b/docs/verification/ots/xunit.md new file mode 100644 index 00000000..e5e57b87 --- /dev/null +++ b/docs/verification/ots/xunit.md @@ -0,0 +1,29 @@ +# xUnit + +## Verification Approach + +xUnit is an OTS unit testing framework. Verification is achieved through the framework's +execution of all project unit tests in the CI pipeline. xUnit discovers and runs all test +methods in `DemaConsulting.BuildMark.Tests`, writes TRX result files, and reports test +outcomes. Successful test execution across all supported operating systems and .NET +runtime versions constitutes evidence that xUnit is functioning correctly. + +## Evidence + +The CI pipeline step `Test` executes: + +```bash +dotnet test --no-build --configuration Release \ + --collect "XPlat Code Coverage;Format=opencover" \ + --logger "trx;LogFilePrefix=" \ + --results-directory artifacts +``` + +The resulting TRX files are consumed by ReqStream to satisfy unit test requirements. +The matrix of operating systems (Windows, Ubuntu, macOS) and .NET versions (8, 9, 10) +provides broad platform coverage evidence. + +## Requirements Coverage + +- **BuildMark-OTS-xUnit**: CI pipeline test execution TRX evidence confirming that + xUnit discovers and runs tests on all supported platforms diff --git a/docs/verification/title.txt b/docs/verification/title.txt new file mode 100644 index 00000000..374bcef7 --- /dev/null +++ b/docs/verification/title.txt @@ -0,0 +1,13 @@ +--- +title: BuildMark Verification Design Document +subtitle: Build Notes Generation Tool +author: DEMA Consulting +description: Verification design document for BuildMark +lang: en-US +keywords: + - BuildMark + - .NET + - Command-Line Tool + - Verification + - Verification Design Document +--- diff --git a/requirements.yaml b/requirements.yaml index c5984c5f..80b1c9e9 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -21,10 +21,16 @@ includes: - docs/reqstream/build-mark/repo-connectors/item-controls-parser.yaml - docs/reqstream/build-mark/repo-connectors/repo-connector-factory.yaml - docs/reqstream/build-mark/repo-connectors/github/github-repo-connector.yaml + - docs/reqstream/build-mark/repo-connectors/github/github.yaml + - docs/reqstream/build-mark/repo-connectors/github/github-graphql-client.yaml - docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-repo-connector.yaml + - docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops.yaml + - docs/reqstream/build-mark/repo-connectors/azure-devops/azure-devops-rest-client.yaml + - docs/reqstream/build-mark/repo-connectors/azure-devops/work-item-mapper.yaml - docs/reqstream/build-mark/repo-connectors/mock/mock-repo-connector.yaml + - docs/reqstream/build-mark/repo-connectors/mock/mock.yaml - docs/reqstream/build-mark/configuration/configuration.yaml - - docs/reqstream/ots/mstest.yaml + - docs/reqstream/ots/xunit.yaml - docs/reqstream/ots/reqstream.yaml - docs/reqstream/ots/buildmark.yaml - docs/reqstream/ots/versionmark.yaml diff --git a/src/DemaConsulting.BuildMark/Program.cs b/src/DemaConsulting.BuildMark/Program.cs index bcd2b30c..71d493dc 100644 --- a/src/DemaConsulting.BuildMark/Program.cs +++ b/src/DemaConsulting.BuildMark/Program.cs @@ -55,6 +55,13 @@ public static string Version /// /// Command-line arguments. /// Exit code: 0 for success, non-zero for failure. + /// + /// Catches and + /// as expected failure modes, writing the message to and + /// returning exit code 1. Any other exception type is re-thrown after logging so that + /// the runtime can generate event logs for unexpected failures. + /// The method itself runs on the main thread; no async execution occurs at this level. + /// private static int Main(string[] args) { try @@ -167,6 +174,16 @@ private static void PrintHelp(Context context) /// Processes build notes and generates markdown output. /// /// The context containing command line arguments and program state. + /// + /// This method is called on the UI thread and blocks until the connector completes. + /// Side effects include: writing build summary lines to the context output, writing + /// the markdown report file to effectiveReportFile, and setting + /// to 1 on any error. + /// When is non-null, configuration-based + /// routing is not applied (test isolation path). When it is null, the connector + /// is configured via + /// before use. + /// private static void ProcessBuildNotes(Context context) { // Load the optional configuration before attempting report generation. @@ -260,6 +277,12 @@ private static void ProcessBuildNotes(Context context) /// Loads the optional repository configuration. /// /// The configuration load result. + /// + /// Uses a sync-over-async pattern (GetAwaiter().GetResult()) to call the + /// async method. This is safe because + /// the method is always called from the UI/main thread, which does not hold a + /// synchronization context that could cause a deadlock. + /// private static ConfigurationLoadResult LoadConfiguration() { return BuildMarkConfigReader.ReadAsync(Environment.CurrentDirectory).GetAwaiter().GetResult(); diff --git a/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs b/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs index a62d2140..15dd94b1 100644 --- a/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs +++ b/test/DemaConsulting.BuildMark.Tests/AssemblyInfo.cs @@ -18,7 +18,9 @@ // 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.BuildMark.Tests/BuildNotes/BuildInformationTests.cs b/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildInformationTests.cs index 3e5e9b3f..2166ca7e 100644 --- a/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildInformationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildInformationTests.cs @@ -31,13 +31,12 @@ namespace DemaConsulting.BuildMark.Tests.BuildNotes; /// /// Tests for the BuildInformation class. /// -[TestClass] public class BuildInformationTests { /// /// Test that GetBuildInformationAsync throws when no version specified and no tags found. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndNoTags() { // Create mock connector that throws for no tags @@ -58,7 +57,7 @@ public async Task BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionA /// /// Test that GetBuildInformationAsync throws when no version specified and current commit doesn't match tag. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionAndCommitDoesNotMatchTag() { // Create mock connector that throws for commit mismatch @@ -79,7 +78,7 @@ public async Task BuildInformation_GetBuildInformationAsync_ThrowsWhenNoVersionA /// /// Test that GetBuildInformationAsync works with explicit version parameter. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_WorksWithExplicitVersion() { // Create build information with explicit version @@ -87,16 +86,16 @@ public async Task BuildInformation_GetBuildInformationAsync_WorksWithExplicitVer var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.1.0")); // Verify version and hashes are set correctly - Assert.AreEqual("v2.1.0", buildInfo.CurrentVersionTag.VersionTag.Tag); // New version preserves input tag - Assert.AreEqual("current123hash456", buildInfo.CurrentVersionTag.CommitHash); - Assert.AreEqual("v2.0.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); - Assert.AreEqual("mno345pqr678", buildInfo.BaselineVersionTag?.CommitHash); + Assert.Equal("v2.1.0", buildInfo.CurrentVersionTag.VersionTag.Tag); // New version preserves input tag + Assert.Equal("current123hash456", buildInfo.CurrentVersionTag.CommitHash); + Assert.Equal("v2.0.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); + Assert.Equal("mno345pqr678", buildInfo.BaselineVersionTag?.CommitHash); } /// /// Test that GetBuildInformationAsync works when current commit matches latest tag. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_WorksWhenCurrentCommitMatchesLatestTag() { // Create mock connector that returns data for matching tag @@ -114,15 +113,15 @@ public async Task BuildInformation_GetBuildInformationAsync_WorksWhenCurrentComm var buildInfo = await connector.GetBuildInformationAsync(); // Assert - Assert.AreEqual("v2.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); - Assert.AreEqual("mno345pqr678", buildInfo.CurrentVersionTag.CommitHash); - Assert.AreEqual("ver-1.1.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); + Assert.Equal("v2.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); + Assert.Equal("mno345pqr678", buildInfo.CurrentVersionTag.CommitHash); + Assert.Equal("ver-1.1.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); } /// /// Test that GetBuildInformationAsync correctly identifies pre-release and uses previous tag. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_PreReleaseUsesPreviousTag() { // Arrange @@ -135,14 +134,14 @@ public async Task BuildInformation_GetBuildInformationAsync_PreReleaseUsesPrevio // Assert // Since "v2.0.0-beta.1" doesn't match any existing repository tag semantically, // it should create a new version using the provided tag - Assert.AreEqual("v2.0.0-beta.1", buildInfo.CurrentVersionTag.VersionTag.Tag); - Assert.AreEqual("v2.0.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); + Assert.Equal("v2.0.0-beta.1", buildInfo.CurrentVersionTag.VersionTag.Tag); + Assert.Equal("v2.0.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); } /// /// Test that GetBuildInformationAsync correctly identifies release and skips pre-releases. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_ReleaseSkipsPreReleases() { // Arrange @@ -152,14 +151,14 @@ public async Task BuildInformation_GetBuildInformationAsync_ReleaseSkipsPreRelea var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - Assert.AreEqual("v2.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); - Assert.AreEqual("ver-1.1.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); + Assert.Equal("v2.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); + Assert.Equal("ver-1.1.0", buildInfo.BaselineVersionTag?.VersionTag.Tag); } /// /// Test that GetBuildInformationAsync collects issues correctly. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_CollectsIssuesCorrectly() { // Create build information for version with issues @@ -167,27 +166,27 @@ public async Task BuildInformation_GetBuildInformationAsync_CollectsIssuesCorrec var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("ver-1.1.0")); // Verify change issues are collected (including PR without issues) - Assert.HasCount(2, buildInfo.Changes); - Assert.AreEqual("1", buildInfo.Changes[0].Id); - Assert.AreEqual("Add feature X", buildInfo.Changes[0].Title); - Assert.AreEqual("https://github.com/example/repo/issues/1", buildInfo.Changes[0].Url); + Assert.Equal(2, buildInfo.Changes.Count); + Assert.Equal("1", buildInfo.Changes[0].Id); + Assert.Equal("Add feature X", buildInfo.Changes[0].Title); + Assert.Equal("https://github.com/example/repo/issues/1", buildInfo.Changes[0].Url); // Second change should be PR #13 (without issues) - Assert.AreEqual("#13", buildInfo.Changes[1].Id); + Assert.Equal("#13", buildInfo.Changes[1].Id); // Verify no bug issues for this version - Assert.IsEmpty(buildInfo.Bugs); + Assert.Empty(buildInfo.Bugs); // Verify known issues include open bugs (issue 5 excluded by affected-versions [5.0.0,)) - Assert.HasCount(2, buildInfo.KnownIssues); - Assert.AreEqual("4", buildInfo.KnownIssues[0].Id); - Assert.AreEqual("6", buildInfo.KnownIssues[1].Id); + Assert.Equal(2, buildInfo.KnownIssues.Count); + Assert.Equal("4", buildInfo.KnownIssues[0].Id); + Assert.Equal("6", buildInfo.KnownIssues[1].Id); } /// /// Test that GetBuildInformationAsync orders changes by Index (PR number). /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex() { // Create build information for version with issues @@ -196,26 +195,24 @@ public async Task BuildInformation_GetBuildInformationAsync_OrdersChangesByIndex // Verify changes are ordered by Index (PR number) // Issue #1 from PR #10 should come before PR #13 - Assert.HasCount(2, buildInfo.Changes); - Assert.AreEqual("1", buildInfo.Changes[0].Id); - Assert.AreEqual(10, buildInfo.Changes[0].Index); - Assert.AreEqual("#13", buildInfo.Changes[1].Id); - Assert.AreEqual(13, buildInfo.Changes[1].Index); + Assert.Equal(2, buildInfo.Changes.Count); + Assert.Equal("1", buildInfo.Changes[0].Id); + Assert.Equal(10, buildInfo.Changes[0].Index); + Assert.Equal("#13", buildInfo.Changes[1].Id); + Assert.Equal(13, buildInfo.Changes[1].Index); // Verify Index values are in ascending order for (var i = 0; i < buildInfo.Changes.Count - 1; i++) { - Assert.IsLessThanOrEqualTo( - buildInfo.Changes[i + 1].Index, - buildInfo.Changes[i].Index, - $"Changes should be ordered by Index. Found {buildInfo.Changes[i].Index} before {buildInfo.Changes[i + 1].Index}"); + Assert.True(buildInfo.Changes[i].Index <= + buildInfo.Changes[i + 1].Index, $"Changes should be ordered by Index. Found {buildInfo.Changes[i].Index} before {buildInfo.Changes[i + 1].Index}"); } } /// /// Test that GetBuildInformationAsync separates bug and change issues. /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_SeparatesBugAndChangeIssues() { // Arrange @@ -225,16 +222,16 @@ public async Task BuildInformation_GetBuildInformationAsync_SeparatesBugAndChang var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - verify bugs and changes are properly separated - Assert.HasCount(1, buildInfo.Changes); - Assert.HasCount(1, buildInfo.Bugs); - Assert.AreEqual("2", buildInfo.Bugs[0].Id); - Assert.AreEqual("Fix bug in Y", buildInfo.Bugs[0].Title); + Assert.Single(buildInfo.Changes); + Assert.Single(buildInfo.Bugs); + Assert.Equal("2", buildInfo.Bugs[0].Id); + Assert.Equal("Fix bug in Y", buildInfo.Bugs[0].Title); } /// /// Test that GetBuildInformationAsync handles first release correctly (no from version). /// - [TestMethod] + [Fact] public async Task BuildInformation_GetBuildInformationAsync_HandlesFirstReleaseCorrectly() { // Arrange @@ -244,14 +241,14 @@ public async Task BuildInformation_GetBuildInformationAsync_HandlesFirstReleaseC var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - verify first release has no previous version - Assert.IsNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("v1.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); + Assert.Null(buildInfo.BaselineVersionTag); + Assert.Equal("v1.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); } /// /// Test that ToMarkdown generates correct markdown with default parameters. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_GeneratesCorrectMarkdownWithDefaults() { // Arrange @@ -274,7 +271,7 @@ public async Task BuildInformation_ToMarkdown_GeneratesCorrectMarkdownWithDefaul /// /// Test that ToMarkdown includes known issues when requested. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_IncludesKnownIssuesWhenRequested() { // Arrange @@ -296,7 +293,7 @@ public async Task BuildInformation_ToMarkdown_IncludesKnownIssuesWhenRequested() /// /// Test that ToMarkdown respects custom heading depth. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_RespectsCustomHeadingDepth() { // Arrange @@ -316,7 +313,7 @@ public async Task BuildInformation_ToMarkdown_RespectsCustomHeadingDepth() /// /// Test that ToMarkdown displays N/A for empty changes table. /// - [TestMethod] + [Fact] public void BuildInformation_ToMarkdown_DisplaysNAForEmptyChanges() { // Arrange - Create build info with no change issues @@ -341,7 +338,7 @@ [new ItemInfo("2", "Bug fix", "https://example.com/2", "bug")], /// /// Test that ToMarkdown displays N/A for empty bugs table. /// - [TestMethod] + [Fact] public void BuildInformation_ToMarkdown_DisplaysNAForEmptyBugs() { // Arrange - Create build info with no bug issues @@ -365,7 +362,7 @@ [new ItemInfo("1", "Feature", "https://example.com/1", "feature")], /// /// Test that ToMarkdown includes issue links in bullet lists. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_IncludesIssueLinks() { // Arrange @@ -383,7 +380,7 @@ public async Task BuildInformation_ToMarkdown_IncludesIssueLinks() /// /// Test that ToMarkdown handles first release with N/A for previous version. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_HandlesFirstReleaseWithNA() { // Arrange @@ -404,7 +401,7 @@ public async Task BuildInformation_ToMarkdown_HandlesFirstReleaseWithNA() /// /// Test that ToMarkdown includes Full Changelog section when link is present. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_IncludesFullChangelogWhenLinkPresent() { // Arrange @@ -424,7 +421,7 @@ public async Task BuildInformation_ToMarkdown_IncludesFullChangelogWhenLinkPrese /// /// Test that ToMarkdown excludes Full Changelog section when no baseline version. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_ExcludesFullChangelogWhenNoBaseline() { // Arrange @@ -441,7 +438,7 @@ public async Task BuildInformation_ToMarkdown_ExcludesFullChangelogWhenNoBaselin /// /// Test that ToMarkdown uses bullet lists for changes, bugs, and known issues. /// - [TestMethod] + [Fact] public async Task BuildInformation_ToMarkdown_UsesBulletLists() { // Arrange @@ -475,7 +472,7 @@ public async Task BuildInformation_ToMarkdown_UsesBulletLists() /// /// Test that VersionCommitTag correctly stores version and hash. /// - [TestMethod] + [Fact] public void VersionCommitTag_Constructor_StoresVersionAndHash() { // Arrange @@ -486,14 +483,14 @@ public void VersionCommitTag_Constructor_StoresVersionAndHash() var versionCommitTag = new VersionCommitTag(version, hash); // Assert - Assert.AreEqual(version, versionCommitTag.VersionTag); - Assert.AreEqual(hash, versionCommitTag.CommitHash); + Assert.Equal(version, versionCommitTag.VersionTag); + Assert.Equal(hash, versionCommitTag.CommitHash); } /// /// Test that WebLink correctly stores text and URL. /// - [TestMethod] + [Fact] public void WebLink_Constructor_StoresTextAndUrl() { // Arrange @@ -504,8 +501,8 @@ public void WebLink_Constructor_StoresTextAndUrl() var webLink = new WebLink(text, url); // Assert - Assert.AreEqual(text, webLink.LinkText); - Assert.AreEqual(url, webLink.TargetUrl); + Assert.Equal(text, webLink.LinkText); + Assert.Equal(url, webLink.TargetUrl); } /// @@ -515,7 +512,7 @@ public void WebLink_Constructor_StoresTextAndUrl() /// What is being tested: BuildInformation.ToMarkdown with RoutedSections /// What the assertions prove: Custom section headings appear and legacy sections do not /// - [TestMethod] + [Fact] public void BuildInformation_ToMarkdown_WithRoutedSections_RendersCustomSections() { // Arrange - Build information with routed sections @@ -536,14 +533,14 @@ public void BuildInformation_ToMarkdown_WithRoutedSections_RendersCustomSections var markdown = buildInfo.ToMarkdown(); // Assert - Custom section headings are present - Assert.Contains("## Features", markdown, "Features heading should be present"); - Assert.Contains("## Bugs", markdown, "Bugs heading should be present"); - Assert.Contains("Add feature X", markdown, "Feature item should be present"); - Assert.Contains("Fix bug Y", markdown, "Bug item should be present"); + Assert.Contains("## Features", markdown); + Assert.Contains("## Bugs", markdown); + Assert.Contains("Add feature X", markdown); + Assert.Contains("Fix bug Y", markdown); // Assert - Legacy sections are not present - Assert.DoesNotContain("## Changes", markdown, "Legacy Changes heading should not be present"); - Assert.DoesNotContain("## Bugs Fixed", markdown, "Legacy Bugs Fixed heading should not be present"); + Assert.DoesNotContain("## Changes", markdown); + Assert.DoesNotContain("## Bugs Fixed", markdown); } /// @@ -553,7 +550,7 @@ public void BuildInformation_ToMarkdown_WithRoutedSections_RendersCustomSections /// What is being tested: BuildInformation.ToMarkdown without RoutedSections /// What the assertions prove: Legacy Changes/Bugs Fixed sections are present /// - [TestMethod] + [Fact] public void BuildInformation_ToMarkdown_WithoutRoutedSections_RendersDefaultSections() { // Arrange - Build information without routed sections (legacy mode) @@ -564,8 +561,8 @@ public void BuildInformation_ToMarkdown_WithoutRoutedSections_RendersDefaultSect var markdown = buildInfo.ToMarkdown(); // Assert - Legacy section headings are present - Assert.Contains("## Changes", markdown, "Legacy Changes heading should be present"); - Assert.Contains("## Bugs Fixed", markdown, "Legacy Bugs Fixed heading should be present"); + Assert.Contains("## Changes", markdown); + Assert.Contains("## Bugs Fixed", markdown); } } diff --git a/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildNotesTests.cs b/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildNotesTests.cs index f8cf0179..31b61734 100644 --- a/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildNotesTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/BuildNotes/BuildNotesTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.BuildNotes; /// /// Subsystem-level tests for the BuildNotes subsystem. /// -[TestClass] public class BuildNotesTests { /// /// Test that the BuildNotes subsystem generates correct markdown from a BuildInformation model. /// - [TestMethod] + [Fact] public async Task BuildNotes_ReportModel_GeneratesCorrectMarkdown() { // Arrange: obtain a BuildInformation model from the mock connector @@ -55,7 +54,7 @@ public async Task BuildNotes_ReportModel_GeneratesCorrectMarkdown() /// /// Test that the BuildNotes subsystem includes known issues in the rendered markdown when requested. /// - [TestMethod] + [Fact] public async Task BuildNotes_ReportModel_IncludesKnownIssues() { // Arrange: obtain a BuildInformation model that has known issues @@ -77,7 +76,7 @@ public async Task BuildNotes_ReportModel_IncludesKnownIssues() /// /// Test that the BuildNotes subsystem includes a full changelog link in the rendered markdown. /// - [TestMethod] + [Fact] public async Task BuildNotes_ReportModel_IncludesFullChangelog() { // Arrange: obtain a BuildInformation model that has a changelog link diff --git a/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs b/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs index b5a2a59b..52e0453b 100644 --- a/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Cli/CliTests.cs @@ -25,74 +25,73 @@ namespace DemaConsulting.BuildMark.Tests.Cli; /// /// Subsystem-level tests for the Cli subsystem. /// -[TestClass] public class CliTests { /// /// Test that the Cli subsystem creates a valid context from empty arguments. /// - [TestMethod] + [Fact] public void Cli_Context_EmptyArguments_CreatesValidContext() { // Arrange & Act: create context with no arguments using var context = Context.Create([]); // Assert: all properties have expected defaults - Assert.IsFalse(context.Version); - Assert.IsFalse(context.Help); - Assert.IsFalse(context.Silent); - Assert.IsFalse(context.Validate); - Assert.IsNull(context.BuildVersion); - Assert.IsNull(context.ReportFile); - Assert.IsNull(context.Depth); - Assert.IsFalse(context.IncludeKnownIssues); - Assert.IsNull(context.ResultsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.False(context.Help); + Assert.False(context.Silent); + Assert.False(context.Validate); + Assert.Null(context.BuildVersion); + Assert.Null(context.ReportFile); + Assert.Null(context.Depth); + Assert.False(context.IncludeKnownIssues); + Assert.Null(context.ResultsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test that the Cli subsystem sets the Version property when --version is specified. /// - [TestMethod] + [Fact] public void Cli_VersionFlag_SetsProperty() { // Arrange & Act: create context with --version flag using var context = Context.Create(["--version"]); // Assert: Version property is set - Assert.IsTrue(context.Version); + Assert.True(context.Version); } /// /// Test that the Cli subsystem sets the Help property when --help is specified. /// - [TestMethod] + [Fact] public void Cli_HelpFlag_SetsProperty() { // Arrange & Act: create context with --help flag using var context = Context.Create(["--help"]); // Assert: Help property is set - Assert.IsTrue(context.Help); + Assert.True(context.Help); } /// /// Test that the Cli subsystem sets the Silent property when --silent is specified. /// - [TestMethod] + [Fact] public void Cli_SilentFlag_SetsProperty() { // Arrange & Act: create context with --silent flag using var context = Context.Create(["--silent"]); // Assert: Silent property is set - Assert.IsTrue(context.Silent); + Assert.True(context.Silent); } /// /// Test that the Cli subsystem suppresses console output when --silent is specified. /// - [TestMethod] + [Fact] public void Cli_SilentFlag_SuppressesConsoleOutput() { // Arrange: create context with silent flag and capture console output @@ -108,7 +107,7 @@ public void Cli_SilentFlag_SuppressesConsoleOutput() context.WriteLine("Test message"); // Assert: no output was written to the console - Assert.AreEqual(string.Empty, output.ToString()); + Assert.Equal(string.Empty, output.ToString()); } finally { @@ -120,35 +119,35 @@ public void Cli_SilentFlag_SuppressesConsoleOutput() /// /// Test that the Cli subsystem sets the BuildVersion property when --build-version is specified. /// - [TestMethod] + [Fact] public void Cli_BuildVersionFlag_SetsProperty() { // Arrange & Act: create context with --build-version argument using var context = Context.Create(["--build-version", "1.2.3"]); // Assert: BuildVersion property is set to the specified value - Assert.AreEqual("1.2.3", context.BuildVersion); + Assert.Equal("1.2.3", context.BuildVersion); } /// /// Test that the Cli subsystem sets report properties when --report and --depth are specified. /// - [TestMethod] + [Fact] public void Cli_ReportFlags_SetProperties() { // Arrange & Act: create context with --report and --depth arguments using var context = Context.Create(["--report", "output.md", "--depth", "3", "--include-known-issues"]); // Assert: report properties are set to the specified values - Assert.AreEqual("output.md", context.ReportFile); - Assert.AreEqual(3, context.Depth); - Assert.IsTrue(context.IncludeKnownIssues); + Assert.Equal("output.md", context.ReportFile); + Assert.Equal(3, context.Depth); + Assert.True(context.IncludeKnownIssues); } /// /// Test that the Cli subsystem creates a log file when --log is specified. /// - [TestMethod] + [Fact] public void Cli_LogFlag_CreatesLogFile() { // Arrange: create a temporary log file path @@ -163,7 +162,7 @@ public void Cli_LogFlag_CreatesLogFile() } // Assert: log file exists and contains the written message - Assert.IsTrue(File.Exists(logFile)); + Assert.True(File.Exists(logFile)); var logContent = File.ReadAllText(logFile); Assert.Contains("Subsystem log test", logContent); } @@ -180,46 +179,46 @@ public void Cli_LogFlag_CreatesLogFile() /// /// Test that the Cli subsystem sets the Validate property when --validate is specified. /// - [TestMethod] + [Fact] public void Cli_ValidateFlag_SetsProperty() { // Arrange & Act: create context with --validate flag using var context = Context.Create(["--validate"]); // Assert: Validate property is set - Assert.IsTrue(context.Validate); + Assert.True(context.Validate); } /// /// Test that the Cli subsystem sets the ResultsFile property when --results is specified. /// - [TestMethod] + [Fact] public void Cli_ResultsFlag_SetsProperty() { // Arrange & Act: create context with --results argument using var context = Context.Create(["--results", "results.trx"]); // Assert: ResultsFile property is set to the specified value - Assert.AreEqual("results.trx", context.ResultsFile); + Assert.Equal("results.trx", context.ResultsFile); } /// /// Test that the Cli subsystem sets the ResultsFile property when --result (alias) is specified. /// - [TestMethod] + [Fact] public void Cli_ResultFlag_SetsProperty() { // Arrange & Act: create context with --result alias argument using var context = Context.Create(["--result", "results.trx"]); // Assert: ResultsFile property is set to the specified value - Assert.AreEqual("results.trx", context.ResultsFile); + Assert.Equal("results.trx", context.ResultsFile); } /// /// Test that the Cli subsystem writes error messages to stderr. /// - [TestMethod] + [Fact] public void Cli_ErrorOutput_WritesToStderr() { // Arrange: create context and capture stderr @@ -247,7 +246,7 @@ public void Cli_ErrorOutput_WritesToStderr() /// /// Test that the Cli subsystem throws an exception for an invalid argument. /// - [TestMethod] + [Fact] public void Cli_InvalidArgument_ThrowsException() { // Arrange & Act & Assert: attempt to create context with an unsupported argument @@ -263,14 +262,14 @@ public void Cli_InvalidArgument_ThrowsException() caughtException = ex; } - Assert.IsNotNull(caughtException); + Assert.NotNull(caughtException); Assert.Contains("Unsupported argument '--unsupported'", caughtException.Message); } /// /// Test that the Cli subsystem throws an exception when a required argument value is missing. /// - [TestMethod] + [Fact] public void Cli_MissingArgumentValue_ThrowsException() { // Arrange & Act & Assert: attempt to create context with --build-version but no value @@ -286,27 +285,27 @@ public void Cli_MissingArgumentValue_ThrowsException() caughtException = ex; } - Assert.IsNotNull(caughtException); + Assert.NotNull(caughtException); Assert.Contains("--build-version requires a version argument", caughtException.Message); } /// /// Test that the Cli subsystem defaults ExitCode to zero. /// - [TestMethod] + [Fact] public void Cli_ExitCode_DefaultsToZero() { // Arrange & Act: create context with no arguments using var context = Context.Create([]); // Assert: exit code defaults to zero - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test that the Cli subsystem sets ExitCode to 1 when WriteError is called. /// - [TestMethod] + [Fact] public void Cli_WriteError_SetsExitCodeToOne() { // Arrange: create context with no arguments @@ -324,7 +323,7 @@ public void Cli_WriteError_SetsExitCodeToOne() context.WriteError("Subsystem exit code test"); // Assert: exit code is set to 1 - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -336,45 +335,45 @@ public void Cli_WriteError_SetsExitCodeToOne() /// /// Test that the Cli subsystem sets the Version property when -v (short form) is specified. /// - [TestMethod] + [Fact] public void Cli_VersionShortFlag_SetsProperty() { // Arrange & Act: create context with -v short flag using var context = Context.Create(["-v"]); // Assert: Version property is set - Assert.IsTrue(context.Version); + Assert.True(context.Version); } /// /// Test that the Cli subsystem sets the Help property when short-form help flags are specified. /// - [TestMethod] + [Fact] public void Cli_HelpShortFlags_SetProperty() { // Arrange & Act: create context with -h short flag using var contextH = Context.Create(["-h"]); // Assert: Help property is set for -h - Assert.IsTrue(contextH.Help); + Assert.True(contextH.Help); // Arrange & Act: create context with -? flag using var contextQuestion = Context.Create(["-?"]); // Assert: Help property is set for -? - Assert.IsTrue(contextQuestion.Help); + Assert.True(contextQuestion.Help); } /// /// Test that the Cli subsystem sets the Lint property when --lint is specified. /// - [TestMethod] + [Fact] public void Cli_LintFlag_SetsProperty() { // Arrange & Act: create context with --lint flag using var context = Context.Create(["--lint"]); // Assert: Lint property is set - Assert.IsTrue(context.Lint); + Assert.True(context.Lint); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs b/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs index 6a227c22..9a060d0e 100644 --- a/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Cli/ContextTests.cs @@ -25,231 +25,230 @@ namespace DemaConsulting.BuildMark.Tests.Cli; /// /// Tests for the Context class. /// -[TestClass] public class ContextTests { /// /// Test that Context.Create with empty arguments creates a valid context. /// - [TestMethod] + [Fact] public void Context_Create_EmptyArguments_CreatesValidContext() { // Create context with empty arguments using var context = Context.Create([]); // Verify properties have expected default values - Assert.IsFalse(context.Version); - Assert.IsFalse(context.Help); - Assert.IsFalse(context.Silent); - Assert.IsFalse(context.Validate); - Assert.IsFalse(context.Lint); - Assert.IsNull(context.BuildVersion); - Assert.IsNull(context.ReportFile); - Assert.IsNull(context.Depth); - Assert.IsFalse(context.IncludeKnownIssues); - Assert.IsNull(context.ResultsFile); - Assert.AreEqual(0, context.ExitCode); + Assert.False(context.Version); + Assert.False(context.Help); + Assert.False(context.Silent); + Assert.False(context.Validate); + Assert.False(context.Lint); + Assert.Null(context.BuildVersion); + Assert.Null(context.ReportFile); + Assert.Null(context.Depth); + Assert.False(context.IncludeKnownIssues); + Assert.Null(context.ResultsFile); + Assert.Equal(0, context.ExitCode); } /// /// Test that Context.Create with -v flag sets Version property. /// - [TestMethod] + [Fact] public void Context_Create_ShortVersionFlag_SetsVersionProperty() { // Create context with -v flag using var context = Context.Create(["-v"]); // Verify Version property is set - Assert.IsTrue(context.Version); + Assert.True(context.Version); } /// /// Test that Context.Create with --version flag sets Version property. /// - [TestMethod] + [Fact] public void Context_Create_LongVersionFlag_SetsVersionProperty() { // Create context with --version flag using var context = Context.Create(["--version"]); // Verify Version property is set - Assert.IsTrue(context.Version); + Assert.True(context.Version); } /// /// Test that Context.Create with -? flag sets Help property. /// - [TestMethod] + [Fact] public void Context_Create_QuestionMarkHelpFlag_SetsHelpProperty() { // Create context with -? flag using var context = Context.Create(["-?"]); // Verify Help property is set - Assert.IsTrue(context.Help); + Assert.True(context.Help); } /// /// Test that Context.Create with -h flag sets Help property. /// - [TestMethod] + [Fact] public void Context_Create_ShortHelpFlag_SetsHelpProperty() { // Create context with -h flag using var context = Context.Create(["-h"]); // Verify Help property is set - Assert.IsTrue(context.Help); + Assert.True(context.Help); } /// /// Test that Context.Create with --help flag sets Help property. /// - [TestMethod] + [Fact] public void Context_Create_LongHelpFlag_SetsHelpProperty() { // Create context with --help flag using var context = Context.Create(["--help"]); // Verify Help property is set - Assert.IsTrue(context.Help); + Assert.True(context.Help); } /// /// Test that Context.Create with --silent flag sets Silent property. /// - [TestMethod] + [Fact] public void Context_Create_SilentFlag_SetsSilentProperty() { // Create context with --silent flag using var context = Context.Create(["--silent"]); // Verify Silent property is set - Assert.IsTrue(context.Silent); + Assert.True(context.Silent); } /// /// Test that Context.Create with --validate flag sets Validate property. /// - [TestMethod] + [Fact] public void Context_Create_ValidateFlag_SetsValidateProperty() { // Create context with --validate flag using var context = Context.Create(["--validate"]); // Verify Validate property is set - Assert.IsTrue(context.Validate); + Assert.True(context.Validate); } /// /// Test that Context.Create with --lint flag sets Lint property. /// - [TestMethod] + [Fact] public void Context_Create_LintFlag_SetsLintProperty() { // Create context with --lint flag using var context = Context.Create(["--lint"]); // Verify Lint property is set - Assert.IsTrue(context.Lint); + Assert.True(context.Lint); } /// /// Test that Context.Create with --build-version argument sets BuildVersion property. /// - [TestMethod] + [Fact] public void Context_Create_BuildVersionArgument_SetsBuildVersionProperty() { // Create context with --build-version argument using var context = Context.Create(["--build-version", "1.2.3"]); // Verify BuildVersion property is set - Assert.AreEqual("1.2.3", context.BuildVersion); + Assert.Equal("1.2.3", context.BuildVersion); } /// /// Test that Context.Create with --report argument sets ReportFile property. /// - [TestMethod] + [Fact] public void Context_Create_ReportArgument_SetsReportFileProperty() { // Create context with --report argument using var context = Context.Create(["--report", "report.md"]); // Verify ReportFile property is set - Assert.AreEqual("report.md", context.ReportFile); + Assert.Equal("report.md", context.ReportFile); } /// /// Test that Context.Create with --depth argument sets Depth property. /// - [TestMethod] + [Fact] public void Context_Create_DepthArgument_SetsDepthProperty() { // Create context with --depth argument using var context = Context.Create(["--depth", "3"]); // Verify Depth property is set - Assert.AreEqual(3, context.Depth); + Assert.Equal(3, context.Depth); } /// /// Test that Context.Create with legacy --report-depth argument sets Depth property. /// - [TestMethod] + [Fact] public void Context_Create_LegacyReportDepthArgument_SetsDepthProperty() { // Create context with legacy --report-depth argument using var context = Context.Create(["--report-depth", "3"]); // Verify Depth property is set (legacy alias) - Assert.AreEqual(3, context.Depth); + Assert.Equal(3, context.Depth); } /// /// Test that Context.Create with --include-known-issues flag sets IncludeKnownIssues property. /// - [TestMethod] + [Fact] public void Context_Create_IncludeKnownIssuesFlag_SetsIncludeKnownIssuesProperty() { // Create context with --include-known-issues flag using var context = Context.Create(["--include-known-issues"]); // Verify IncludeKnownIssues property is set - Assert.IsTrue(context.IncludeKnownIssues); + Assert.True(context.IncludeKnownIssues); } /// /// Test that Context.Create with --results argument sets ResultsFile property. /// - [TestMethod] + [Fact] public void Context_Create_ResultsArgument_SetsResultsFileProperty() { // Create context with --results argument using var context = Context.Create(["--results", "results.trx"]); // Verify ResultsFile property is set - Assert.AreEqual("results.trx", context.ResultsFile); + Assert.Equal("results.trx", context.ResultsFile); } /// /// Test that Context.Create with --result (alias) argument sets ResultsFile property. /// - [TestMethod] + [Fact] public void Context_Create_ResultArgument_SetsResultsFileProperty() { // Create context with --result alias argument using var context = Context.Create(["--result", "results.trx"]); // Verify ResultsFile property is set - Assert.AreEqual("results.trx", context.ResultsFile); + Assert.Equal("results.trx", context.ResultsFile); } /// /// Test that Context.Create with --log argument creates log file. /// - [TestMethod] + [Fact] public void Context_Create_LogArgument_CreatesLogFile() { // Create temporary log file path @@ -260,7 +259,7 @@ public void Context_Create_LogArgument_CreatesLogFile() using var context = Context.Create(["--log", logFile]); // Verify log file exists - Assert.IsTrue(File.Exists(logFile)); + Assert.True(File.Exists(logFile)); } finally { @@ -275,7 +274,7 @@ public void Context_Create_LogArgument_CreatesLogFile() /// /// Test that Context.Create with multiple arguments sets all properties correctly. /// - [TestMethod] + [Fact] public void Context_Create_MultipleArguments_SetsAllPropertiesCorrectly() { // Create context with multiple arguments @@ -292,145 +291,145 @@ public void Context_Create_MultipleArguments_SetsAllPropertiesCorrectly() ]); // Verify all properties are set correctly - Assert.IsTrue(context.Silent); - Assert.IsTrue(context.Validate); - Assert.IsTrue(context.Lint); - Assert.AreEqual("1.2.3", context.BuildVersion); - Assert.AreEqual("report.md", context.ReportFile); - Assert.AreEqual(2, context.Depth); - Assert.IsTrue(context.IncludeKnownIssues); - Assert.AreEqual("results.trx", context.ResultsFile); + Assert.True(context.Silent); + Assert.True(context.Validate); + Assert.True(context.Lint); + Assert.Equal("1.2.3", context.BuildVersion); + Assert.Equal("report.md", context.ReportFile); + Assert.Equal(2, context.Depth); + Assert.True(context.IncludeKnownIssues); + Assert.Equal("results.trx", context.ResultsFile); } /// /// Test that Context.Create throws ArgumentException for unsupported argument. /// - [TestMethod] + [Fact] public void Context_Create_UnsupportedArgument_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--unsupported"])); + var exception = Assert.Throws(() => Context.Create(["--unsupported"])); Assert.Contains("Unsupported argument '--unsupported'", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --build-version has no value. /// - [TestMethod] + [Fact] public void Context_Create_BuildVersionWithoutValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--build-version"])); + var exception = Assert.Throws(() => Context.Create(["--build-version"])); Assert.Contains("--build-version requires a version argument", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --report has no value. /// - [TestMethod] + [Fact] public void Context_Create_ReportWithoutValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--report"])); + var exception = Assert.Throws(() => Context.Create(["--report"])); Assert.Contains("--report requires a filename argument", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --depth has no value. /// - [TestMethod] + [Fact] public void Context_Create_DepthWithoutValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--depth"])); + var exception = Assert.Throws(() => Context.Create(["--depth"])); Assert.Contains("--depth requires a depth argument", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --depth has non-integer value. /// - [TestMethod] + [Fact] public void Context_Create_DepthWithNonIntegerValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--depth", "abc"])); + var exception = Assert.Throws(() => Context.Create(["--depth", "abc"])); Assert.Contains("--depth requires a positive integer", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --depth has zero value. /// - [TestMethod] + [Fact] public void Context_Create_DepthWithZeroValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--depth", "0"])); + var exception = Assert.Throws(() => Context.Create(["--depth", "0"])); Assert.Contains("--depth requires a positive integer", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --depth has negative value. /// - [TestMethod] + [Fact] public void Context_Create_DepthWithNegativeValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--depth", "-1"])); + var exception = Assert.Throws(() => Context.Create(["--depth", "-1"])); Assert.Contains("--depth requires a positive integer", exception.Message); } /// /// Test that Context.Create throws ArgumentOutOfRangeException when --depth exceeds maximum value. /// - [TestMethod] + [Fact] public void Context_Create_DepthExceedingMaximum_ThrowsArgumentOutOfRangeException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--depth", "7"])); + var exception = Assert.Throws(() => Context.Create(["--depth", "7"])); Assert.Contains("'--depth' must be between 1 and 6", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --results has no value. /// - [TestMethod] + [Fact] public void Context_Create_ResultsWithoutValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--results"])); + var exception = Assert.Throws(() => Context.Create(["--results"])); Assert.Contains("--results requires a results filename argument", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --result (alias) has no value. /// - [TestMethod] + [Fact] public void Context_Create_ResultWithoutValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--result"])); + var exception = Assert.Throws(() => Context.Create(["--result"])); Assert.Contains("--result requires a results filename argument", exception.Message); } /// /// Test that Context.Create throws ArgumentException when --log has no value. /// - [TestMethod] + [Fact] public void Context_Create_LogWithoutValue_ThrowsArgumentException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => Context.Create(["--log"])); + var exception = Assert.Throws(() => Context.Create(["--log"])); Assert.Contains("--log requires a filename argument", exception.Message); } /// /// Test that Context.Create throws InvalidOperationException when log file cannot be created. /// - [TestMethod] + [Fact] public void Context_Create_InvalidLogFilePath_ThrowsInvalidOperationException() { // Act & Assert - var exception = Assert.ThrowsExactly( + var exception = Assert.Throws( () => Context.Create(["--log", "/invalid/path/to/log.txt"])); Assert.Contains("Failed to open log file", exception.Message); } @@ -438,7 +437,7 @@ public void Context_Create_InvalidLogFilePath_ThrowsInvalidOperationException() /// /// Test that Context.WriteLine writes to console when not in silent mode. /// - [TestMethod] + [Fact] public void Context_WriteLine_NotSilent_WritesToConsole() { // Create context without silent flag @@ -455,7 +454,7 @@ public void Context_WriteLine_NotSilent_WritesToConsole() context.WriteLine("Test message"); // Verify message was written to console - Assert.AreEqual("Test message" + Environment.NewLine, output.ToString()); + Assert.Equal("Test message" + Environment.NewLine, output.ToString()); } finally { @@ -467,7 +466,7 @@ public void Context_WriteLine_NotSilent_WritesToConsole() /// /// Test that Context.WriteLine does not write to console when in silent mode. /// - [TestMethod] + [Fact] public void Context_WriteLine_Silent_DoesNotWriteToConsole() { // Create context with silent flag @@ -484,7 +483,7 @@ public void Context_WriteLine_Silent_DoesNotWriteToConsole() context.WriteLine("Test message"); // Verify no output to console - Assert.AreEqual(string.Empty, output.ToString()); + Assert.Equal(string.Empty, output.ToString()); } finally { @@ -496,7 +495,7 @@ public void Context_WriteLine_Silent_DoesNotWriteToConsole() /// /// Test that Context.WriteLine writes to log file when logging is enabled. /// - [TestMethod] + [Fact] public void Context_WriteLine_WithLogFile_WritesToLogFile() { // Create temporary log file path @@ -512,7 +511,7 @@ public void Context_WriteLine_WithLogFile_WritesToLogFile() // Verify message was written to log file var logContent = File.ReadAllText(logFile); - Assert.AreEqual("Test message" + Environment.NewLine, logContent); + Assert.Equal("Test message" + Environment.NewLine, logContent); } finally { @@ -527,7 +526,7 @@ public void Context_WriteLine_WithLogFile_WritesToLogFile() /// /// Test that Context.WriteError writes to console when not in silent mode. /// - [TestMethod] + [Fact] public void Context_WriteError_NotSilent_WritesToConsole() { // Create context without silent flag @@ -544,7 +543,7 @@ public void Context_WriteError_NotSilent_WritesToConsole() context.WriteError("Error message"); // Verify message was written to error console - Assert.AreEqual("Error message" + Environment.NewLine, output.ToString()); + Assert.Equal("Error message" + Environment.NewLine, output.ToString()); } finally { @@ -556,7 +555,7 @@ public void Context_WriteError_NotSilent_WritesToConsole() /// /// Test that Context.WriteError does not write to console when in silent mode. /// - [TestMethod] + [Fact] public void Context_WriteError_Silent_DoesNotWriteToConsole() { // Create context with silent flag @@ -573,7 +572,7 @@ public void Context_WriteError_Silent_DoesNotWriteToConsole() context.WriteError("Error message"); // Verify no output to console - Assert.AreEqual(string.Empty, output.ToString()); + Assert.Equal(string.Empty, output.ToString()); } finally { @@ -585,7 +584,7 @@ public void Context_WriteError_Silent_DoesNotWriteToConsole() /// /// Test that Context.WriteError writes to log file when logging is enabled. /// - [TestMethod] + [Fact] public void Context_WriteError_WithLogFile_WritesToLogFile() { // Create temporary log file path @@ -601,7 +600,7 @@ public void Context_WriteError_WithLogFile_WritesToLogFile() // Verify message was written to log file var logContent = File.ReadAllText(logFile); - Assert.AreEqual("Error message" + Environment.NewLine, logContent); + Assert.Equal("Error message" + Environment.NewLine, logContent); } finally { @@ -616,14 +615,14 @@ public void Context_WriteError_WithLogFile_WritesToLogFile() /// /// Test that Context.WriteError sets ExitCode to 1. /// - [TestMethod] + [Fact] public void Context_WriteError_SetsExitCodeToOne() { // Create context using var context = Context.Create([]); // Verify initial exit code is 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); // Capture console error output to avoid displaying error during test using var output = new StringWriter(); @@ -636,7 +635,7 @@ public void Context_WriteError_SetsExitCodeToOne() context.WriteError("Error message"); // Verify exit code is now 1 - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } finally { @@ -648,7 +647,7 @@ public void Context_WriteError_SetsExitCodeToOne() /// /// Test that Context.ExitCode remains 0 when no errors are written. /// - [TestMethod] + [Fact] public void Context_ExitCode_NoErrors_RemainsZero() { // Create context @@ -666,7 +665,7 @@ public void Context_ExitCode_NoErrors_RemainsZero() context.WriteLine("Message 2"); // Verify exit code remains 0 - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } finally { @@ -678,7 +677,7 @@ public void Context_ExitCode_NoErrors_RemainsZero() /// /// Test that Context.Dispose closes log file properly. /// - [TestMethod] + [Fact] public void Context_Dispose_ClosesLogFileProperly() { // Create temporary log file path @@ -693,7 +692,7 @@ public void Context_Dispose_ClosesLogFileProperly() // Verify we can delete the log file (it's been closed) File.Delete(logFile); - Assert.IsFalse(File.Exists(logFile)); + Assert.False(File.Exists(logFile)); } finally { diff --git a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs index 4baf0a96..0d294c11 100644 --- a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationSubsystemTests.cs @@ -26,14 +26,8 @@ namespace DemaConsulting.BuildMark.Tests.Configuration; /// /// Subsystem-level tests for the Configuration subsystem. /// -[TestClass] public class ConfigurationSubsystemTests { - /// - /// Gets or sets the test context for the current test run. - /// - public TestContext TestContext { get; set; } = null!; - // ───────────────────────────────────────────────────────────────────────── // BuildMark-Configuration-Read // ───────────────────────────────────────────────────────────────────────── @@ -41,7 +35,7 @@ public class ConfigurationSubsystemTests /// /// Test that the Configuration subsystem reads a valid .buildmark.yaml file and returns a populated result. /// - [TestMethod] + [Fact] public async Task Configuration_ReadAsync_ValidFile_ReturnsConfiguration() { // Arrange: create a temporary directory with a valid .buildmark.yaml file @@ -64,7 +58,7 @@ await File.WriteAllTextAsync( label: [feature] route: changes """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -72,13 +66,13 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert: configuration is returned with expected structure - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.AreEqual("github", result.Config.Connector?.Type); - Assert.AreEqual("test-owner", result.Config.Connector?.GitHub?.Owner); - Assert.AreEqual("test-repo", result.Config.Connector?.GitHub?.Repo); - Assert.HasCount(1, result.Config.Sections); - Assert.HasCount(1, result.Config.Rules); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("github", result.Config.Connector?.Type); + Assert.Equal("test-owner", result.Config.Connector?.GitHub?.Owner); + Assert.Equal("test-repo", result.Config.Connector?.GitHub?.Repo); + Assert.Single(result.Config.Sections); + Assert.Single(result.Config.Rules); } finally { @@ -90,7 +84,7 @@ await File.WriteAllTextAsync( /// /// Test that the Configuration subsystem returns an empty result when the file is missing. /// - [TestMethod] + [Fact] public async Task Configuration_ReadAsync_MissingFile_ReturnsEmptyResult() { // Arrange: create a temporary directory with no .buildmark.yaml file @@ -103,9 +97,9 @@ public async Task Configuration_ReadAsync_MissingFile_ReturnsEmptyResult() var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert: result has null Config and no errors - Assert.IsNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.IsEmpty(result.Issues); + Assert.Null(result.Config); + Assert.False(result.HasErrors); + Assert.Empty(result.Issues); } finally { @@ -117,7 +111,7 @@ public async Task Configuration_ReadAsync_MissingFile_ReturnsEmptyResult() /// /// Test that the Configuration subsystem reports errors for a malformed .buildmark.yaml file. /// - [TestMethod] + [Fact] public async Task Configuration_ReadAsync_MalformedFile_ReportsError() { // Arrange: create a temporary directory with a malformed .buildmark.yaml file @@ -127,7 +121,7 @@ public async Task Configuration_ReadAsync_MalformedFile_ReportsError() await File.WriteAllTextAsync( filePath, "connector:\n\ttype: github\n", - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -135,10 +129,10 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert: result contains errors and Config is null - Assert.IsNull(result.Config); - Assert.IsTrue(result.HasErrors); - Assert.IsNotEmpty(result.Issues); - Assert.AreEqual(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.NotEmpty(result.Issues); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); } finally { @@ -154,7 +148,7 @@ await File.WriteAllTextAsync( /// /// Test that the Configuration subsystem sets the context exit code when an error issue is reported. /// - [TestMethod] + [Fact] public void Configuration_Issues_ErrorIssue_SetsExitCode() { // Arrange: create a context and a result with an error issue @@ -173,13 +167,13 @@ public void Configuration_Issues_ErrorIssue_SetsExitCode() result.ReportTo(context); // Assert: exit code is set to 1 due to the error issue - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } /// /// Test that the Configuration subsystem does not set the context exit code when only a warning issue is reported. /// - [TestMethod] + [Fact] public void Configuration_Issues_WarningIssue_DoesNotSetExitCode() { // Arrange: create a context and a result with a warning-only issue @@ -198,13 +192,13 @@ public void Configuration_Issues_WarningIssue_DoesNotSetExitCode() result.ReportTo(context); // Assert: exit code remains 0 for warning-only issues - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test that the Configuration subsystem reports accurate 1-based line numbers for validation errors. /// - [TestMethod] + [Fact] public async Task Configuration_Issues_ValidationError_ReportsAccurateLine() { // Arrange: create a YAML file where the unsupported key is on a known line number @@ -218,7 +212,7 @@ await File.WriteAllTextAsync( type: github unsupported-key: value """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -226,11 +220,10 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert: the error is reported with the correct line number (3) - Assert.IsNotNull(result.Issues); - Assert.IsTrue(result.HasErrors); + Assert.NotNull(result.Issues); + Assert.True(result.HasErrors); var issue = result.Issues[0]; - Assert.AreEqual(3, issue.Line, - $"Expected line 3 for 'unsupported-key' but got {issue.Line}"); + Assert.True(issue.Line == 3, $"Expected line 3 for 'unsupported-key' but got {issue.Line}"); } finally { @@ -246,7 +239,7 @@ await File.WriteAllTextAsync( /// /// Test that the Configuration subsystem parses connector-specific settings from a valid file. /// - [TestMethod] + [Fact] public async Task Configuration_ConnectorConfig_ValidFile_ParsesConnectorSettings() { // Arrange: create a temporary directory with a valid .buildmark.yaml containing connector settings @@ -263,7 +256,7 @@ await File.WriteAllTextAsync( repo: my-project base-url: https://api.github.example.com """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -271,12 +264,12 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert: connector settings are parsed correctly - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.AreEqual("github", result.Config.Connector?.Type); - Assert.AreEqual("acme-org", result.Config.Connector?.GitHub?.Owner); - Assert.AreEqual("my-project", result.Config.Connector?.GitHub?.Repo); - Assert.AreEqual("https://api.github.example.com", result.Config.Connector?.GitHub?.BaseUrl); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("github", result.Config.Connector?.Type); + Assert.Equal("acme-org", result.Config.Connector?.GitHub?.Owner); + Assert.Equal("my-project", result.Config.Connector?.GitHub?.Repo); + Assert.Equal("https://api.github.example.com", result.Config.Connector?.GitHub?.BaseUrl); } finally { @@ -288,7 +281,7 @@ await File.WriteAllTextAsync( /// /// Test that the Configuration subsystem parses Azure DevOps connector settings from a valid file. /// - [TestMethod] + [Fact] public async Task Configuration_ConnectorConfig_ValidFile_ParsesAzureDevOpsSettings() { // Arrange: create a temporary directory with a valid .buildmark.yaml containing Azure DevOps settings @@ -306,7 +299,7 @@ await File.WriteAllTextAsync( project: my-project repository: my-repo """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -314,13 +307,13 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert: Azure DevOps connector settings are parsed correctly - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.AreEqual("azure-devops", result.Config.Connector?.Type); - Assert.AreEqual("https://dev.azure.com/acme", result.Config.Connector?.AzureDevOps?.OrganizationUrl); - Assert.AreEqual("acme", result.Config.Connector?.AzureDevOps?.Organization); - Assert.AreEqual("my-project", result.Config.Connector?.AzureDevOps?.Project); - Assert.AreEqual("my-repo", result.Config.Connector?.AzureDevOps?.Repository); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("azure-devops", result.Config.Connector?.Type); + Assert.Equal("https://dev.azure.com/acme", result.Config.Connector?.AzureDevOps?.OrganizationUrl); + Assert.Equal("acme", result.Config.Connector?.AzureDevOps?.Organization); + Assert.Equal("my-project", result.Config.Connector?.AzureDevOps?.Project); + Assert.Equal("my-repo", result.Config.Connector?.AzureDevOps?.Repository); } finally { diff --git a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs index 2c3113b8..d649ab52 100644 --- a/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Configuration/ConfigurationTests.cs @@ -26,18 +26,12 @@ namespace DemaConsulting.BuildMark.Tests.Configuration; /// /// Tests for configuration loading and reporting. /// -[TestClass] public class ConfigurationTests { - /// - /// Gets or sets the test context for the current test run. - /// - public TestContext TestContext { get; set; } = null!; - /// /// Test that missing configuration files return an empty result. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult() { // Arrange @@ -50,9 +44,9 @@ public async Task BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.IsEmpty(result.Issues); + Assert.Null(result.Config); + Assert.False(result.HasErrors); + Assert.Empty(result.Issues); } finally { @@ -63,7 +57,7 @@ public async Task BuildMarkConfigReader_ReadAsync_MissingFile_ReturnsEmptyResult /// /// Test that valid configuration files are parsed into the configuration model. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_ValidFile_ReturnsParsedConfiguration() { // Arrange @@ -87,7 +81,7 @@ await File.WriteAllTextAsync( label: [feature] route: changes """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -95,17 +89,17 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.AreEqual("github", result.Config.Connector?.Type); - Assert.AreEqual("example-owner", result.Config.Connector?.GitHub?.Owner); - Assert.AreEqual("hello-world", result.Config.Connector?.GitHub?.Repo); - Assert.AreEqual("https://api.github.com", result.Config.Connector?.GitHub?.BaseUrl); - Assert.HasCount(1, result.Config.Sections); - Assert.AreEqual("changes", result.Config.Sections[0].Id); - Assert.HasCount(1, result.Config.Rules); - Assert.AreEqual("changes", result.Config.Rules[0].Route); - Assert.AreEqual("feature", result.Config.Rules[0].Match?.Label[0]); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("github", result.Config.Connector?.Type); + Assert.Equal("example-owner", result.Config.Connector?.GitHub?.Owner); + Assert.Equal("hello-world", result.Config.Connector?.GitHub?.Repo); + Assert.Equal("https://api.github.com", result.Config.Connector?.GitHub?.BaseUrl); + Assert.Single(result.Config.Sections); + Assert.Equal("changes", result.Config.Sections[0].Id); + Assert.Single(result.Config.Rules); + Assert.Equal("changes", result.Config.Rules[0].Route); + Assert.Equal("feature", result.Config.Rules[0].Match?.Label[0]); } finally { @@ -116,7 +110,7 @@ await File.WriteAllTextAsync( /// /// Test that malformed configuration files surface an error issue. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_InvalidRepositoryValue_ReturnsErrorIssue() { // Arrange @@ -131,7 +125,7 @@ await File.WriteAllTextAsync( github: repository: invalid """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -139,9 +133,9 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNull(result.Config); - Assert.IsTrue(result.HasErrors); - Assert.AreEqual(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); Assert.Contains("owner/repo", result.Issues[0].Description); } finally @@ -153,7 +147,7 @@ await File.WriteAllTextAsync( /// /// Test that malformed configuration files surface an error issue. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_MalformedFile_ReturnsErrorIssue() { // Arrange @@ -163,7 +157,7 @@ public async Task BuildMarkConfigReader_ReadAsync_MalformedFile_ReturnsErrorIssu await File.WriteAllTextAsync( filePath, "connector:\n\ttype: github\n", - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -171,9 +165,9 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNull(result.Config); - Assert.IsTrue(result.HasErrors); - Assert.AreEqual(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); Assert.Contains("tab", result.Issues[0].Description, StringComparison.OrdinalIgnoreCase); } finally @@ -185,7 +179,7 @@ await File.WriteAllTextAsync( /// /// Test that a valid Azure DevOps connector block is parsed into the configuration model. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_ValidAzureDevOpsConnector_ReturnsParsedConfiguration() { // Arrange @@ -206,7 +200,7 @@ await File.WriteAllTextAsync( - id: changes title: Changes """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -214,13 +208,13 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.AreEqual("azure-devops", result.Config.Connector?.Type); - Assert.AreEqual("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); - Assert.AreEqual("myorg", result.Config.Connector?.AzureDevOps?.Organization); - Assert.AreEqual("myproject", result.Config.Connector?.AzureDevOps?.Project); - Assert.AreEqual("myrepo", result.Config.Connector?.AzureDevOps?.Repository); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("azure-devops", result.Config.Connector?.Type); + Assert.Equal("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); + Assert.Equal("myorg", result.Config.Connector?.AzureDevOps?.Organization); + Assert.Equal("myproject", result.Config.Connector?.AzureDevOps?.Project); + Assert.Equal("myrepo", result.Config.Connector?.AzureDevOps?.Repository); } finally { @@ -231,7 +225,7 @@ await File.WriteAllTextAsync( /// /// Test that Azure DevOps connector block with alternate key aliases is parsed correctly. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsConnectorAliases_ReturnsParsedConfiguration() { // Arrange @@ -252,7 +246,7 @@ await File.WriteAllTextAsync( - id: changes title: Changes """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -260,12 +254,12 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.AreEqual("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); - Assert.AreEqual("myorg", result.Config.Connector?.AzureDevOps?.Organization); - Assert.AreEqual("myproject", result.Config.Connector?.AzureDevOps?.Project); - Assert.AreEqual("myrepo", result.Config.Connector?.AzureDevOps?.Repository); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.Equal("https://dev.azure.com/myorg", result.Config.Connector?.AzureDevOps?.OrganizationUrl); + Assert.Equal("myorg", result.Config.Connector?.AzureDevOps?.Organization); + Assert.Equal("myproject", result.Config.Connector?.AzureDevOps?.Project); + Assert.Equal("myrepo", result.Config.Connector?.AzureDevOps?.Repository); } finally { @@ -276,7 +270,7 @@ await File.WriteAllTextAsync( /// /// Test that an unsupported key inside the Azure DevOps connector block produces an error. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsUnsupportedKey_ReturnsErrorIssue() { // Arrange @@ -291,7 +285,7 @@ await File.WriteAllTextAsync( azure-devops: unknown-key: some-value """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -299,9 +293,9 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNull(result.Config); - Assert.IsTrue(result.HasErrors); - Assert.AreEqual(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); Assert.Contains("Unsupported Azure DevOps connector key", result.Issues[0].Description); } finally @@ -313,7 +307,7 @@ await File.WriteAllTextAsync( /// /// Test that a non-mapping Azure DevOps connector node produces an error. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_AzureDevOpsNonMapping_ReturnsErrorIssue() { // Arrange @@ -327,7 +321,7 @@ await File.WriteAllTextAsync( type: azure-devops azure-devops: not-a-mapping """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -335,9 +329,9 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNull(result.Config); - Assert.IsTrue(result.HasErrors); - Assert.AreEqual(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Null(result.Config); + Assert.True(result.HasErrors); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); Assert.Contains("YAML mapping", result.Issues[0].Description); } finally @@ -349,7 +343,7 @@ await File.WriteAllTextAsync( /// /// Test that reporting an error issue sets the context exit code. /// - [TestMethod] + [Fact] public void ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode() { // Arrange @@ -368,13 +362,13 @@ public void ConfigurationLoadResult_ReportTo_ErrorIssue_SetsExitCode() result.ReportTo(context); // Assert - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); } /// /// Test that reporting a warning issue does not set the context exit code. /// - [TestMethod] + [Fact] public void ConfigurationLoadResult_ReportTo_WarningIssue_DoesNotSetExitCode() { // Arrange @@ -393,13 +387,13 @@ public void ConfigurationLoadResult_ReportTo_WarningIssue_DoesNotSetExitCode() result.ReportTo(context); // Assert - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); } /// /// Test that ReportTo includes the file path and line number in the formatted message. /// - [TestMethod] + [Fact] public void ConfigurationLoadResult_ReportTo_IssueMessage_IncludesLineNumber() { // Arrange @@ -419,64 +413,64 @@ public void ConfigurationLoadResult_ReportTo_IssueMessage_IncludesLineNumber() // Assert - the issue's FilePath and Line are surfaced via WriteError; confirm HasErrors // and that the issue record exposes the correct location fields - Assert.IsTrue(result.HasErrors); - Assert.AreEqual(1, context.ExitCode, "Error severity should set exit code"); - Assert.AreEqual("/repo/.buildmark.yaml", result.Issues[0].FilePath); - Assert.AreEqual(7, result.Issues[0].Line); - Assert.AreEqual(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); - Assert.AreEqual("Unexpected value", result.Issues[0].Description); + Assert.True(result.HasErrors); + Assert.True(context.ExitCode == 1, "Error severity should set exit code"); + Assert.Equal("/repo/.buildmark.yaml", result.Issues[0].FilePath); + Assert.Equal(7, result.Issues[0].Line); + Assert.Equal(ConfigurationIssueSeverity.Error, result.Issues[0].Severity); + Assert.Equal("Unexpected value", result.Issues[0].Description); } /// /// Test that the default configuration contains the expected sections and routing rules. /// - [TestMethod] + [Fact] public void BuildMarkConfig_CreateDefault_ContainsDependencyUpdatesSection() { // Act var config = BuildMarkConfig.CreateDefault(); // Assert - verify sections have correct IDs and titles - Assert.HasCount(3, config.Sections); - Assert.AreEqual("changes", config.Sections[0].Id); - Assert.AreEqual("Changes", config.Sections[0].Title); - Assert.AreEqual("bugs-fixed", config.Sections[1].Id); - Assert.AreEqual("Bugs Fixed", config.Sections[1].Title); - Assert.AreEqual("dependency-updates", config.Sections[2].Id); - Assert.AreEqual("Dependency Updates", config.Sections[2].Title); + Assert.Equal(3, config.Sections.Count); + Assert.Equal("changes", config.Sections[0].Id); + Assert.Equal("Changes", config.Sections[0].Title); + Assert.Equal("bugs-fixed", config.Sections[1].Id); + Assert.Equal("Bugs Fixed", config.Sections[1].Title); + Assert.Equal("dependency-updates", config.Sections[2].Id); + Assert.Equal("Dependency Updates", config.Sections[2].Title); // Assert - verify rules have correct routes and match conditions - Assert.HasCount(6, config.Rules); + Assert.Equal(6, config.Rules.Count); - Assert.AreEqual("dependency-updates", config.Rules[0].Route); + Assert.Equal("dependency-updates", config.Rules[0].Route); Assert.Contains("dependencies", config.Rules[0].Match!.Label); Assert.Contains("renovate", config.Rules[0].Match!.Label); Assert.Contains("dependabot", config.Rules[0].Match!.Label); - Assert.AreEqual("bugs-fixed", config.Rules[1].Route); + Assert.Equal("bugs-fixed", config.Rules[1].Route); Assert.Contains("Bug", config.Rules[1].Match!.WorkItemType); - Assert.AreEqual("bugs-fixed", config.Rules[2].Route); + Assert.Equal("bugs-fixed", config.Rules[2].Route); Assert.Contains("bug", config.Rules[2].Match!.Label); Assert.Contains("defect", config.Rules[2].Match!.Label); Assert.Contains("regression", config.Rules[2].Match!.Label); - Assert.AreEqual("suppressed", config.Rules[3].Route); + Assert.Equal("suppressed", config.Rules[3].Route); Assert.Contains("internal", config.Rules[3].Match!.Label); Assert.Contains("chore", config.Rules[3].Match!.Label); - Assert.AreEqual("suppressed", config.Rules[4].Route); + Assert.Equal("suppressed", config.Rules[4].Route); Assert.Contains("Task", config.Rules[4].Match!.WorkItemType); Assert.Contains("Epic", config.Rules[4].Match!.WorkItemType); - Assert.AreEqual("changes", config.Rules[5].Route); - Assert.IsNull(config.Rules[5].Match); + Assert.Equal("changes", config.Rules[5].Route); + Assert.Null(config.Rules[5].Match); } /// /// Test that a valid report section is parsed into the report configuration model. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_ValidReportSection_ReturnsParsedReportConfig() { // Arrange @@ -491,7 +485,7 @@ await File.WriteAllTextAsync( depth: 2 include-known-issues: true """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -499,12 +493,12 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNotNull(result.Config); - Assert.IsFalse(result.HasErrors); - Assert.IsNotNull(result.Config.Report); - Assert.AreEqual("build-notes.md", result.Config.Report.File); - Assert.AreEqual(2, result.Config.Report.Depth); - Assert.IsTrue(result.Config.Report.IncludeKnownIssues); + Assert.NotNull(result.Config); + Assert.False(result.HasErrors); + Assert.NotNull(result.Config.Report); + Assert.Equal("build-notes.md", result.Config.Report.File); + Assert.Equal(2, result.Config.Report.Depth); + Assert.True(result.Config.Report.IncludeKnownIssues); } finally { @@ -515,7 +509,7 @@ await File.WriteAllTextAsync( /// /// Test that an invalid report depth produces an error issue. /// - [TestMethod] + [Fact] public async Task BuildMarkConfigReader_ReadAsync_InvalidReportDepth_ReturnsErrorIssue() { // Arrange @@ -528,7 +522,7 @@ await File.WriteAllTextAsync( report: depth: -1 """, - TestContext.CancellationToken); + TestContext.Current.CancellationToken); try { @@ -536,8 +530,8 @@ await File.WriteAllTextAsync( var result = await BuildMarkConfigReader.ReadAsync(directory); // Assert - Assert.IsNull(result.Config); - Assert.IsTrue(result.HasErrors); + Assert.Null(result.Config); + Assert.True(result.HasErrors); Assert.Contains("positive integer", result.Issues[0].Description); } finally diff --git a/test/DemaConsulting.BuildMark.Tests/DemaConsulting.BuildMark.Tests.csproj b/test/DemaConsulting.BuildMark.Tests/DemaConsulting.BuildMark.Tests.csproj index a4f77fb5..38b51f27 100644 --- a/test/DemaConsulting.BuildMark.Tests/DemaConsulting.BuildMark.Tests.csproj +++ b/test/DemaConsulting.BuildMark.Tests/DemaConsulting.BuildMark.Tests.csproj @@ -2,6 +2,7 @@ + Exe net8.0;net9.0;net10.0 latest enable @@ -21,6 +22,7 @@ + @@ -34,8 +36,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs b/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs index dd74e5b0..786178d1 100644 --- a/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/IntegrationTests.cs @@ -20,8 +20,11 @@ using DemaConsulting.BuildMark.BuildNotes; using DemaConsulting.BuildMark.Cli; +using DemaConsulting.BuildMark.Configuration; using DemaConsulting.BuildMark.RepoConnectors; +using DemaConsulting.BuildMark.RepoConnectors.AzureDevOps; using DemaConsulting.BuildMark.RepoConnectors.Mock; +using DemaConsulting.BuildMark.Tests.RepoConnectors.AzureDevOps; using DemaConsulting.BuildMark.Utilities; using DemaConsulting.BuildMark.Version; @@ -30,30 +33,28 @@ namespace DemaConsulting.BuildMark.Tests; /// /// Integration tests that run the BuildMark application through dotnet. /// -[TestClass] public class IntegrationTests { - private string _dllPath = string.Empty; + private readonly string _dllPath; /// /// Initialize test by locating the BuildMark 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.BuildMark.dll"); - Assert.IsTrue(File.Exists(_dllPath), $"Could not find BuildMark DLL at {_dllPath}"); + Assert.True(File.Exists(_dllPath), $"Could not find BuildMark DLL at {_dllPath}"); } /// /// Test that version flag outputs version information. /// - [TestMethod] - public void IntegrationTest_VersionFlag_OutputsVersion() + [Fact] + public void BuildMark_VersionFlag_OutputsVersion() { // Run the application with --version flag var exitCode = Runner.Run( @@ -63,18 +64,18 @@ public void IntegrationTest_VersionFlag_OutputsVersion() "--version"); // Verify success - 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); } /// /// Test that help flag outputs usage information. /// - [TestMethod] - public void IntegrationTest_HelpFlag_OutputsUsageInformation() + [Fact] + public void BuildMark_HelpFlag_OutputsUsageInformation() { // Run the application with --help flag var exitCode = Runner.Run( @@ -84,7 +85,7 @@ public void IntegrationTest_HelpFlag_OutputsUsageInformation() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify usage information Assert.Contains("Usage: buildmark", output); @@ -96,8 +97,8 @@ public void IntegrationTest_HelpFlag_OutputsUsageInformation() /// /// Test that silent flag suppresses output. /// - [TestMethod] - public void IntegrationTest_SilentFlag_SuppressesOutput() + [Fact] + public void BuildMark_SilentFlag_SuppressesOutput() { // Run the application with --silent and --help flags var exitCode = Runner.Run( @@ -108,7 +109,7 @@ public void IntegrationTest_SilentFlag_SuppressesOutput() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify no banner in output Assert.DoesNotContain("BuildMark version", output); @@ -117,8 +118,8 @@ public void IntegrationTest_SilentFlag_SuppressesOutput() /// /// Test that invalid argument shows error. /// - [TestMethod] - public void IntegrationTest_InvalidArgument_ShowsError() + [Fact] + public void BuildMark_InvalidArgument_ShowsError() { // Run the application with invalid argument var exitCode = Runner.Run( @@ -128,7 +129,7 @@ public void IntegrationTest_InvalidArgument_ShowsError() "--invalid-argument"); // Verify error exit code - Assert.AreEqual(1, exitCode); + Assert.Equal(1, exitCode); // Verify error message Assert.Contains("Error:", output); @@ -138,8 +139,8 @@ public void IntegrationTest_InvalidArgument_ShowsError() /// /// Test that the tool handles an invalid report file path gracefully. /// - [TestMethod] - public void IntegrationTest_InvalidReportPath_ShowsError() + [Fact] + public void BuildMark_InvalidReportPath_ShowsError() { // Arrange: construct a path whose parent directory does not exist var invalidPath = Path.Combine( @@ -168,15 +169,15 @@ public void IntegrationTest_InvalidReportPath_ShowsError() } // Assert: tool reports an error message and exits with error code - Assert.AreEqual(1, context.ExitCode); + Assert.Equal(1, context.ExitCode); Assert.Contains("Error:", errorOutput.ToString()); } /// /// Test that validate flag runs self-validation. /// - [TestMethod] - public void IntegrationTest_ValidateFlag_RunsSelfValidation() + [Fact] + public void BuildMark_ValidateFlag_RunsSelfValidation() { // Run the application with --validate flag var exitCode = Runner.Run( @@ -186,17 +187,17 @@ public void IntegrationTest_ValidateFlag_RunsSelfValidation() "--validate"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify validation runs - Assert.IsFalse(string.IsNullOrWhiteSpace(output)); + Assert.False(string.IsNullOrWhiteSpace(output)); } /// /// Test that log parameter is accepted. /// - [TestMethod] - public void IntegrationTest_LogParameter_IsAccepted() + [Fact] + public void BuildMark_LogParameter_IsAccepted() { // Run the application with log parameter var exitCode = Runner.Run( @@ -207,7 +208,7 @@ public void IntegrationTest_LogParameter_IsAccepted() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); @@ -216,8 +217,8 @@ public void IntegrationTest_LogParameter_IsAccepted() /// /// Test that report parameter is accepted. /// - [TestMethod] - public void IntegrationTest_ReportParameter_IsAccepted() + [Fact] + public void BuildMark_ReportParameter_IsAccepted() { // Run the application with report parameter var exitCode = Runner.Run( @@ -228,7 +229,7 @@ public void IntegrationTest_ReportParameter_IsAccepted() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); @@ -237,8 +238,8 @@ public void IntegrationTest_ReportParameter_IsAccepted() /// /// Test that depth parameter is accepted. /// - [TestMethod] - public void IntegrationTest_DepthParameter_IsAccepted() + [Fact] + public void BuildMark_DepthParameter_IsAccepted() { // Run the application with depth parameter var exitCode = Runner.Run( @@ -249,7 +250,7 @@ public void IntegrationTest_DepthParameter_IsAccepted() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); @@ -258,8 +259,8 @@ public void IntegrationTest_DepthParameter_IsAccepted() /// /// Test that build-version parameter is accepted. /// - [TestMethod] - public void IntegrationTest_BuildVersionParameter_IsAccepted() + [Fact] + public void BuildMark_BuildVersionParameter_IsAccepted() { // Run the application with build-version parameter var exitCode = Runner.Run( @@ -270,7 +271,7 @@ public void IntegrationTest_BuildVersionParameter_IsAccepted() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); @@ -279,8 +280,8 @@ public void IntegrationTest_BuildVersionParameter_IsAccepted() /// /// Test that results parameter is accepted. /// - [TestMethod] - public void IntegrationTest_ResultsParameter_IsAccepted() + [Fact] + public void BuildMark_ResultsParameter_IsAccepted() { // Run the application with results parameter var exitCode = Runner.Run( @@ -291,7 +292,7 @@ public void IntegrationTest_ResultsParameter_IsAccepted() "--help"); // Verify success - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); // Verify it's not an argument error Assert.DoesNotContain("Unsupported argument", output); @@ -300,8 +301,8 @@ public void IntegrationTest_ResultsParameter_IsAccepted() /// /// Test that the report generates a markdown file with version information. /// - [TestMethod] - public void IntegrationTest_Report_GeneratesMarkdownWithVersionInformation() + [Fact] + public void BuildMark_Report_GeneratesMarkdownWithVersionInformation() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -316,7 +317,7 @@ public void IntegrationTest_Report_GeneratesMarkdownWithVersionInformation() Program.Run(context); // Assert: report file contains markdown title and version information - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("# Build Report", content); Assert.Contains("## Version Information", content); @@ -334,8 +335,8 @@ public void IntegrationTest_Report_GeneratesMarkdownWithVersionInformation() /// /// Test that the report contains changes and bug fixes with hyperlinks. /// - [TestMethod] - public void IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks() + [Fact] + public void BuildMark_Report_ContainsChangesAndBugFixesWithHyperlinks() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -350,7 +351,7 @@ public void IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks() Program.Run(context); // Assert: report contains changes and bug fixes sections with linked items - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("## Changes", content); Assert.Contains("## Bugs Fixed", content); @@ -368,8 +369,8 @@ public void IntegrationTest_Report_ContainsChangesAndBugFixesWithHyperlinks() /// /// Test that the report shows the version range from the previous release. /// - [TestMethod] - public void IntegrationTest_Report_ShowsVersionRangeFromPreviousRelease() + [Fact] + public void BuildMark_Report_ShowsVersionRangeFromPreviousRelease() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -384,7 +385,7 @@ public void IntegrationTest_Report_ShowsVersionRangeFromPreviousRelease() Program.Run(context); // Assert: report identifies the previous version as the baseline of the version range - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("Previous Version", content); Assert.Contains("ver-1.1.0", content); @@ -401,8 +402,8 @@ public void IntegrationTest_Report_ShowsVersionRangeFromPreviousRelease() /// /// Test that the report includes known issues when the flag is set. /// - [TestMethod] - public void IntegrationTest_Report_IncludesKnownIssues_WhenFlagIsSet() + [Fact] + public void BuildMark_Report_IncludesKnownIssues_WhenFlagIsSet() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -417,7 +418,7 @@ public void IntegrationTest_Report_IncludesKnownIssues_WhenFlagIsSet() Program.Run(context); // Assert: report includes a known issues section - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("## Known Issues", content); } @@ -433,8 +434,8 @@ public void IntegrationTest_Report_IncludesKnownIssues_WhenFlagIsSet() /// /// Test that report-depth 2 uses level-two headings in the report. /// - [TestMethod] - public void IntegrationTest_Report_DepthTwo_UsesLevelTwoHeadings() + [Fact] + public void BuildMark_Report_DepthTwo_UsesLevelTwoHeadings() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -449,7 +450,7 @@ public void IntegrationTest_Report_DepthTwo_UsesLevelTwoHeadings() Program.Run(context); // Assert: report uses level-two heading for the title and level-three for sections - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("## Build Report", content); Assert.Contains("### Version Information", content); @@ -466,8 +467,8 @@ public void IntegrationTest_Report_DepthTwo_UsesLevelTwoHeadings() /// /// Test that the --lint flag is accepted and validates configuration without error. /// - [TestMethod] - public void IntegrationTest_LintFlag_IsAccepted() + [Fact] + public void BuildMark_LintFlag_IsAccepted() { // Act: run the application with --lint flag var exitCode = Runner.Run( @@ -477,15 +478,15 @@ public void IntegrationTest_LintFlag_IsAccepted() "--lint"); // Assert: tool completes successfully - Assert.AreEqual(0, exitCode); + Assert.Equal(0, exitCode); Assert.DoesNotContain("Unsupported argument", output); } /// /// Test that the tool consumes the .buildmark.yaml configuration file during report generation. /// - [TestMethod] - public void IntegrationTest_Report_ConsumesConfigurationFileDuringGeneration() + [Fact] + public void BuildMark_Report_ConsumesConfigurationFileDuringGeneration() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -500,9 +501,9 @@ public void IntegrationTest_Report_ConsumesConfigurationFileDuringGeneration() Program.Run(context); // Assert: tool succeeds and produces a report (configuration was loaded without error) - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); - Assert.IsFalse(string.IsNullOrWhiteSpace(content)); + Assert.False(string.IsNullOrWhiteSpace(content)); Assert.Contains("# Build Report", content); } finally @@ -517,8 +518,8 @@ public void IntegrationTest_Report_ConsumesConfigurationFileDuringGeneration() /// /// Test that the tool uses the configured repository connector to fetch build data. /// - [TestMethod] - public void IntegrationTest_Report_UsesConnectorForBuildData() + [Fact] + public void BuildMark_Report_UsesConnectorForBuildData() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -533,7 +534,7 @@ public void IntegrationTest_Report_UsesConnectorForBuildData() Program.Run(context); // Assert: report contains data sourced from the mock connector - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("Update documentation", content); Assert.Contains("Fix bug in Y", content); @@ -550,8 +551,8 @@ public void IntegrationTest_Report_UsesConnectorForBuildData() /// /// Test that the report contains section definitions matching expected structure. /// - [TestMethod] - public void IntegrationTest_Report_ContainsSectionDefinitions() + [Fact] + public void BuildMark_Report_ContainsSectionDefinitions() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -566,7 +567,7 @@ public void IntegrationTest_Report_ContainsSectionDefinitions() Program.Run(context); // Assert: report contains the expected section headings - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); Assert.Contains("## Version Information", content); Assert.Contains("## Changes", content); @@ -584,8 +585,8 @@ public void IntegrationTest_Report_ContainsSectionDefinitions() /// /// Test that items are routed to the correct report sections by type. /// - [TestMethod] - public void IntegrationTest_Report_RoutesItemsToCorrectSections() + [Fact] + public void BuildMark_Report_RoutesItemsToCorrectSections() { // Arrange: create a temporary report file path var reportFile = Path.GetTempFileName(); @@ -600,12 +601,12 @@ public void IntegrationTest_Report_RoutesItemsToCorrectSections() Program.Run(context); // Assert: bug items appear in Bugs Fixed section; non-bug items appear in Changes section - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); var content = File.ReadAllText(reportFile); var changesStart = content.IndexOf("## Changes", StringComparison.Ordinal); var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.IsGreaterThanOrEqualTo(0, changesStart, "Report must contain Changes section"); - Assert.IsGreaterThanOrEqualTo(0, bugsStart, "Report must contain Bugs Fixed section"); + Assert.True(changesStart >= 0, "Report must contain Changes section"); + Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); // Bug-typed item "Fix bug in Y" should be in Bugs Fixed section var bugsSection = content[bugsStart..]; @@ -627,8 +628,8 @@ public void IntegrationTest_Report_RoutesItemsToCorrectSections() /// /// Test that the tool recognizes a buildmark code block in item descriptions. /// - [TestMethod] - public void IntegrationTest_Report_RecognizesBuildmarkCodeBlock() + [Fact] + public void BuildMark_Report_RecognizesBuildmarkCodeBlock() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -640,8 +641,8 @@ public void IntegrationTest_Report_RecognizesBuildmarkCodeBlock() /// /// Test that the tool supports a visibility field in the buildmark block. /// - [TestMethod] - public void IntegrationTest_Report_VisibilityFieldControlsInclusion() + [Fact] + public void BuildMark_Report_VisibilityFieldControlsInclusion() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -654,8 +655,8 @@ public void IntegrationTest_Report_VisibilityFieldControlsInclusion() /// /// Test that the tool includes an item when visibility is set to public. /// - [TestMethod] - public void IntegrationTest_Report_PublicVisibility_IncludesItem() + [Fact] + public void BuildMark_Report_PublicVisibility_IncludesItem() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -667,8 +668,8 @@ public void IntegrationTest_Report_PublicVisibility_IncludesItem() /// /// Test that the tool excludes an item when visibility is set to internal. /// - [TestMethod] - public void IntegrationTest_Report_InternalVisibility_ExcludesItem() + [Fact] + public void BuildMark_Report_InternalVisibility_ExcludesItem() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -680,15 +681,15 @@ public void IntegrationTest_Report_InternalVisibility_ExcludesItem() /// /// Test that the tool supports a type field in the buildmark block to override classification. /// - [TestMethod] - public void IntegrationTest_Report_TypeFieldOverridesClassification() + [Fact] + public void BuildMark_Report_TypeFieldOverridesClassification() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); // Assert: item originally typed as feature was reclassified to bug (appears in Bugs Fixed) var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.IsGreaterThanOrEqualTo(0, bugsStart, "Report must contain Bugs Fixed section"); + Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); var bugsSection = content[bugsStart..]; Assert.Contains("Reclassified as bug", bugsSection); @@ -701,15 +702,15 @@ public void IntegrationTest_Report_TypeFieldOverridesClassification() /// /// Test that the tool classifies an item as a bug fix when type is set to bug. /// - [TestMethod] - public void IntegrationTest_Report_TypeBug_PlacesItemInBugsFixed() + [Fact] + public void BuildMark_Report_TypeBug_PlacesItemInBugsFixed() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); // Assert: item with type: bug override appears in the Bugs Fixed section var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.IsGreaterThanOrEqualTo(0, bugsStart, "Report must contain Bugs Fixed section"); + Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); var bugsSection = content[bugsStart..]; Assert.Contains("Reclassified as bug", bugsSection); } @@ -717,8 +718,8 @@ public void IntegrationTest_Report_TypeBug_PlacesItemInBugsFixed() /// /// Test that the tool classifies an item as a feature when type is set to feature. /// - [TestMethod] - public void IntegrationTest_Report_TypeFeature_PlacesItemInChanges() + [Fact] + public void BuildMark_Report_TypeFeature_PlacesItemInChanges() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -726,8 +727,8 @@ public void IntegrationTest_Report_TypeFeature_PlacesItemInChanges() // Assert: item with type: feature override appears in the Changes section var changesStart = content.IndexOf("## Changes", StringComparison.Ordinal); var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.IsGreaterThanOrEqualTo(0, changesStart, "Report must contain Changes section"); - Assert.IsGreaterThan(changesStart, bugsStart, "Bugs Fixed section must follow Changes section"); + Assert.True(changesStart >= 0, "Report must contain Changes section"); + Assert.True(bugsStart > changesStart, "Bugs Fixed section must follow Changes section"); var changesSection = content[changesStart..bugsStart]; Assert.Contains("Reclassified as feature", changesSection); } @@ -735,8 +736,8 @@ public void IntegrationTest_Report_TypeFeature_PlacesItemInChanges() /// /// Test that the tool supports an affected-versions field in the buildmark block. /// - [TestMethod] - public void IntegrationTest_Report_AffectedVersionsField_ProcessesSuccessfully() + [Fact] + public void BuildMark_Report_AffectedVersionsField_ProcessesSuccessfully() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -749,8 +750,8 @@ public void IntegrationTest_Report_AffectedVersionsField_ProcessesSuccessfully() /// /// Test that the affected-versions field uses interval notation. /// - [TestMethod] - public void IntegrationTest_Report_AffectedVersionsInterval_ParsesNotation() + [Fact] + public void BuildMark_Report_AffectedVersionsInterval_ParsesNotation() { // Arrange: generate a report using the controls mock connector // (the connector creates an item with interval notation "[1.0.0, 2.0.0)") @@ -758,7 +759,7 @@ public void IntegrationTest_Report_AffectedVersionsInterval_ParsesNotation() // Assert: the item with interval notation was parsed and routed to Bugs Fixed section var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.IsGreaterThanOrEqualTo(0, bugsStart, "Report must contain Bugs Fixed section"); + Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); var bugsSection = content[bugsStart..]; Assert.Contains("Bug with affected versions", bugsSection); } @@ -766,8 +767,8 @@ public void IntegrationTest_Report_AffectedVersionsInterval_ParsesNotation() /// /// Test that the tool recognizes a buildmark block wrapped in an HTML comment. /// - [TestMethod] - public void IntegrationTest_Report_HiddenBuildmarkBlock_IsRecognized() + [Fact] + public void BuildMark_Report_HiddenBuildmarkBlock_IsRecognized() { // Arrange: generate a report using the controls mock connector var content = GenerateControlsMockReport(); @@ -775,58 +776,291 @@ public void IntegrationTest_Report_HiddenBuildmarkBlock_IsRecognized() // Assert: item with HTML-comment-wrapped buildmark block is recognized and processed // The hidden block specifies type: bug, so the item should appear in Bugs Fixed section var bugsStart = content.IndexOf("## Bugs Fixed", StringComparison.Ordinal); - Assert.IsGreaterThanOrEqualTo(0, bugsStart, "Report must contain Bugs Fixed section"); + Assert.True(bugsStart >= 0, "Report must contain Bugs Fixed section"); var bugsSection = content[bugsStart..]; Assert.Contains("Hidden block item", bugsSection); } // ───────────────────────────────────────────────────────────────────────── - // Azure DevOps Integration (placeholder tests — Phase 2) + // Azure DevOps Integration // ───────────────────────────────────────────────────────────────────────── /// - /// Placeholder: verify that BuildMark generates markdown with version information - /// from an Azure DevOps repository. - /// Phase 2: Replace with a real end-to-end test once the AzureDevOps connector - /// implementation is available. + /// Test that BuildMark generates a markdown report with version information + /// from a mocked Azure DevOps repository. /// - [TestMethod] - public void IntegrationTest_AzureDevOps_Report_GeneratesMarkdownWithVersionInformation() + [Fact] + public void BuildMark_AzureDevOps_Report_GeneratesMarkdownWithVersionInformation() { - // Phase 2: Implement when the AzureDevOpsRepoConnector is available. - // This test will verify that running BuildMark against a mocked Azure DevOps - // repository produces a markdown report containing correct version information. - Assert.IsTrue(File.Exists(_dllPath)); + // Arrange: create a temporary report file path + var reportFile = Path.GetTempFileName(); + try + { + // Set up a mocked REST handler with a single version tag and commit + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse(new MockAdoTag("v1.0.0", "abc123")) + .AddCommitsResponse(new MockAdoCommit("abc123")) + .AddPullRequestsResponse() + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var adoConnector = CreateMockAdoConnector(mockHttpClient, "abc123"); + + // Create context with Azure DevOps connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "1.0.0", "--report", reportFile, "--silent"], + () => adoConnector); + + // Act: run the program + Program.Run(context); + + // Assert: report file contains markdown title and version information + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("# Build Report", content); + Assert.Contains("## Version Information", content); + Assert.Contains("1.0.0", content); + } + finally + { + if (File.Exists(reportFile)) + { + File.Delete(reportFile); + } + } + } + + /// + /// Test that BuildMark generates a report containing changes and bug fixes + /// with hyperlinks from a mocked Azure DevOps repository. + /// + [Fact] + public void BuildMark_AzureDevOps_Report_ContainsChangesAndBugFixesWithHyperlinks() + { + // Arrange: create a temporary report file path + var reportFile = Path.GetTempFileName(); + try + { + // Set up mocked REST data with two versions, two pull requests, and linked work items + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v1.1.0", "commit3"), + new MockAdoTag("v1.0.0", "commit1")) + .AddCommitsResponse( + new MockAdoCommit("commit3"), + new MockAdoCommit("commit2"), + new MockAdoCommit("commit1")) + .AddPullRequestsResponse( + new MockAdoPullRequest(101, "Add new feature", "completed", "commit3"), + new MockAdoPullRequest(100, "Fix critical bug", "completed", "commit2")) + .AddPullRequestWorkItemsResponse("repo", 101, 201) + .AddPullRequestWorkItemsResponse("repo", 100, 200) + .AddWorkItemsResponse( + new MockAdoWorkItem(201, "New feature work item", "User Story"), + new MockAdoWorkItem(200, "Bug work item", "Bug")) + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var adoConnector = CreateMockAdoConnector(mockHttpClient, "commit3"); + + // Create context with Azure DevOps connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "1.1.0", "--report", reportFile, "--silent"], + () => adoConnector); + + // Act: run the program + Program.Run(context); + + // Assert: report contains changes and bug fixes sections with linked items + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("## Changes", content); + Assert.Contains("## Bugs Fixed", content); + Assert.Contains("](", content); // markdown hyperlink syntax [text](url) + } + finally + { + if (File.Exists(reportFile)) + { + File.Delete(reportFile); + } + } + } + + /// + /// Test that BuildMark shows the correct version range from the previous release + /// in a mocked Azure DevOps repository. + /// + [Fact] + public void BuildMark_AzureDevOps_Report_ShowsVersionRangeFromPreviousRelease() + { + // Arrange: create a temporary report file path + var reportFile = Path.GetTempFileName(); + try + { + // Set up mocked REST data with three version tags so previous version selection is exercised + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v2.0.0", "commit3"), + new MockAdoTag("v1.1.0", "commit2"), + new MockAdoTag("v1.0.0", "commit1")) + .AddCommitsResponse( + new MockAdoCommit("commit3"), + new MockAdoCommit("commit2"), + new MockAdoCommit("commit1")) + .AddPullRequestsResponse() + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var adoConnector = CreateMockAdoConnector(mockHttpClient, "commit3"); + + // Create context with Azure DevOps connector injected for deterministic output + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--silent"], + () => adoConnector); + + // Act: run the program + Program.Run(context); + + // Assert: report identifies the previous version as the baseline of the version range + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("Previous Version", content); + Assert.Contains("v1.1.0", content); + } + finally + { + if (File.Exists(reportFile)) + { + File.Delete(reportFile); + } + } } /// - /// Placeholder: verify that BuildMark generates a report containing changes and bug - /// fixes with hyperlinks from an Azure DevOps repository. - /// Phase 2: Replace with a real end-to-end test once the AzureDevOps connector - /// implementation is available. + /// Test that the --results flag writes a TRX file when --validate is specified. /// - [TestMethod] - public void IntegrationTest_AzureDevOps_Report_ContainsChangesAndBugFixesWithHyperlinks() + [Fact] + public void BuildMark_ResultsParameter_WritesTrxFile() { - // Phase 2: Implement when the AzureDevOpsRepoConnector is available. - // This test will verify that the generated report contains hyperlinked work items - // and pull requests from Azure DevOps. - Assert.IsTrue(File.Exists(_dllPath)); + // Arrange: create a temporary TRX results file path + var resultsFile = Path.ChangeExtension(Path.GetTempFileName(), ".trx"); + try + { + // Act: run the application with --validate and --results pointing to a TRX file + var exitCode = Runner.Run( + out _, + "dotnet", + _dllPath, + "--validate", + "--results", resultsFile, + "--silent"); + + // Assert: tool succeeds and writes a TRX file containing the TestRun XML element + Assert.Equal(0, exitCode); + Assert.True(File.Exists(resultsFile), "TRX results file should have been created"); + var content = File.ReadAllText(resultsFile); + Assert.Contains(" + /// Test that the --results flag writes a JUnit XML file when --validate is specified. + /// + [Fact] + public void BuildMark_ResultsParameter_WritesJUnitFile() + { + // Arrange: create a temporary JUnit XML results file path + var resultsFile = Path.ChangeExtension(Path.GetTempFileName(), ".xml"); + try + { + // Act: run the application with --validate and --results pointing to an XML file + var exitCode = Runner.Run( + out _, + "dotnet", + _dllPath, + "--validate", + "--results", resultsFile, + "--silent"); + + // Assert: tool succeeds and writes an XML file containing the testsuites element + Assert.Equal(0, exitCode); + Assert.True(File.Exists(resultsFile), "JUnit XML results file should have been created"); + var content = File.ReadAllText(resultsFile); + Assert.Contains(" + /// Test that the tool reads the connector type from the .buildmark.yaml configuration file. + /// + [Fact] + public async Task BuildMark_Config_ConnectorType_ReadFromConfigFile() + { + // Arrange: create a temporary directory containing a .buildmark.yaml with an explicit connector type + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + try + { + // Write a configuration file that explicitly selects the azure-devops connector type + var configContent = "connector:\n type: azure-devops\n"; + await File.WriteAllTextAsync( + Path.Combine(tempDir, ".buildmark.yaml"), + configContent, + TestContext.Current.CancellationToken); + + // Act: read configuration and create the connector through the factory + var loadResult = await BuildMarkConfigReader.ReadAsync(tempDir); + var connector = RepoConnectorFactory.Create(loadResult.Config?.Connector); + + // Assert: configuration was parsed without errors and the factory created an Azure DevOps connector + Assert.False(loadResult.HasErrors, "Configuration file should load without errors"); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, recursive: true); + } + } } /// - /// Placeholder: verify that BuildMark shows the correct version range from the - /// previous release in an Azure DevOps repository. - /// Phase 2: Replace with a real end-to-end test once the AzureDevOps connector - /// implementation is available. + /// Creates a mock Azure DevOps connector with pre-configured git command responses + /// for use in integration tests. /// - [TestMethod] - public void IntegrationTest_AzureDevOps_Report_ShowsVersionRangeFromPreviousRelease() + /// Mock HTTP client for REST API calls. + /// Current commit hash to return from git rev-parse HEAD. + /// Configured MockableAzureDevOpsRepoConnector ready for use in a connector factory. + private static MockableAzureDevOpsRepoConnector CreateMockAdoConnector( + HttpClient mockHttpClient, + string currentCommitHash) { - // Phase 2: Implement when the AzureDevOpsRepoConnector is available. - // This test will verify that the previous version tag is correctly identified - // and the change range is accurately reported from an Azure DevOps repository. - Assert.IsTrue(File.Exists(_dllPath)); + var connector = new MockableAzureDevOpsRepoConnector(mockHttpClient); + connector.SetCommandResponse( + "git remote get-url origin", + "https://dev.azure.com/org/project/_git/repo"); + connector.SetCommandResponse("git rev-parse HEAD", currentCommitHash); + connector.SetCommandResponse( + "az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv", + "mock-token"); + return connector; } /// @@ -847,7 +1081,7 @@ private static string GenerateControlsMockReport() Program.Run(context); // Verify success and return content - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); return File.ReadAllText(reportFile); } finally diff --git a/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsParserTests.cs b/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsParserTests.cs index 4353f70e..5d8ba30c 100644 --- a/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsParserTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsParserTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.BuildMark.Tests.ItemControls; /// /// Unit tests for ItemControlsParser.Parse method. /// -[TestClass] public class ItemControlsParserTests { /// /// Test that Parse returns null for null description. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithNullDescription_ReturnsNull() { // Arrange - null input @@ -40,13 +39,13 @@ public void ItemControlsParser_Parse_WithNullDescription_ReturnsNull() var result = ItemControlsParser.Parse(null); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse returns null for empty description. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithEmptyDescription_ReturnsNull() { // Arrange @@ -56,13 +55,13 @@ public void ItemControlsParser_Parse_WithEmptyDescription_ReturnsNull() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse returns null when there is no buildmark block. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithNoBlock_ReturnsNull() { // Arrange @@ -72,13 +71,13 @@ public void ItemControlsParser_Parse_WithNoBlock_ReturnsNull() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse returns "public" visibility when visibility is set to public. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithVisibilityPublic_ReturnsPublicVisibility() { // Arrange @@ -88,14 +87,14 @@ public void ItemControlsParser_Parse_WithVisibilityPublic_ReturnsPublicVisibilit var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("public", result.Visibility); + Assert.NotNull(result); + Assert.Equal("public", result.Visibility); } /// /// Test that Parse returns "internal" visibility when visibility is set to internal. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithVisibilityInternal_ReturnsInternalVisibility() { // Arrange @@ -105,14 +104,14 @@ public void ItemControlsParser_Parse_WithVisibilityInternal_ReturnsInternalVisib var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("internal", result.Visibility); + Assert.NotNull(result); + Assert.Equal("internal", result.Visibility); } /// /// Test that Parse returns "bug" type when type is set to bug. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithTypeBug_ReturnsBugType() { // Arrange @@ -122,14 +121,14 @@ public void ItemControlsParser_Parse_WithTypeBug_ReturnsBugType() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("bug", result.Type); + Assert.NotNull(result); + Assert.Equal("bug", result.Type); } /// /// Test that Parse returns "feature" type when type is set to feature. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithTypeFeature_ReturnsFeatureType() { // Arrange @@ -139,14 +138,14 @@ public void ItemControlsParser_Parse_WithTypeFeature_ReturnsFeatureType() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("feature", result.Type); + Assert.NotNull(result); + Assert.Equal("feature", result.Type); } /// /// Test that Parse returns correct interval set for affected-versions. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithAffectedVersions_ReturnsIntervalSet() { // Arrange @@ -156,17 +155,17 @@ public void ItemControlsParser_Parse_WithAffectedVersions_ReturnsIntervalSet() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); - Assert.AreEqual("1.0.0", result.AffectedVersions.Intervals[0].LowerBound); - Assert.AreEqual("2.0.0", result.AffectedVersions.Intervals[0].UpperBound); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); + Assert.Equal("1.0.0", result.AffectedVersions.Intervals[0].LowerBound); + Assert.Equal("2.0.0", result.AffectedVersions.Intervals[0].UpperBound); } /// /// Test that Parse recognizes a buildmark block hidden inside an HTML comment. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithHiddenBlock_ReturnsControls() { // Arrange - buildmark block wrapped in an HTML comment to hide from GitHub rendered view @@ -176,14 +175,14 @@ public void ItemControlsParser_Parse_WithHiddenBlock_ReturnsControls() var result = ItemControlsParser.Parse(description); // Assert - HTML comment delimiters are stripped, exposing the block - Assert.IsNotNull(result); - Assert.AreEqual("public", result.Visibility); + Assert.NotNull(result); + Assert.Equal("public", result.Visibility); } /// /// Test that Parse recognizes internal visibility from a block hidden inside an HTML comment. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithHiddenBlockVisibilityInternal_ReturnsInternalVisibility() { // Arrange - internal visibility block wrapped in HTML comment @@ -193,14 +192,14 @@ public void ItemControlsParser_Parse_WithHiddenBlockVisibilityInternal_ReturnsIn var result = ItemControlsParser.Parse(description); // Assert - hidden block is parsed and returns internal visibility - Assert.IsNotNull(result); - Assert.AreEqual("internal", result.Visibility); + Assert.NotNull(result); + Assert.Equal("internal", result.Visibility); } /// /// Test that Parse ignores unknown keys and returns null when no recognized keys are found. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithUnknownKey_IgnoresKey() { // Arrange - block with only unknown keys, which are silently ignored @@ -210,13 +209,13 @@ public void ItemControlsParser_Parse_WithUnknownKey_IgnoresKey() var result = ItemControlsParser.Parse(description); // Assert - no recognized keys, so null is returned - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse ignores an unrecognized visibility value and treats the field as absent. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithUnrecognizedVisibilityValue_IgnoresValue() { // Arrange - visibility value is not "public" or "internal" @@ -226,13 +225,13 @@ public void ItemControlsParser_Parse_WithUnrecognizedVisibilityValue_IgnoresValu var result = ItemControlsParser.Parse(description); // Assert - unrecognized value is ignored; no valid fields → null result - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse ignores an unrecognized type value and treats the field as absent. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithUnrecognizedTypeValue_IgnoresValue() { // Arrange - type value is not "bug" or "feature" @@ -242,13 +241,13 @@ public void ItemControlsParser_Parse_WithUnrecognizedTypeValue_IgnoresValue() var result = ItemControlsParser.Parse(description); // Assert - unrecognized value is ignored; no valid fields → null result - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse returns complete info when all fields are present. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_AllFields_ReturnsCompleteInfo() { // Arrange @@ -258,17 +257,17 @@ public void ItemControlsParser_Parse_AllFields_ReturnsCompleteInfo() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("public", result.Visibility); - Assert.AreEqual("bug", result.Type); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); + Assert.NotNull(result); + Assert.Equal("public", result.Visibility); + Assert.Equal("bug", result.Type); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); } /// /// Test that Parse ignores an unrecognized affected-versions value and treats the field as absent. /// - [TestMethod] + [Fact] public void ItemControlsParser_Parse_WithUnrecognizedAffectedVersionsValue_IgnoresValue() { // Arrange - affected-versions value is not a valid version interval @@ -278,7 +277,7 @@ public void ItemControlsParser_Parse_WithUnrecognizedAffectedVersionsValue_Ignor var result = ItemControlsParser.Parse(description); // Assert - unrecognized value is ignored; no valid fields → null result - Assert.IsNull(result); + Assert.Null(result); } } diff --git a/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsTests.cs b/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsTests.cs index 282262ff..c51cc7d3 100644 --- a/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/ItemControls/ItemControlsTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.BuildMark.Tests.ItemControls; /// /// Subsystem-level tests for ItemControls parsing. /// -[TestClass] public class ItemControlsTests { /// /// Test that the subsystem returns "public" visibility when specified. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithVisibilityPublic_ReturnsPublicVisibility() { // Arrange @@ -41,14 +40,14 @@ public void ItemControls_Parse_WithVisibilityPublic_ReturnsPublicVisibility() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("public", result.Visibility); + Assert.NotNull(result); + Assert.Equal("public", result.Visibility); } /// /// Test that the subsystem returns "internal" visibility when specified. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithVisibilityInternal_ReturnsInternalVisibility() { // Arrange @@ -58,14 +57,14 @@ public void ItemControls_Parse_WithVisibilityInternal_ReturnsInternalVisibility( var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("internal", result.Visibility); + Assert.NotNull(result); + Assert.Equal("internal", result.Visibility); } /// /// Test that the subsystem returns "bug" type when specified. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithTypeBug_ReturnsBugType() { // Arrange @@ -75,14 +74,14 @@ public void ItemControls_Parse_WithTypeBug_ReturnsBugType() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("bug", result.Type); + Assert.NotNull(result); + Assert.Equal("bug", result.Type); } /// /// Test that the subsystem returns "feature" type when specified. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithTypeFeature_ReturnsFeatureType() { // Arrange @@ -92,14 +91,14 @@ public void ItemControls_Parse_WithTypeFeature_ReturnsFeatureType() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("feature", result.Type); + Assert.NotNull(result); + Assert.Equal("feature", result.Type); } /// /// Test that the subsystem parses an affected-versions interval set correctly. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithAffectedVersions_ReturnsIntervalSet() { // Arrange - multi-range affected-versions field @@ -109,19 +108,19 @@ public void ItemControls_Parse_WithAffectedVersions_ReturnsIntervalSet() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(2, result.AffectedVersions.Intervals); - Assert.IsNull(result.AffectedVersions.Intervals[0].LowerBound); - Assert.AreEqual("1.0.1", result.AffectedVersions.Intervals[0].UpperBound); - Assert.AreEqual("1.1.0", result.AffectedVersions.Intervals[1].LowerBound); - Assert.AreEqual("1.2.0", result.AffectedVersions.Intervals[1].UpperBound); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Equal(2, result.AffectedVersions.Intervals.Count); + Assert.Null(result.AffectedVersions.Intervals[0].LowerBound); + Assert.Equal("1.0.1", result.AffectedVersions.Intervals[0].UpperBound); + Assert.Equal("1.1.0", result.AffectedVersions.Intervals[1].LowerBound); + Assert.Equal("1.2.0", result.AffectedVersions.Intervals[1].UpperBound); } /// /// Test that the subsystem recognizes a buildmark block hidden inside an HTML comment. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithHiddenBlock_ReturnsControls() { // Arrange - buildmark block wrapped in HTML comment to hide from GitHub rendered view @@ -131,14 +130,14 @@ public void ItemControls_Parse_WithHiddenBlock_ReturnsControls() var result = ItemControlsParser.Parse(description); // Assert - HTML comment delimiters are stripped, exposing and parsing the block - Assert.IsNotNull(result); - Assert.AreEqual("feature", result.Type); + Assert.NotNull(result); + Assert.Equal("feature", result.Type); } /// /// Test that the subsystem returns null when no buildmark block is present. /// - [TestMethod] + [Fact] public void ItemControls_Parse_WithNoBlock_ReturnsNull() { // Arrange - description with no buildmark fenced block @@ -148,13 +147,13 @@ public void ItemControls_Parse_WithNoBlock_ReturnsNull() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that a single version interval in affected-versions is returned correctly. /// - [TestMethod] + [Fact] public void ItemControls_VersionInterval_Parse_SingleInterval_ReturnsInterval() { // Arrange @@ -164,15 +163,15 @@ public void ItemControls_VersionInterval_Parse_SingleInterval_ReturnsInterval() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); } /// /// Test that multiple version intervals in affected-versions are returned correctly. /// - [TestMethod] + [Fact] public void ItemControls_VersionInterval_Parse_MultipleIntervals_ReturnsIntervalSet() { // Arrange @@ -182,15 +181,15 @@ public void ItemControls_VersionInterval_Parse_MultipleIntervals_ReturnsInterval var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(2, result.AffectedVersions.Intervals); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Equal(2, result.AffectedVersions.Intervals.Count); } /// /// Test that LowerInclusive is true when '[' is used in affected-versions. /// - [TestMethod] + [Fact] public void ItemControls_VersionInterval_Parse_InclusiveLowerBound_IsInclusive() { // Arrange @@ -200,16 +199,16 @@ public void ItemControls_VersionInterval_Parse_InclusiveLowerBound_IsInclusive() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); - Assert.IsTrue(result.AffectedVersions.Intervals[0].LowerInclusive); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); + Assert.True(result.AffectedVersions.Intervals[0].LowerInclusive); } /// /// Test that UpperInclusive is false when ')' is used in affected-versions. /// - [TestMethod] + [Fact] public void ItemControls_VersionInterval_Parse_ExclusiveUpperBound_IsExclusive() { // Arrange @@ -219,16 +218,16 @@ public void ItemControls_VersionInterval_Parse_ExclusiveUpperBound_IsExclusive() var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); - Assert.IsFalse(result.AffectedVersions.Intervals[0].UpperInclusive); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); + Assert.False(result.AffectedVersions.Intervals[0].UpperInclusive); } /// /// Test that LowerBound is null when lower bound is empty in affected-versions. /// - [TestMethod] + [Fact] public void ItemControls_VersionInterval_Parse_UnboundedLower_HasNullLowerBound() { // Arrange @@ -238,16 +237,16 @@ public void ItemControls_VersionInterval_Parse_UnboundedLower_HasNullLowerBound( var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); - Assert.IsNull(result.AffectedVersions.Intervals[0].LowerBound); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); + Assert.Null(result.AffectedVersions.Intervals[0].LowerBound); } /// /// Test that UpperBound is null when upper bound is empty in affected-versions. /// - [TestMethod] + [Fact] public void ItemControls_VersionInterval_Parse_UnboundedUpper_HasNullUpperBound() { // Arrange @@ -257,10 +256,10 @@ public void ItemControls_VersionInterval_Parse_UnboundedUpper_HasNullUpperBound( var result = ItemControlsParser.Parse(description); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(1, result.AffectedVersions.Intervals); - Assert.IsNull(result.AffectedVersions.Intervals[0].UpperBound); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Single(result.AffectedVersions.Intervals); + Assert.Null(result.AffectedVersions.Intervals[0].UpperBound); } } diff --git a/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs b/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs index 2d376997..da4505b4 100644 --- a/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/ProgramTests.cs @@ -18,34 +18,37 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using DemaConsulting.BuildMark.BuildNotes; using DemaConsulting.BuildMark.Cli; +using DemaConsulting.BuildMark.RepoConnectors; +using DemaConsulting.BuildMark.RepoConnectors.Mock; +using DemaConsulting.BuildMark.Version; namespace DemaConsulting.BuildMark.Tests; /// /// Tests for the Program class. /// -[TestClass] public class ProgramTests { /// /// Test that the version property returns a valid version string. /// - [TestMethod] + [Fact] public void Program_Version_ReturnsValidVersion() { // Retrieve version string from Program var version = Program.Version; // Verify version is not null or empty - Assert.IsNotNull(version); - Assert.IsFalse(string.IsNullOrWhiteSpace(version)); + Assert.NotNull(version); + Assert.False(string.IsNullOrWhiteSpace(version)); } /// /// Test that Run with version flag outputs version to console. /// - [TestMethod] + [Fact] public void Program_Run_VersionFlag_OutputsVersionToConsole() { // Create context with version flag @@ -75,7 +78,7 @@ public void Program_Run_VersionFlag_OutputsVersionToConsole() /// /// Test that Run with help flag outputs help message. /// - [TestMethod] + [Fact] public void Program_Run_HelpFlag_OutputsHelpMessage() { AssertHelpFlagOutputsHelpMessage("-h"); @@ -84,7 +87,7 @@ public void Program_Run_HelpFlag_OutputsHelpMessage() /// /// Test that Run with question-mark help flag outputs help message. /// - [TestMethod] + [Fact] public void Program_Run_QuestionMarkFlag_OutputsHelpMessage() { AssertHelpFlagOutputsHelpMessage("-?"); @@ -93,7 +96,7 @@ public void Program_Run_QuestionMarkFlag_OutputsHelpMessage() /// /// Test that Run with long help flag outputs help message. /// - [TestMethod] + [Fact] public void Program_Run_LongHelpFlag_OutputsHelpMessage() { AssertHelpFlagOutputsHelpMessage("--help"); @@ -135,7 +138,7 @@ private static void AssertHelpFlagOutputsHelpMessage(string helpFlag) /// /// Test that Run with validate flag outputs validation message. /// - [TestMethod] + [Fact] public void Program_Run_ValidateFlag_OutputsValidationMessage() { // Create context with validate flag @@ -165,7 +168,7 @@ public void Program_Run_ValidateFlag_OutputsValidationMessage() /// /// Test that Run with report and include-known-issues flags generates report with known issues. /// - [TestMethod] + [Fact] public void Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnownIssues() { // Create temporary report file path @@ -173,10 +176,12 @@ public void Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnow try { // Create context with report and include-known-issues flags - using var context = Context.Create(["--report", reportFile, "--include-known-issues"]); + using var context = Context.Create( + ["--build-version", "2.0.0", "--report", reportFile, "--include-known-issues", "--silent"], + () => new MockRepoConnector()); // Verify IncludeKnownIssues property is set - Assert.IsTrue(context.IncludeKnownIssues); + Assert.True(context.IncludeKnownIssues); // Capture console output var originalOut = Console.Out; @@ -189,10 +194,10 @@ public void Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnow Program.Run(context); // Verify report file was created - Assert.IsTrue(File.Exists(reportFile)); + Assert.True(File.Exists(reportFile)); // Verify the context flag was set correctly - Assert.IsTrue(context.IncludeKnownIssues); + Assert.True(context.IncludeKnownIssues); } finally { @@ -213,7 +218,7 @@ public void Program_Run_ReportWithIncludeKnownIssuesFlag_GeneratesReportWithKnow /// /// Test that Run with lint flag succeeds when no configuration file is present. /// - [TestMethod] + [Fact] public void Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero() { // Arrange @@ -223,6 +228,240 @@ public void Program_Run_LintFlagWithoutConfiguration_LeavesExitCodeAtZero() Program.Run(context); // Assert - Assert.AreEqual(0, context.ExitCode); + Assert.Equal(0, context.ExitCode); + } + + /// + /// Test that Run with silent flag suppresses banner output. + /// + [Fact] + public void Program_Run_WithSilentFlag_SuppressesOutput() + { + // Arrange: create context with --silent and --help flags + using var context = Context.Create(["--silent", "--help"]); + + // Capture console output + var originalOut = Console.Out; + using var writer = new StringWriter(); + Console.SetOut(writer); + + try + { + // Act: run program + Program.Run(context); + + // Assert: banner text is suppressed in output + var output = writer.ToString(); + Assert.DoesNotContain("BuildMark version", output); + } + finally + { + Console.SetOut(originalOut); + } + } + + /// + /// Test that Run with log flag writes output to the specified log file. + /// + [Fact] + public void Program_Run_WithLogFlag_WritesToLogFile() + { + // Arrange: create a temporary log file path and context with --log flag + var logFile = Path.ChangeExtension(Path.GetTempFileName(), ".log"); + try + { + // Dispose context before reading file so the log file handle is released + using (var context = Context.Create(["--log", logFile, "--help"])) + { + // Act: run the program (help output is written to log file via context) + Program.Run(context); + } + + // Assert: log file exists and contains output (checked after context is disposed) + Assert.True(File.Exists(logFile), "Log file should have been created"); + var logContent = File.ReadAllText(logFile); + Assert.False(string.IsNullOrWhiteSpace(logContent), "Log file should contain output"); + } + finally + { + if (File.Exists(logFile)) + { + File.Delete(logFile); + } + } + } + + /// + /// Test that Run with results flag writes a results file when --validate is specified. + /// + [Fact] + public void Program_Run_WithResultsFlag_WritesResultsFile() + { + // Arrange: create a temporary TRX results file path and context with --validate and --results flags + var resultsFile = Path.ChangeExtension(Path.GetTempFileName(), ".trx"); + try + { + using var context = Context.Create(["--validate", "--results", resultsFile, "--silent"]); + + // Act: run the program in validate mode + Program.Run(context); + + // Assert: results file was created by the validation run + Assert.Equal(0, context.ExitCode); + Assert.True(File.Exists(resultsFile), "Results file should have been created"); + } + finally + { + if (File.Exists(resultsFile)) + { + File.Delete(resultsFile); + } + } + } + + /// + /// Test that Run with build-version flag accepts and processes a valid version string. + /// + [Fact] + public void Program_Run_WithBuildVersionFlag_AcceptsBuildVersion() + { + // Arrange: create a temporary report file path and context with --build-version flag + var reportFile = Path.GetTempFileName(); + try + { + using var context = Context.Create( + ["--build-version", "3.2.1", "--report", reportFile, "--silent"], + () => new MockRepoConnector()); + + // Act: run the program + Program.Run(context); + + // Assert: program succeeds and the build version appears in the report + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("3.2.1", content); + } + finally + { + if (File.Exists(reportFile)) + { + File.Delete(reportFile); + } + } + } + + /// + /// Test that Run with depth flag applies the configured heading depth to the generated report. + /// + [Fact] + public void Program_Run_WithDepthFlag_SetsHeadingDepth() + { + // Arrange: create a temporary report file path and context with --depth 3 flag + var reportFile = Path.GetTempFileName(); + try + { + using var context = Context.Create( + ["--build-version", "1.0.0", "--report", reportFile, "--depth", "3", "--silent"], + () => new MockRepoConnector()); + + // Act: run the program + Program.Run(context); + + // Assert: report uses level-three heading for the title (depth 3 = ###) + Assert.Equal(0, context.ExitCode); + var content = File.ReadAllText(reportFile); + Assert.Contains("### Build Report", content); + } + finally + { + if (File.Exists(reportFile)) + { + File.Delete(reportFile); + } + } + } + + /// + /// Test that Run with an invalid build version format writes an error and exits with code 1. + /// + [Fact] + public void Program_Run_InvalidBuildVersion_WritesErrorAndSetsExitCode() + { + // Arrange + using var context = Context.Create( + ["--build-version", "not-a-version"], + () => new MockRepoConnector()); + + // Capture Console.Error and Console.Out + using var errorOutput = new StringWriter(); + using var stdOutput = new StringWriter(); + var originalError = Console.Error; + var originalOut = Console.Out; + try + { + Console.SetError(errorOutput); + Console.SetOut(stdOutput); + + // Act + Program.Run(context); + } + finally + { + Console.SetError(originalError); + Console.SetOut(originalOut); + } + + // Assert + Assert.Equal(1, context.ExitCode); + Assert.Contains("Error", errorOutput.ToString()); + } + + /// + /// Test that Run when the connector throws InvalidOperationException during data retrieval + /// writes an error and exits with code 1. + /// + [Fact] + public void Program_Run_ConnectorThrowsInvalidOperationException_WritesErrorAndSetsExitCode() + { + // Arrange: inject a connector whose GetBuildInformationAsync throws + using var context = Context.Create( + ["--build-version", "2.0.0"], + () => new ThrowingConnector()); + + // Capture Console.Error and Console.Out + using var errorOutput = new StringWriter(); + using var stdOutput = new StringWriter(); + var originalError = Console.Error; + var originalOut = Console.Out; + try + { + Console.SetError(errorOutput); + Console.SetOut(stdOutput); + + // Act + Program.Run(context); + } + finally + { + Console.SetError(originalError); + Console.SetOut(originalOut); + } + + // Assert + Assert.Equal(1, context.ExitCode); + Assert.Contains("Error", errorOutput.ToString()); + } + + /// + /// Stub connector that throws on + /// to simulate a connector failure. + /// + private sealed class ThrowingConnector : IRepoConnector + { + /// + public Task GetBuildInformationAsync(VersionTag? version = null) + { + throw new InvalidOperationException("Simulated connector failure"); + } } } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnectorTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnectorTests.cs index 4a6f63f5..58abce9f 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnectorTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRepoConnectorTests.cs @@ -31,7 +31,6 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.AzureDevOps; /// /// Unit tests for the AzureDevOps subsystem. /// -[TestClass] public class AzureDevOpsRepoConnectorTests { // ───────────────────────────────────────────────────────────────────────── @@ -41,7 +40,7 @@ public class AzureDevOpsRepoConnectorTests /// /// Verify that the constructor stores AzureDevOpsConnectorConfig overrides. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_Constructor_WithConfig_StoresConfigurationOverrides() { // Arrange @@ -56,10 +55,10 @@ public void AzureDevOpsRepoConnector_Constructor_WithConfig_StoresConfigurationO var connector = new AzureDevOpsRepoConnector(config); // Assert - Assert.IsNotNull(connector.ConfigurationOverrides); - Assert.AreEqual("https://dev.azure.com/myorg", connector.ConfigurationOverrides.OrganizationUrl); - Assert.AreEqual("myproject", connector.ConfigurationOverrides.Project); - Assert.AreEqual("myrepo", connector.ConfigurationOverrides.Repository); + Assert.NotNull(connector.ConfigurationOverrides); + Assert.Equal("https://dev.azure.com/myorg", connector.ConfigurationOverrides.OrganizationUrl); + Assert.Equal("myproject", connector.ConfigurationOverrides.Project); + Assert.Equal("myrepo", connector.ConfigurationOverrides.Repository); } // ───────────────────────────────────────────────────────────────────────── @@ -69,7 +68,7 @@ public void AzureDevOpsRepoConnector_Constructor_WithConfig_StoresConfigurationO /// /// Verify that GetBuildInformationAsync returns valid build information from mocked data. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation() { // Arrange @@ -86,18 +85,18 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithMockedDa var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("abc123", buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.Changes); - Assert.IsNotNull(buildInfo.Bugs); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("abc123", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); } /// /// Verify that GetBuildInformationAsync selects the correct previous version with multiple versions. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithMultipleVersions_SelectsCorrectPreviousVersion() { // Arrange @@ -120,17 +119,17 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithMultiple var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit2", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit2", buildInfo.BaselineVersionTag.CommitHash); } /// /// Verify that GetBuildInformationAsync gathers changes from pull requests correctly. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithPullRequests_GathersChangesCorrectly() { // Arrange @@ -159,26 +158,26 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithPullRequ var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.1.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.NotNull(buildInfo); + Assert.Equal("1.1.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); // Bug work item should be in bugs - Assert.IsGreaterThanOrEqualTo(1, buildInfo.Bugs.Count, $"Expected at least 1 bug, got {buildInfo.Bugs.Count}"); + Assert.True(buildInfo.Bugs.Count >= 1, $"Expected at least 1 bug, got {buildInfo.Bugs.Count}"); var bugItem = buildInfo.Bugs.FirstOrDefault(b => b.Id == "200"); - Assert.IsNotNull(bugItem, "Work item 200 should be categorized as a bug"); - Assert.AreEqual("Bug fix work item", bugItem.Title); + Assert.True(bugItem != null, "Work item 200 should be categorized as a bug"); + Assert.Equal("Bug fix work item", bugItem.Title); // Feature work item should be in changes - Assert.IsGreaterThanOrEqualTo(1, buildInfo.Changes.Count, $"Expected at least 1 change, got {buildInfo.Changes.Count}"); + Assert.True(buildInfo.Changes.Count >= 1, $"Expected at least 1 change, got {buildInfo.Changes.Count}"); var featureItem = buildInfo.Changes.FirstOrDefault(c => c.Id == "201"); - Assert.IsNotNull(featureItem, "Work item 201 should be categorized as a change"); - Assert.AreEqual("New feature work item", featureItem.Title); + Assert.True(featureItem != null, "Work item 201 should be categorized as a change"); + Assert.Equal("New feature work item", featureItem.Title); } /// /// Verify that GetBuildInformationAsync identifies open work items as known issues. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithOpenWorkItems_IdentifiesKnownIssues() { // Arrange @@ -197,17 +196,17 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithOpenWork var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsGreaterThanOrEqualTo(1, buildInfo.KnownIssues.Count, $"Expected at least 1 known issue, got {buildInfo.KnownIssues.Count}"); + Assert.NotNull(buildInfo); + Assert.True(buildInfo.KnownIssues.Count >= 1, $"Expected at least 1 known issue, got {buildInfo.KnownIssues.Count}"); var knownIssue = buildInfo.KnownIssues.FirstOrDefault(i => i.Id == "301"); - Assert.IsNotNull(knownIssue, "Work item 301 should be a known issue"); - Assert.AreEqual("Known open bug", knownIssue.Title); + Assert.True(knownIssue != null, "Work item 301 should be a known issue"); + Assert.Equal("Known open bug", knownIssue.Title); } /// /// Verify that release baseline selection skips all pre-release versions. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_ReleaseVersion_SkipsAllPreReleases() { // Arrange @@ -232,16 +231,16 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_ReleaseVersi var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - Should skip pre-releases and use v1.0.0 as baseline - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit1", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit1", buildInfo.BaselineVersionTag.CommitHash); } /// /// Verify that annotated tags (with peeledObjectId) resolve to the correct commit. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_AnnotatedTags_ResolvesToPeeledCommit() { // Arrange - Annotated tags have objectId pointing to the tag object, peeledObjectId to the commit @@ -263,18 +262,18 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_AnnotatedTag var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - Tags should resolve via peeledObjectId, not the tag object SHA - Assert.IsNotNull(buildInfo); - Assert.AreEqual("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit3", buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit1", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("commit3", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit1", buildInfo.BaselineVersionTag.CommitHash); } /// /// Verify that mixed annotated and lightweight tags both resolve correctly. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_MixedTagTypes_ResolvesCorrectly() { // Arrange - Mix of annotated (with peeledObjectId) and lightweight (without) tags @@ -295,25 +294,25 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_MixedTagType var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - Both tag types should resolve to correct commits - Assert.IsNotNull(buildInfo); - Assert.AreEqual("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit2", buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit1", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("commit2", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit1", buildInfo.BaselineVersionTag.CommitHash); } /// /// Verify that AzureDevOpsRepoConnector implements IRepoConnector. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ImplementsInterface_ReturnsTrue() { // Arrange var connector = new AzureDevOpsRepoConnector(); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } // ───────────────────────────────────────────────────────────────────────── @@ -323,7 +322,7 @@ public void AzureDevOpsRepoConnector_ImplementsInterface_ReturnsTrue() /// /// Verify that visibility:internal in a buildmark block excludes the item from the report. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_VisibilityInternal_ExcludesItem() { // Arrange - work item with visibility:internal in description @@ -349,15 +348,15 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_VisibilityIn var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - the internal item should not appear in bugs or changes - Assert.IsNotNull(buildInfo); - Assert.DoesNotContain(b => b.Id == "200", buildInfo.Bugs, "Internal item should be excluded from bugs"); - Assert.DoesNotContain(c => c.Id == "200", buildInfo.Changes, "Internal item should be excluded from changes"); + Assert.NotNull(buildInfo); + Assert.DoesNotContain(buildInfo.Bugs, b => b.Id == "200"); + Assert.DoesNotContain(buildInfo.Changes, c => c.Id == "200"); } /// /// Verify that visibility:public in a buildmark block includes the item in the report. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_VisibilityPublic_IncludesItem() { // Arrange - work item with visibility:public in description @@ -383,17 +382,15 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_VisibilityPu var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - the public item should appear in changes - Assert.IsNotNull(buildInfo); - Assert.Contains( - c => c.Id == "200", - buildInfo.Changes, - "Public item should be included in changes"); + Assert.NotNull(buildInfo); + Assert.Contains(buildInfo.Changes, + c => c.Id == "200"); } /// /// Verify that type:bug override classifies the item as a bug. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug() { // Arrange - User Story with type:bug override in description @@ -419,17 +416,15 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeBugOverr var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - User Story should be classified as bug due to override - Assert.IsNotNull(buildInfo); - Assert.Contains( - b => b.Id == "200", - buildInfo.Bugs, - "Item with type:bug override should appear in bugs"); + Assert.NotNull(buildInfo); + Assert.Contains(buildInfo.Bugs, + b => b.Id == "200"); } /// /// Verify that type:feature override classifies the item as a feature. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature() { // Arrange - Bug with type:feature override in description @@ -455,76 +450,11 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_TypeFeatureO var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - Bug should be classified as feature due to override - Assert.IsNotNull(buildInfo); - Assert.Contains( - c => c.Id == "200", - buildInfo.Changes, - "Item with type:feature override should appear in changes"); - Assert.DoesNotContain( - b => b.Id == "200", - buildInfo.Bugs, - "Item with type:feature override should NOT appear in bugs"); - } - - // ───────────────────────────────────────────────────────────────────────── - // BuildMark-AzureDevOps-CustomFields - // ───────────────────────────────────────────────────────────────────────── - - /// - /// Verify that Custom.Visibility field returns mapped controls. - /// - [TestMethod] - public void WorkItemMapper_ExtractItemControls_CustomVisibilityField_ReturnsMappedControls() - { - // Arrange - work item with Custom.Visibility field - var workItem = CreateWorkItem(200, "Test item", "User Story", "Active", - customVisibility: "internal"); - - // Act - var controls = WorkItemMapper.ExtractItemControls(workItem); - - // Assert - Assert.IsNotNull(controls); - Assert.AreEqual("internal", controls.Visibility); - } - - /// - /// Verify that Custom.AffectedVersions field returns mapped version set. - /// - [TestMethod] - public void WorkItemMapper_ExtractItemControls_CustomAffectedVersionsField_ReturnsMappedVersionSet() - { - // Arrange - work item with Custom.AffectedVersions field - var workItem = CreateWorkItem(200, "Test item", "Bug", "Active", - customAffectedVersions: "(,1.0.1]"); - - // Act - var controls = WorkItemMapper.ExtractItemControls(workItem); - - // Assert - Assert.IsNotNull(controls); - Assert.IsNotNull(controls.AffectedVersions); - Assert.IsNotEmpty(controls.AffectedVersions.Intervals); - } - - /// - /// Verify that custom fields take precedence over buildmark blocks. - /// - [TestMethod] - public void WorkItemMapper_ExtractItemControls_CustomFieldsTakePrecedenceOverBuildmarkBlock() - { - // Arrange - work item with BOTH a buildmark block saying "public" AND a custom field saying "internal" - var description = "Description\n```buildmark\nvisibility: public\n```"; - var workItem = CreateWorkItem(200, "Test item", "Bug", "Active", - description: description, - customVisibility: "internal"); - - // Act - var controls = WorkItemMapper.ExtractItemControls(workItem); - - // Assert - custom field "internal" should take precedence over buildmark block "public" - Assert.IsNotNull(controls); - Assert.AreEqual("internal", controls.Visibility); + Assert.NotNull(buildInfo); + Assert.Contains(buildInfo.Changes, + c => c.Id == "200"); + Assert.DoesNotContain(buildInfo.Bugs, + b => b.Id == "200"); } // ───────────────────────────────────────────────────────────────────────── @@ -534,7 +464,7 @@ public void WorkItemMapper_ExtractItemControls_CustomFieldsTakePrecedenceOverBui /// /// Verify that Configure with rules causes HasRules to return true. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_Configure_WithRules_HasRulesReturnsTrue() { // Arrange - configure a connector with rules and verify via GetBuildInformationAsync @@ -555,13 +485,13 @@ public async Task AzureDevOpsRepoConnector_Configure_WithRules_HasRulesReturnsTr var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - RoutedSections is only populated when HasRules is true - Assert.IsNotNull(buildInfo.RoutedSections, "RoutedSections should be populated when rules are configured (HasRules == true)"); + Assert.True(buildInfo.RoutedSections != null, "RoutedSections should be populated when rules are configured (HasRules == true)"); } /// /// Verify that configured rules populate routed sections. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections() { // Arrange @@ -597,339 +527,14 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_WithConfigur var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.RoutedSections); - Assert.IsNotEmpty(buildInfo.RoutedSections, "Should have routed sections"); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.RoutedSections); + Assert.NotEmpty(buildInfo.RoutedSections); // Verify bug was routed to bugs section var bugsSection = buildInfo.RoutedSections.FirstOrDefault(s => s.SectionId == "bugs"); - Assert.AreEqual("bugs", bugsSection.SectionId, "Should contain a bugs routed section"); - Assert.IsNotEmpty(bugsSection.Items, "Bugs section should contain the routed bug"); - } - - // ───────────────────────────────────────────────────────────────────────── - // BuildMark-AzureDevOps-RestClient - // ───────────────────────────────────────────────────────────────────────── - - /// - /// Verify that GetRepositoryAsync returns a repository record from a valid response. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_GetRepositoryAsync_ValidResponse_ReturnsRepository() - { - // Arrange - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddRepositoryResponse("repo-123", "MyRepo", "https://dev.azure.com/org/project/_git/MyRepo"); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var repo = await client.GetRepositoryAsync("MyRepo"); - - // Assert - Assert.IsNotNull(repo); - Assert.AreEqual("repo-123", repo.Id); - Assert.AreEqual("MyRepo", repo.Name); - Assert.AreEqual("https://dev.azure.com/org/project/_git/MyRepo", repo.RemoteUrl); - } - - /// - /// Verify that GetCommitsAsync returns commits from a valid response. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits() - { - // Arrange - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddCommitsResponse( - new MockAdoCommit("abc123", "First commit"), - new MockAdoCommit("def456", "Second commit")); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var commits = await client.GetCommitsAsync("repo-id"); - - // Assert - Assert.IsNotNull(commits); - Assert.HasCount(2, commits); - Assert.AreEqual("abc123", commits[0].CommitId); - Assert.AreEqual("First commit", commits[0].Comment); - Assert.AreEqual("def456", commits[1].CommitId); - } - - /// - /// Verify that GetPullRequestsAsync returns pull requests from a valid response. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests() - { - // Arrange - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddPullRequestsResponse( - new MockAdoPullRequest(101, "Feature PR", "completed", "merge-commit-1"), - new MockAdoPullRequest(102, "Bug PR", "active")); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var prs = await client.GetPullRequestsAsync("repo-id"); - - // Assert - Assert.IsNotNull(prs); - Assert.HasCount(2, prs); - Assert.AreEqual(101, prs[0].PullRequestId); - Assert.AreEqual("Feature PR", prs[0].Title); - Assert.AreEqual("completed", prs[0].Status); - Assert.AreEqual("merge-commit-1", prs[0].MergeCommitId); - } - - /// - /// Verify that GetPullRequestWorkItemsAsync returns work item references from a valid response. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_ValidResponse_ReturnsWorkItemRefs() - { - // Arrange - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddPullRequestWorkItemsResponse("repo-id", 101, 200, 201); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var workItemRefs = await client.GetPullRequestWorkItemsAsync("repo-id", 101); - - // Assert - Assert.IsNotNull(workItemRefs); - Assert.HasCount(2, workItemRefs); - Assert.AreEqual(200, workItemRefs[0].Id); - Assert.AreEqual(201, workItemRefs[1].Id); - } - - /// - /// Verify that GetWorkItemsAsync returns work item details from a valid response. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems() - { - // Arrange - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddWorkItemsResponse( - new MockAdoWorkItem(200, "Bug work item", "Bug", "Active"), - new MockAdoWorkItem(201, "Feature work item", "User Story", "Resolved")); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var workItems = await client.GetWorkItemsAsync([200, 201]); - - // Assert - Assert.IsNotNull(workItems); - Assert.HasCount(2, workItems); - Assert.AreEqual(200, workItems[0].Id); - Assert.AreEqual(201, workItems[1].Id); - } - - /// - /// Verify that QueryWorkItemsAsync returns work item ids for a valid WIQL query. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_QueryWorkItemsAsync_ValidWiql_ReturnsWorkItemIds() - { - // Arrange - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddWiqlResponse(300, 301, 302); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var query = await client.QueryWorkItemsAsync("SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Bug'"); - - // Assert - Assert.IsNotNull(query); - Assert.HasCount(3, query.WorkItems); - Assert.AreEqual(300, query.WorkItems[0].Id); - Assert.AreEqual(301, query.WorkItems[1].Id); - Assert.AreEqual(302, query.WorkItems[2].Id); - } - - /// - /// Verify that GetPullRequestWorkItemsAsync deserializes string-valued ids - /// as returned by the Azure DevOps PR work items endpoint. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_StringValuedIds_DeserializesCorrectly() - { - // Arrange - raw JSON matching the real Azure DevOps response format where id is a string - const string json = """{"count":2,"value":[{"id":"1234","url":"https://dev.azure.com/org/project/_apis/wit/workItems/1234"},{"id":"5678","url":"https://dev.azure.com/org/project/_apis/wit/workItems/5678"}]}"""; - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddResponse("pullrequests/101/workitems", json); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var workItemRefs = await client.GetPullRequestWorkItemsAsync("repo-id", 101); - - // Assert - Assert.IsNotNull(workItemRefs); - Assert.HasCount(2, workItemRefs); - Assert.AreEqual(1234, workItemRefs[0].Id); - Assert.AreEqual(5678, workItemRefs[1].Id); - } - - /// - /// Verify that QueryWorkItemsAsync deserializes string-valued ids - /// when the WIQL endpoint returns them as strings. - /// - [TestMethod] - public async Task AzureDevOpsRestClient_QueryWorkItemsAsync_StringValuedIds_DeserializesCorrectly() - { - // Arrange - raw JSON with string-valued ids - const string json = """{"workItems":[{"id":"300","url":"https://dev.azure.com/org/project/_apis/wit/workItems/300"},{"id":"301","url":"https://dev.azure.com/org/project/_apis/wit/workItems/301"}]}"""; - using var mockHandler = new MockAzureDevOpsHttpMessageHandler() - .AddResponse("wit/wiql", json); - using var mockHttpClient = new HttpClient(mockHandler); - using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); - - // Act - var query = await client.QueryWorkItemsAsync("SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Bug'"); - - // Assert - Assert.IsNotNull(query); - Assert.HasCount(2, query.WorkItems); - Assert.AreEqual(300, query.WorkItems[0].Id); - Assert.AreEqual(301, query.WorkItems[1].Id); - } - - // ───────────────────────────────────────────────────────────────────────── - // BuildMark-AzureDevOps-WorkItemMapper - // ───────────────────────────────────────────────────────────────────────── - - /// - /// Verify that Bug work item type maps to a bug ItemInfo. - /// - [TestMethod] - public void WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem() - { - // Arrange - var workItem = CreateWorkItem(100, "A bug", "Bug", "Active"); - - // Act - var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/100", 1); - - // Assert - Assert.IsNotNull(itemInfo); - Assert.AreEqual("100", itemInfo.Id); - Assert.AreEqual("A bug", itemInfo.Title); - Assert.AreEqual("bug", itemInfo.Type); - } - - /// - /// Verify that User Story work item type maps to a feature ItemInfo. - /// - [TestMethod] - public void WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureItem() - { - // Arrange - var workItem = CreateWorkItem(101, "A user story", "User Story", "Active"); - - // Act - var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/101", 2); - - // Assert - Assert.IsNotNull(itemInfo); - Assert.AreEqual("101", itemInfo.Id); - Assert.AreEqual("A user story", itemInfo.Title); - Assert.AreEqual("feature", itemInfo.Type); - } - - /// - /// Verify that Epic work item type maps to a feature ItemInfo. - /// - [TestMethod] - public void WorkItemMapper_MapWorkItemToItemInfo_EpicType_ReturnsFeatureItem() - { - // Arrange - var workItem = CreateWorkItem(103, "An epic", "Epic", "Active"); - - // Act - var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/103", 4); - - // Assert - Assert.IsNotNull(itemInfo); - Assert.AreEqual("103", itemInfo.Id); - Assert.AreEqual("An epic", itemInfo.Title); - Assert.AreEqual("feature", itemInfo.Type); - } - - /// - /// Verify that Task work item type maps to an ItemInfo with the raw type name. - /// - [TestMethod] - public void WorkItemMapper_MapWorkItemToItemInfo_TaskType_ReturnsTaskItem() - { - // Arrange - var workItem = CreateWorkItem(102, "A task", "Task", "Active"); - - // Act - var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/102", 3); - - // Assert - Assert.IsNotNull(itemInfo); - Assert.AreEqual("102", itemInfo.Id); - Assert.AreEqual("A task", itemInfo.Title); - Assert.AreEqual("Task", itemInfo.Type); - } - - /// - /// Verify that IsWorkItemResolved returns true for a resolved work item. - /// - [TestMethod] - public void WorkItemMapper_IsWorkItemResolved_ResolvedState_ReturnsTrue() - { - // Arrange - test all resolved states - var resolvedItem = CreateWorkItem(100, "Resolved item", "Bug", "Resolved"); - var closedItem = CreateWorkItem(101, "Closed item", "Bug", "Closed"); - var doneItem = CreateWorkItem(102, "Done item", "Bug", "Done"); - - // Act & Assert - Assert.IsTrue(WorkItemMapper.IsWorkItemResolved(resolvedItem), "Resolved state should be resolved"); - Assert.IsTrue(WorkItemMapper.IsWorkItemResolved(closedItem), "Closed state should be resolved"); - Assert.IsTrue(WorkItemMapper.IsWorkItemResolved(doneItem), "Done state should be resolved"); - } - - /// - /// Verify that IsWorkItemResolved returns false for an active work item. - /// - [TestMethod] - public void WorkItemMapper_IsWorkItemResolved_ActiveState_ReturnsFalse() - { - // Arrange - var activeItem = CreateWorkItem(100, "Active item", "Bug", "Active"); - var newItem = CreateWorkItem(101, "New item", "Bug", "New"); - - // Act & Assert - Assert.IsFalse(WorkItemMapper.IsWorkItemResolved(activeItem), "Active state should not be resolved"); - Assert.IsFalse(WorkItemMapper.IsWorkItemResolved(newItem), "New state should not be resolved"); - } - - /// - /// Verify that GetWorkItemTypeForRuleMatching returns the raw work item type name. - /// - [TestMethod] - public void WorkItemMapper_GetWorkItemTypeForRuleMatching_ReturnsWorkItemTypeName() - { - // Arrange - var bugItem = CreateWorkItem(100, "Bug item", "Bug", "Active"); - var storyItem = CreateWorkItem(101, "Story item", "User Story", "Active"); - - // Act - var bugType = WorkItemMapper.GetWorkItemTypeForRuleMatching(bugItem); - var storyType = WorkItemMapper.GetWorkItemTypeForRuleMatching(storyItem); - - // Assert - Assert.AreEqual("Bug", bugType); - Assert.AreEqual("User Story", storyType); + Assert.True(bugsSection.SectionId == "bugs", "Should contain a bugs routed section"); + Assert.NotEmpty(bugsSection.Items); } // ───────────────────────────────────────────────────────────────────────── @@ -939,7 +544,7 @@ public void WorkItemMapper_GetWorkItemTypeForRuleMatching_ReturnsWorkItemTypeNam /// /// Verify that a dev.azure.com HTTPS URL is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_DevAzureComHttps_ReturnsCorrectComponents() { // Act @@ -947,15 +552,15 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_DevAzureComHttps_Return "https://dev.azure.com/myorg/myproject/_git/myrepo"); // Assert - Assert.AreEqual("https://dev.azure.com/myorg", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://dev.azure.com/myorg", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that a dev.azure.com HTTPS URL with .git suffix is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_DevAzureComWithGitSuffix_StripsGitSuffix() { // Act @@ -963,15 +568,15 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_DevAzureComWithGitSuffi "https://dev.azure.com/myorg/myproject/_git/myrepo.git"); // Assert - Assert.AreEqual("https://dev.azure.com/myorg", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://dev.azure.com/myorg", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that a visualstudio.com HTTPS URL is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_VisualStudioComHttps_ReturnsCorrectComponents() { // Act @@ -979,15 +584,15 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_VisualStudioComHttps_Re "https://myorg.visualstudio.com/myproject/_git/myrepo"); // Assert - Assert.AreEqual("https://myorg.visualstudio.com", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://myorg.visualstudio.com", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that an SSH URL is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_SshUrl_ReturnsCorrectComponents() { // Act @@ -995,15 +600,15 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_SshUrl_ReturnsCorrectCo "git@ssh.dev.azure.com:v3/myorg/myproject/myrepo"); // Assert - Assert.AreEqual("https://dev.azure.com/myorg", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://dev.azure.com/myorg", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that an on-premises Azure DevOps Server URL is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_OnPremServer_ReturnsCorrectComponents() { // Act @@ -1011,15 +616,15 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_OnPremServer_ReturnsCor "https://devops.mycompany.com/DefaultCollection/myproject/_git/myrepo"); // Assert - Assert.AreEqual("https://devops.mycompany.com/DefaultCollection", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://devops.mycompany.com/DefaultCollection", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that an on-premises Azure DevOps Server URL with a custom port is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_OnPremWithPort_ReturnsCorrectComponents() { // Act @@ -1027,15 +632,15 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_OnPremWithPort_ReturnsC "https://devops.internal.net:8080/tfs/DefaultCollection/myproject/_git/myrepo"); // Assert - Assert.AreEqual("https://devops.internal.net:8080/tfs/DefaultCollection", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://devops.internal.net:8080/tfs/DefaultCollection", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that an on-premises Azure DevOps Server URL with .git suffix is parsed correctly. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_OnPremWithGitSuffix_StripsGitSuffix() { // Act @@ -1043,19 +648,19 @@ public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_OnPremWithGitSuffix_Str "https://devops.mycompany.com/DefaultCollection/myproject/_git/myrepo.git"); // Assert - Assert.AreEqual("https://devops.mycompany.com/DefaultCollection", orgUrl); - Assert.AreEqual("myproject", project); - Assert.AreEqual("myrepo", repo); + Assert.Equal("https://devops.mycompany.com/DefaultCollection", orgUrl); + Assert.Equal("myproject", project); + Assert.Equal("myrepo", repo); } /// /// Verify that an unsupported URL format throws ArgumentException. /// - [TestMethod] + [Fact] public void AzureDevOpsRepoConnector_ParseAzureDevOpsUrl_UnsupportedFormat_ThrowsArgumentException() { // Act / Assert - Assert.ThrowsExactly(() => + Assert.Throws(() => AzureDevOpsRepoConnector.ParseAzureDevOpsUrl("https://example.com/not-a-valid-url")); } @@ -1080,53 +685,12 @@ private static MockableAzureDevOpsRepoConnector CreateMockConnector( return connector; } - /// - /// Creates an AzureDevOpsWorkItem record for testing. - /// - /// Work item ID. - /// Work item title. - /// Work item type. - /// Work item state. - /// Optional description body. - /// Optional Custom.Visibility field. - /// Optional Custom.AffectedVersions field. - /// AzureDevOpsWorkItem record. - private static AzureDevOpsWorkItem CreateWorkItem( - int id, - string title, - string workItemType, - string state, - string? description = null, - string? customVisibility = null, - string? customAffectedVersions = null) - { - var fields = new Dictionary - { - ["System.Title"] = title, - ["System.WorkItemType"] = workItemType, - ["System.State"] = state, - ["System.Description"] = description - }; - - if (customVisibility != null) - { - fields["Custom.Visibility"] = customVisibility; - } - - if (customAffectedVersions != null) - { - fields["Custom.AffectedVersions"] = customAffectedVersions; - } - - return new AzureDevOpsWorkItem(id, fields); - } - /// /// Verify that known issues are filtered by affected-versions (via Custom.AffectedVersions) /// when the field is present on a work item. Bugs whose affected-versions do not contain /// the build version are excluded; bugs with matching ranges or no field are included. /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_KnownIssues_FilteredByAffectedVersions() { // Arrange - three open bugs via WIQL: @@ -1152,21 +716,21 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_KnownIssues_ var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.5.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.KnownIssues); // Bug 401 should be included (v1.5.0 is in [1.0.0,2.0.0)) - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "401"), "Bug 401 with Custom.AffectedVersions [1.0.0,2.0.0) should be a known issue for v1.5.0"); // Bug 402 should be excluded (v1.5.0 is NOT in [3.0.0,)) - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "402"), "Bug 402 with Custom.AffectedVersions [3.0.0,) should NOT be a known issue for v1.5.0"); // Bug 403 should be included (no affected-versions, fallback to open status) - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "403"), "Bug 403 with no Custom.AffectedVersions should be a known issue (open status fallback)"); } @@ -1175,7 +739,7 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_KnownIssues_ /// Verify that a RESOLVED/CLOSED bug with a Custom.AffectedVersions range that contains /// the build version is reported as a known issue (LTS back-port gap scenario). /// - [TestMethod] + [Fact] public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchingAffectedVersions_IsKnownIssue() { // Arrange - three resolved bugs: @@ -1201,21 +765,21 @@ public async Task AzureDevOpsRepoConnector_GetBuildInformationAsync_ClosedBugWit var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.5.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.KnownIssues); // Bug 404 is Resolved but AV [1.0.0,2.0.0) contains v1.5.0 → IS a known issue - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "404"), "Resolved bug 404 with AV [1.0.0,2.0.0) should be a known issue for v1.5.0 (LTS back-port gap)"); // Bug 405 is Resolved and AV [3.0.0,) does NOT contain v1.5.0 → NOT a known issue - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "405"), "Resolved bug 405 with AV [3.0.0,) should NOT be a known issue for v1.5.0"); // Bug 406 is Resolved with no AV → NOT a known issue (resolved/unresolved fallback) - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "406"), "Resolved bug 406 with no AV should NOT be a known issue (resolved, no AV)"); } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs new file mode 100644 index 00000000..1d57b5ff --- /dev/null +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsRestClientTests.cs @@ -0,0 +1,253 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.BuildMark.RepoConnectors.AzureDevOps; + +namespace DemaConsulting.BuildMark.Tests.RepoConnectors.AzureDevOps; + +/// +/// Unit tests for the AzureDevOpsRestClient class. +/// +public class AzureDevOpsRestClientTests +{ + // ───────────────────────────────────────────────────────────────────────── + // BuildMark-AzureDevOps-RestClient + // ───────────────────────────────────────────────────────────────────────── + + /// + /// Verify that GetRepositoryAsync returns a repository record from a valid response. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetRepositoryAsync_ValidResponse_ReturnsRepository() + { + // Arrange + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddRepositoryResponse("repo-123", "MyRepo", "https://dev.azure.com/org/project/_git/MyRepo"); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var repo = await client.GetRepositoryAsync("MyRepo"); + + // Assert + Assert.NotNull(repo); + Assert.Equal("repo-123", repo.Id); + Assert.Equal("MyRepo", repo.Name); + Assert.Equal("https://dev.azure.com/org/project/_git/MyRepo", repo.RemoteUrl); + } + + /// + /// Verify that GetCommitsAsync returns commits from a valid response. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetCommitsAsync_ValidResponse_ReturnsCommits() + { + // Arrange + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddCommitsResponse( + new MockAdoCommit("abc123", "First commit"), + new MockAdoCommit("def456", "Second commit")); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var commits = await client.GetCommitsAsync("repo-id"); + + // Assert + Assert.NotNull(commits); + Assert.Equal(2, commits.Count); + Assert.Equal("abc123", commits[0].CommitId); + Assert.Equal("First commit", commits[0].Comment); + Assert.Equal("def456", commits[1].CommitId); + } + + /// + /// Verify that GetTagsAsync returns tag references from a valid response. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetTagsAsync_ValidResponse_ReturnsTags() + { + // Arrange: two tags — one lightweight (no peeled object) and one annotated (with peeled object) + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v1.0.0", "abc123"), + new MockAdoTag("v2.0.0", "tagobj456", "commit789")); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var tags = await client.GetTagsAsync("repo-id"); + + // Assert + Assert.NotNull(tags); + Assert.Equal(2, tags.Count); + + // Lightweight tag: CommitId resolves to ObjectId since PeeledObjectId is null + Assert.Equal("refs/tags/v1.0.0", tags[0].Name); + Assert.Equal("abc123", tags[0].CommitId); + + // Annotated tag: CommitId resolves to PeeledObjectId (the underlying commit) + Assert.Equal("refs/tags/v2.0.0", tags[1].Name); + Assert.Equal("commit789", tags[1].CommitId); + } + + /// + /// Verify that GetPullRequestsAsync returns pull requests from a valid response. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests() + { + // Arrange + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddPullRequestsResponse( + new MockAdoPullRequest(101, "Feature PR", "completed", "merge-commit-1"), + new MockAdoPullRequest(102, "Bug PR", "active")); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var prs = await client.GetPullRequestsAsync("repo-id"); + + // Assert + Assert.NotNull(prs); + Assert.Equal(2, prs.Count); + Assert.Equal(101, prs[0].PullRequestId); + Assert.Equal("Feature PR", prs[0].Title); + Assert.Equal("completed", prs[0].Status); + Assert.Equal("merge-commit-1", prs[0].MergeCommitId); + } + + /// + /// Verify that GetPullRequestWorkItemsAsync returns work item references from a valid response. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_ValidResponse_ReturnsWorkItemRefs() + { + // Arrange + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddPullRequestWorkItemsResponse("repo-id", 101, 200, 201); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var workItemRefs = await client.GetPullRequestWorkItemsAsync("repo-id", 101); + + // Assert + Assert.NotNull(workItemRefs); + Assert.Equal(2, workItemRefs.Count); + Assert.Equal(200, workItemRefs[0].Id); + Assert.Equal(201, workItemRefs[1].Id); + } + + /// + /// Verify that GetWorkItemsAsync returns work item details from a valid response. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetWorkItemsAsync_ValidResponse_ReturnsWorkItems() + { + // Arrange + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddWorkItemsResponse( + new MockAdoWorkItem(200, "Bug work item", "Bug", "Active"), + new MockAdoWorkItem(201, "Feature work item", "User Story", "Resolved")); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var workItems = await client.GetWorkItemsAsync([200, 201]); + + // Assert + Assert.NotNull(workItems); + Assert.Equal(2, workItems.Count); + Assert.Equal(200, workItems[0].Id); + Assert.Equal(201, workItems[1].Id); + } + + /// + /// Verify that QueryWorkItemsAsync returns work item ids for a valid WIQL query. + /// + [Fact] + public async Task AzureDevOpsRestClient_QueryWorkItemsAsync_ValidWiql_ReturnsWorkItemIds() + { + // Arrange + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddWiqlResponse(300, 301, 302); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var query = await client.QueryWorkItemsAsync("SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Bug'"); + + // Assert + Assert.NotNull(query); + Assert.Equal(3, query.WorkItems.Count); + Assert.Equal(300, query.WorkItems[0].Id); + Assert.Equal(301, query.WorkItems[1].Id); + Assert.Equal(302, query.WorkItems[2].Id); + } + + /// + /// Verify that GetPullRequestWorkItemsAsync deserializes string-valued ids + /// as returned by the Azure DevOps PR work items endpoint. + /// + [Fact] + public async Task AzureDevOpsRestClient_GetPullRequestWorkItemsAsync_StringValuedIds_DeserializesCorrectly() + { + // Arrange - raw JSON matching the real Azure DevOps response format where id is a string + const string json = """{"count":2,"value":[{"id":"1234","url":"https://dev.azure.com/org/project/_apis/wit/workItems/1234"},{"id":"5678","url":"https://dev.azure.com/org/project/_apis/wit/workItems/5678"}]}"""; + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddResponse("pullrequests/101/workitems", json); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var workItemRefs = await client.GetPullRequestWorkItemsAsync("repo-id", 101); + + // Assert + Assert.NotNull(workItemRefs); + Assert.Equal(2, workItemRefs.Count); + Assert.Equal(1234, workItemRefs[0].Id); + Assert.Equal(5678, workItemRefs[1].Id); + } + + /// + /// Verify that QueryWorkItemsAsync deserializes string-valued ids + /// when the WIQL endpoint returns them as strings. + /// + [Fact] + public async Task AzureDevOpsRestClient_QueryWorkItemsAsync_StringValuedIds_DeserializesCorrectly() + { + // Arrange - raw JSON with string-valued ids + const string json = """{"workItems":[{"id":"300","url":"https://dev.azure.com/org/project/_apis/wit/workItems/300"},{"id":"301","url":"https://dev.azure.com/org/project/_apis/wit/workItems/301"}]}"""; + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddResponse("wit/wiql", json); + using var mockHttpClient = new HttpClient(mockHandler); + using var client = new AzureDevOpsRestClient(mockHttpClient, "https://dev.azure.com/org", "project"); + + // Act + var query = await client.QueryWorkItemsAsync("SELECT [System.Id] FROM workitems WHERE [System.WorkItemType] = 'Bug'"); + + // Assert + Assert.NotNull(query); + Assert.Equal(2, query.WorkItems.Count); + Assert.Equal(300, query.WorkItems[0].Id); + Assert.Equal(301, query.WorkItems[1].Id); + } +} diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs new file mode 100644 index 00000000..bd520dc7 --- /dev/null +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/AzureDevOpsTests.cs @@ -0,0 +1,203 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.BuildMark.RepoConnectors; +using DemaConsulting.BuildMark.RepoConnectors.AzureDevOps; +using DemaConsulting.BuildMark.Version; + +namespace DemaConsulting.BuildMark.Tests.RepoConnectors.AzureDevOps; + +/// +/// Sub-subsystem tests for the AzureDevOps sub-subsystem. +/// These tests verify the contract exposed by the AzureDevOps sub-subsystem as a whole, +/// exercising the connector through its public IRepoConnector interface. +/// +public class AzureDevOpsTests +{ + // ───────────────────────────────────────────────────────────────────────── + // BuildMark-AzureDevOps-SubSystem + // ───────────────────────────────────────────────────────────────────────── + + /// + /// Test that the AzureDevOps sub-subsystem provides a connector that implements IRepoConnector. + /// + [Fact] + public void AzureDevOps_ImplementsInterface_ReturnsTrue() + { + // Arrange: create an AzureDevOpsRepoConnector instance from the AzureDevOps sub-subsystem + var connector = new AzureDevOpsRepoConnector(); + + // Assert: the sub-subsystem connector satisfies the shared IRepoConnector interface + Assert.IsAssignableFrom(connector); + } + + /// + /// Test that the AzureDevOps sub-subsystem returns valid build information from mocked API data. + /// + /// + /// What is being tested: AzureDevOps sub-subsystem end-to-end build information retrieval + /// What the assertions prove: Build information is complete and accurate for a single tag + /// + [Fact] + public async Task AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation() + { + // Arrange: set up a mocked REST handler with a single tag and commit + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse(new MockAdoTag("v1.0.0", "abc123")) + .AddCommitsResponse(new MockAdoCommit("abc123")) + .AddPullRequestsResponse() + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = CreateMockAdoConnector(mockHttpClient, "abc123"); + + // Act: retrieve build information for v1.0.0 + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); + + // Assert: build information is complete and accurate + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("abc123", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); + } + + /// + /// Test that the AzureDevOps sub-subsystem gathers changes from pull requests. + /// + /// + /// What is being tested: AzureDevOps sub-subsystem PR-based change gathering + /// What the assertions prove: Merged PR work items appear in the Changes collection + /// + [Fact] + public async Task AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges() + { + // Arrange: set up two versions with a PR merged between them + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v1.1.0", "commit2"), + new MockAdoTag("v1.0.0", "commit1")) + .AddCommitsResponse( + new MockAdoCommit("commit2"), + new MockAdoCommit("commit1")) + .AddPullRequestsResponse( + new MockAdoPullRequest(100, "Add feature", "completed", "commit2")) + .AddPullRequestWorkItemsResponse("repo", 100, 200) + .AddWorkItemsResponse( + new MockAdoWorkItem(200, "Feature work item", "User Story")) + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = CreateMockAdoConnector(mockHttpClient, "commit2"); + + // Act: retrieve build information for v1.1.0 + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); + + // Assert: changes include the work item from the PR + Assert.NotNull(buildInfo); + Assert.NotEmpty(buildInfo.Changes); + } + + /// + /// Test that the AzureDevOps sub-subsystem identifies open work items as known issues. + /// + /// + /// What is being tested: AzureDevOps sub-subsystem known-issues identification + /// What the assertions prove: Open bug work items from WIQL queries appear in KnownIssues + /// + [Fact] + public async Task AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues() + { + // Arrange: set up a version with an open bug from WIQL query + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse(new MockAdoTag("v1.0.0", "abc123")) + .AddCommitsResponse(new MockAdoCommit("abc123")) + .AddPullRequestsResponse() + .AddWiqlResponse(500) + .AddWorkItemsResponse( + new MockAdoWorkItem(500, "Open bug", "Bug", "Active")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = CreateMockAdoConnector(mockHttpClient, "abc123"); + + // Act: retrieve build information for v1.0.0 + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); + + // Assert: known issues include the open bug + Assert.NotNull(buildInfo); + Assert.NotEmpty(buildInfo.KnownIssues); + } + + /// + /// Test that the AzureDevOps sub-subsystem skips pre-release tags for release versions. + /// + /// + /// What is being tested: AzureDevOps sub-subsystem pre-release tag handling + /// What the assertions prove: A release version uses only prior release tags as its baseline + /// + [Fact] + public async Task AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases() + { + // Arrange: a release version with a pre-release between it and the previous release + using var mockHandler = new MockAzureDevOpsHttpMessageHandler() + .AddTagsResponse( + new MockAdoTag("v2.0.0", "commit3"), + new MockAdoTag("v2.0.0-rc.1", "commit2"), + new MockAdoTag("v1.0.0", "commit1")) + .AddCommitsResponse( + new MockAdoCommit("commit3"), + new MockAdoCommit("commit2"), + new MockAdoCommit("commit1")) + .AddPullRequestsResponse() + .AddWiqlResponse(); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = CreateMockAdoConnector(mockHttpClient, "commit3"); + + // Act: retrieve build information for v2.0.0 (a release version) + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); + + // Assert: baseline should be v1.0.0, skipping the pre-release v2.0.0-rc.1 + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + } + + /// + /// Creates a mock Azure DevOps connector with pre-configured git command responses. + /// + /// Mock HTTP client for REST API. + /// Current commit hash to return from git rev-parse HEAD. + /// Configured MockableAzureDevOpsRepoConnector. + private static MockableAzureDevOpsRepoConnector CreateMockAdoConnector( + HttpClient mockHttpClient, + string currentCommitHash) + { + var connector = new MockableAzureDevOpsRepoConnector(mockHttpClient); + connector.SetCommandResponse("git remote get-url origin", + "https://dev.azure.com/org/project/_git/repo"); + connector.SetCommandResponse("git rev-parse HEAD", currentCommitHash); + connector.SetCommandResponse( + "az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv", + "mock-token"); + return connector; + } +} diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs new file mode 100644 index 00000000..4159cb05 --- /dev/null +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/AzureDevOps/WorkItemMapperTests.cs @@ -0,0 +1,250 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.BuildMark.RepoConnectors.AzureDevOps; + +namespace DemaConsulting.BuildMark.Tests.RepoConnectors.AzureDevOps; + +/// +/// +public class WorkItemMapperTests +{ + // ───────────────────────────────────────────────────────────────────────── + // BuildMark-AzureDevOps-WorkItemMapper + // ───────────────────────────────────────────────────────────────────────── + + /// + /// Verify that Bug work item type maps to a bug ItemInfo. + /// + [Fact] + public void WorkItemMapper_MapWorkItemToItemInfo_BugType_ReturnsBugItem() + { + // Arrange + var workItem = CreateWorkItem(100, "A bug", "Bug", "Active"); + + // Act + var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/100", 1); + + // Assert + Assert.NotNull(itemInfo); + Assert.Equal("100", itemInfo.Id); + Assert.Equal("A bug", itemInfo.Title); + Assert.Equal("bug", itemInfo.Type); + } + + /// + /// Verify that User Story work item type maps to a feature ItemInfo. + /// + [Fact] + public void WorkItemMapper_MapWorkItemToItemInfo_UserStoryType_ReturnsFeatureItem() + { + // Arrange + var workItem = CreateWorkItem(101, "A user story", "User Story", "Active"); + + // Act + var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/101", 2); + + // Assert + Assert.NotNull(itemInfo); + Assert.Equal("101", itemInfo.Id); + Assert.Equal("A user story", itemInfo.Title); + Assert.Equal("feature", itemInfo.Type); + } + + /// + /// Verify that Epic work item type maps to a feature ItemInfo. + /// + [Fact] + public void WorkItemMapper_MapWorkItemToItemInfo_EpicType_ReturnsFeatureItem() + { + // Arrange + var workItem = CreateWorkItem(103, "An epic", "Epic", "Active"); + + // Act + var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/103", 4); + + // Assert + Assert.NotNull(itemInfo); + Assert.Equal("103", itemInfo.Id); + Assert.Equal("An epic", itemInfo.Title); + Assert.Equal("feature", itemInfo.Type); + } + + /// + /// Verify that Task work item type maps to an ItemInfo with the raw type name. + /// + [Fact] + public void WorkItemMapper_MapWorkItemToItemInfo_TaskType_ReturnsTaskItem() + { + // Arrange + var workItem = CreateWorkItem(102, "A task", "Task", "Active"); + + // Act + var itemInfo = WorkItemMapper.MapWorkItemToItemInfo(workItem, "https://example.com/102", 3); + + // Assert + Assert.NotNull(itemInfo); + Assert.Equal("102", itemInfo.Id); + Assert.Equal("A task", itemInfo.Title); + Assert.Equal("Task", itemInfo.Type); + } + + /// + /// Verify that IsWorkItemResolved returns true for a resolved work item. + /// + [Fact] + public void WorkItemMapper_IsWorkItemResolved_ResolvedState_ReturnsTrue() + { + // Arrange - test all resolved states + var resolvedItem = CreateWorkItem(100, "Resolved item", "Bug", "Resolved"); + var closedItem = CreateWorkItem(101, "Closed item", "Bug", "Closed"); + var doneItem = CreateWorkItem(102, "Done item", "Bug", "Done"); + + // Act & Assert + Assert.True(WorkItemMapper.IsWorkItemResolved(resolvedItem), "Resolved state should be resolved"); + Assert.True(WorkItemMapper.IsWorkItemResolved(closedItem), "Closed state should be resolved"); + Assert.True(WorkItemMapper.IsWorkItemResolved(doneItem), "Done state should be resolved"); + } + + /// + /// Verify that IsWorkItemResolved returns false for an active work item. + /// + [Fact] + public void WorkItemMapper_IsWorkItemResolved_ActiveState_ReturnsFalse() + { + // Arrange + var activeItem = CreateWorkItem(100, "Active item", "Bug", "Active"); + var newItem = CreateWorkItem(101, "New item", "Bug", "New"); + + // Act & Assert + Assert.False(WorkItemMapper.IsWorkItemResolved(activeItem), "Active state should not be resolved"); + Assert.False(WorkItemMapper.IsWorkItemResolved(newItem), "New state should not be resolved"); + } + + /// + /// Verify that GetWorkItemTypeForRuleMatching returns the raw work item type name. + /// + [Fact] + public void WorkItemMapper_GetWorkItemTypeForRuleMatching_ReturnsWorkItemTypeName() + { + // Arrange + var bugItem = CreateWorkItem(100, "Bug item", "Bug", "Active"); + var storyItem = CreateWorkItem(101, "Story item", "User Story", "Active"); + + // Act + var bugType = WorkItemMapper.GetWorkItemTypeForRuleMatching(bugItem); + var storyType = WorkItemMapper.GetWorkItemTypeForRuleMatching(storyItem); + + // Assert + Assert.Equal("Bug", bugType); + Assert.Equal("User Story", storyType); + } + + // ───────────────────────────────────────────────────────────────────────── + // BuildMark-AzureDevOps-CustomFields + // ───────────────────────────────────────────────────────────────────────── + + /// + /// Verify that Custom.Visibility field returns mapped controls. + /// + [Fact] + public void WorkItemMapper_ExtractItemControls_CustomVisibilityField_ReturnsMappedControls() + { + // Arrange - work item with Custom.Visibility field + var workItem = CreateWorkItem(200, "Test item", "User Story", "Active", + customVisibility: "internal"); + + // Act + var controls = WorkItemMapper.ExtractItemControls(workItem); + + // Assert + Assert.NotNull(controls); + Assert.Equal("internal", controls.Visibility); + } + + /// + /// Verify that Custom.AffectedVersions field returns mapped version set. + /// + [Fact] + public void WorkItemMapper_ExtractItemControls_CustomAffectedVersionsField_ReturnsMappedVersionSet() + { + // Arrange - work item with Custom.AffectedVersions field + var workItem = CreateWorkItem(200, "Test item", "Bug", "Active", + customAffectedVersions: "(,1.0.1]"); + + // Act + var controls = WorkItemMapper.ExtractItemControls(workItem); + + // Assert + Assert.NotNull(controls); + Assert.NotNull(controls.AffectedVersions); + Assert.NotEmpty(controls.AffectedVersions.Intervals); + } + + /// + /// Verify that custom fields take precedence over buildmark blocks. + /// + [Fact] + public void WorkItemMapper_ExtractItemControls_CustomFieldsTakePrecedenceOverBuildmarkBlock() + { + // Arrange - work item with BOTH a buildmark block saying "public" AND a custom field saying "internal" + var description = "Description\n```buildmark\nvisibility: public\n```"; + var workItem = CreateWorkItem(200, "Test item", "Bug", "Active", + description: description, + customVisibility: "internal"); + + // Act + var controls = WorkItemMapper.ExtractItemControls(workItem); + + // Assert - custom field "internal" should take precedence over buildmark block "public" + Assert.NotNull(controls); + Assert.Equal("internal", controls.Visibility); + } + + private static AzureDevOpsWorkItem CreateWorkItem( + int id, + string title, + string workItemType, + string state, + string? description = null, + string? customVisibility = null, + string? customAffectedVersions = null) + { + var fields = new Dictionary + { + ["System.Title"] = title, + ["System.WorkItemType"] = workItemType, + ["System.State"] = state, + ["System.Description"] = description + }; + + if (customVisibility != null) + { + fields["Custom.Visibility"] = customVisibility; + } + + if (customAffectedVersions != null) + { + fields["Custom.AffectedVersions"] = customAffectedVersions; + } + + return new AzureDevOpsWorkItem(id, fields); + } +} diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs index c3a24b20..6ce4ded6 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientFindIssueIdsTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubGraphQLClient FindIssueIdsLinkedToPullRequestAsync method. /// -[TestClass] public class GitHubGraphQLClientFindIssueIdsTests { /// /// Test that FindIssueIdsLinkedToPullRequestAsync returns expected issue IDs with valid response. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_ValidResponse_ReturnsIssueIds() { // Arrange @@ -64,17 +63,17 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_Valid var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 42); // Assert - Assert.IsNotNull(issueIds); - Assert.HasCount(3, issueIds); - Assert.AreEqual(123, issueIds[0]); - Assert.AreEqual(456, issueIds[1]); - Assert.AreEqual(789, issueIds[2]); + Assert.NotNull(issueIds); + Assert.Equal(3, issueIds.Count); + Assert.Equal(123, issueIds[0]); + Assert.Equal(456, issueIds[1]); + Assert.Equal(789, issueIds[2]); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync returns empty list when no issues are linked. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_NoIssues_ReturnsEmptyList() { // Arrange @@ -101,14 +100,14 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_NoIss var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 42); // Assert - Assert.IsNotNull(issueIds); - Assert.IsEmpty(issueIds); + Assert.NotNull(issueIds); + Assert.Empty(issueIds); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync returns empty list when response has missing data. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_MissingData_ReturnsEmptyList() { // Arrange @@ -125,14 +124,14 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_Missi var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 42); // Assert - Assert.IsNotNull(issueIds); - Assert.IsEmpty(issueIds); + Assert.NotNull(issueIds); + Assert.Empty(issueIds); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync returns empty list on HTTP error. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_HttpError_ReturnsEmptyList() { // Arrange @@ -145,14 +144,14 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_HttpE var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 42); // Assert - Assert.IsNotNull(issueIds); - Assert.IsEmpty(issueIds); + Assert.NotNull(issueIds); + Assert.Empty(issueIds); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync returns empty list on invalid JSON. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_InvalidJson_ReturnsEmptyList() { // Arrange @@ -165,14 +164,14 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_Inval var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 42); // Assert - Assert.IsNotNull(issueIds); - Assert.IsEmpty(issueIds); + Assert.NotNull(issueIds); + Assert.Empty(issueIds); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync returns single issue ID correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_SingleIssue_ReturnsOneIssueId() { // Arrange @@ -201,15 +200,15 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_Singl var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 1); // Assert - Assert.IsNotNull(issueIds); - Assert.HasCount(1, issueIds); - Assert.AreEqual(999, issueIds[0]); + Assert.NotNull(issueIds); + Assert.Single(issueIds); + Assert.Equal(999, issueIds[0]); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync handles nodes with missing number property. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_MissingNumberProperty_SkipsInvalidNodes() { // Arrange @@ -240,16 +239,16 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_Missi var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 5); // Assert - Assert.IsNotNull(issueIds); - Assert.HasCount(2, issueIds); - Assert.AreEqual(100, issueIds[0]); - Assert.AreEqual(200, issueIds[1]); + Assert.NotNull(issueIds); + Assert.Equal(2, issueIds.Count); + Assert.Equal(100, issueIds[0]); + Assert.Equal(200, issueIds[1]); } /// /// Test that FindIssueIdsLinkedToPullRequestAsync handles pagination correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithPagination_ReturnsAllIssues() { // Arrange - Create mock handler that returns different responses for different pages @@ -261,11 +260,11 @@ public async Task GitHubGraphQLClient_FindIssueIdsLinkedToPullRequestAsync_WithP var issueIds = await client.FindIssueIdsLinkedToPullRequestAsync("owner", "repo", 10); // Assert - Assert.IsNotNull(issueIds); - Assert.HasCount(3, issueIds); - Assert.AreEqual(100, issueIds[0]); - Assert.AreEqual(200, issueIds[1]); - Assert.AreEqual(300, issueIds[2]); + Assert.NotNull(issueIds); + Assert.Equal(3, issueIds.Count); + Assert.Equal(100, issueIds[0]); + Assert.Equal(200, issueIds[1]); + Assert.Equal(300, issueIds[2]); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllIssuesTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllIssuesTests.cs index e5043b7d..144f989b 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllIssuesTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllIssuesTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubGraphQLClient GetAllIssuesAsync method. /// -[TestClass] public class GitHubGraphQLClientGetAllIssuesTests { /// /// Test that GetAllIssuesAsync returns expected issues with valid response. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssues() { // Arrange @@ -82,31 +81,31 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIss var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.HasCount(2, issues); - - Assert.AreEqual(1, issues[0].Number); - Assert.AreEqual("Bug: Application crashes on startup", issues[0].Title); - Assert.AreEqual("https://github.com/owner/repo/issues/1", issues[0].Url); - Assert.AreEqual("OPEN", issues[0].State); - Assert.IsNotNull(issues[0].Labels?.Nodes); - Assert.HasCount(1, issues[0].Labels!.Nodes!); - Assert.AreEqual("bug", issues[0].Labels!.Nodes![0].Name); - - Assert.AreEqual(2, issues[1].Number); - Assert.AreEqual("Feature: Add dark mode", issues[1].Title); - Assert.AreEqual("https://github.com/owner/repo/issues/2", issues[1].Url); - Assert.AreEqual("CLOSED", issues[1].State); - Assert.IsNotNull(issues[1].Labels?.Nodes); - Assert.HasCount(2, issues[1].Labels!.Nodes!); - Assert.AreEqual("feature", issues[1].Labels!.Nodes![0].Name); - Assert.AreEqual("enhancement", issues[1].Labels!.Nodes![1].Name); + Assert.NotNull(issues); + Assert.Equal(2, issues.Count); + + Assert.Equal(1, issues[0].Number); + Assert.Equal("Bug: Application crashes on startup", issues[0].Title); + Assert.Equal("https://github.com/owner/repo/issues/1", issues[0].Url); + Assert.Equal("OPEN", issues[0].State); + Assert.NotNull(issues[0].Labels?.Nodes); + Assert.Single(issues[0].Labels!.Nodes!); + Assert.Equal("bug", issues[0].Labels!.Nodes![0].Name); + + Assert.Equal(2, issues[1].Number); + Assert.Equal("Feature: Add dark mode", issues[1].Title); + Assert.Equal("https://github.com/owner/repo/issues/2", issues[1].Url); + Assert.Equal("CLOSED", issues[1].State); + Assert.NotNull(issues[1].Labels?.Nodes); + Assert.Equal(2, issues[1].Labels!.Nodes!.Count); + Assert.Equal("feature", issues[1].Labels!.Nodes![0].Name); + Assert.Equal("enhancement", issues[1].Labels!.Nodes![1].Name); } /// /// Test that GetAllIssuesAsync returns empty list when no issues are found. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_NoIssues_ReturnsEmptyList() { // Arrange @@ -131,14 +130,14 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_NoIssues_ReturnsEmptyLis var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.IsEmpty(issues); + Assert.NotNull(issues); + Assert.Empty(issues); } /// /// Test that GetAllIssuesAsync returns empty list when response has missing data. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_MissingData_ReturnsEmptyList() { // Arrange @@ -155,14 +154,14 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_MissingData_ReturnsEmpty var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.IsEmpty(issues); + Assert.NotNull(issues); + Assert.Empty(issues); } /// /// Test that GetAllIssuesAsync handles null nodes gracefully. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_NullNodes_ReturnsEmptyList() { // Arrange @@ -187,14 +186,14 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_NullNodes_ReturnsEmptyLi var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.IsEmpty(issues); + Assert.NotNull(issues); + Assert.Empty(issues); } /// /// Test that GetAllIssuesAsync filters out issues with missing required fields. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_InvalidIssues_FiltersThemOut() { // Arrange @@ -247,16 +246,16 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_InvalidIssues_FiltersThe var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.HasCount(1, issues); - Assert.AreEqual(2, issues[0].Number); - Assert.AreEqual("Valid issue", issues[0].Title); + Assert.NotNull(issues); + Assert.Single(issues); + Assert.Equal(2, issues[0].Number); + Assert.Equal("Valid issue", issues[0].Title); } /// /// Test that GetAllIssuesAsync handles pagination correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAllIssues() { // Arrange - Create mock handler that returns different responses for different pages @@ -268,20 +267,20 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_WithPagination_ReturnsAl var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.HasCount(3, issues); - Assert.AreEqual(1, issues[0].Number); - Assert.AreEqual("Issue from page 1", issues[0].Title); - Assert.AreEqual(2, issues[1].Number); - Assert.AreEqual("Issue from page 2", issues[1].Title); - Assert.AreEqual(3, issues[2].Number); - Assert.AreEqual("Issue from page 3", issues[2].Title); + Assert.NotNull(issues); + Assert.Equal(3, issues.Count); + Assert.Equal(1, issues[0].Number); + Assert.Equal("Issue from page 1", issues[0].Title); + Assert.Equal(2, issues[1].Number); + Assert.Equal("Issue from page 2", issues[1].Title); + Assert.Equal(3, issues[2].Number); + Assert.Equal("Issue from page 3", issues[2].Title); } /// /// Test that GetAllIssuesAsync returns empty list on exception. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_Exception_ReturnsEmptyList() { // Arrange @@ -292,8 +291,8 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_Exception_ReturnsEmptyLi var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.IsEmpty(issues); + Assert.NotNull(issues); + Assert.Empty(issues); } /// @@ -311,7 +310,7 @@ private static HttpClient CreateMockHttpClient(string responseContent, HttpStatu /// /// Test that GetAllIssuesAsync returns issues with body. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIssuesWithBody() { // Arrange @@ -332,9 +331,9 @@ public async Task GitHubGraphQLClient_GetAllIssuesAsync_ValidResponse_ReturnsIss var issues = await client.GetAllIssuesAsync("owner", "repo"); // Assert - Assert.IsNotNull(issues); - Assert.HasCount(1, issues); - Assert.AreEqual("This is an issue.\n\n```buildmark\nvisibility: internal\n```", issues[0].Body); + Assert.NotNull(issues); + Assert.Single(issues); + Assert.Equal("This is an issue.\n\n```buildmark\nvisibility: internal\n```", issues[0].Body); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllTagsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllTagsTests.cs index aaee83a1..d91e7725 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllTagsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetAllTagsTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubGraphQLClient GetAllTagsAsync method. /// -[TestClass] public class GitHubGraphQLClientGetAllTagsTests { /// /// Test that GetAllTagsAsync returns expected tag names and SHAs with valid response. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNodes() { // Arrange @@ -62,20 +61,20 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_ValidResponse_ReturnsTagNo var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.HasCount(3, tagNodes); - Assert.AreEqual("v1.0.0", tagNodes[0].Name); - Assert.AreEqual("abc123", tagNodes[0].Target?.Oid); - Assert.AreEqual("v0.9.0", tagNodes[1].Name); - Assert.AreEqual("def456", tagNodes[1].Target?.Oid); - Assert.AreEqual("v0.8.5", tagNodes[2].Name); - Assert.AreEqual("ghi789", tagNodes[2].Target?.Oid); + Assert.NotNull(tagNodes); + Assert.Equal(3, tagNodes.Count); + Assert.Equal("v1.0.0", tagNodes[0].Name); + Assert.Equal("abc123", tagNodes[0].Target?.Oid); + Assert.Equal("v0.9.0", tagNodes[1].Name); + Assert.Equal("def456", tagNodes[1].Target?.Oid); + Assert.Equal("v0.8.5", tagNodes[2].Name); + Assert.Equal("ghi789", tagNodes[2].Target?.Oid); } /// /// Test that GetAllTagsAsync returns empty list when no tags are found. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_NoTags_ReturnsEmptyList() { // Arrange @@ -100,14 +99,14 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_NoTags_ReturnsEmptyList() var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.IsEmpty(tagNodes); + Assert.NotNull(tagNodes); + Assert.Empty(tagNodes); } /// /// Test that GetAllTagsAsync returns empty list when response has missing data. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_MissingData_ReturnsEmptyList() { // Arrange @@ -124,14 +123,14 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_MissingData_ReturnsEmptyLi var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.IsEmpty(tagNodes); + Assert.NotNull(tagNodes); + Assert.Empty(tagNodes); } /// /// Test that GetAllTagsAsync returns empty list on HTTP error. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_HttpError_ReturnsEmptyList() { // Arrange @@ -144,14 +143,14 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_HttpError_ReturnsEmptyList var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.IsEmpty(tagNodes); + Assert.NotNull(tagNodes); + Assert.Empty(tagNodes); } /// /// Test that GetAllTagsAsync returns empty list on invalid JSON. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_InvalidJson_ReturnsEmptyList() { // Arrange @@ -164,14 +163,14 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_InvalidJson_ReturnsEmptyLi var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.IsEmpty(tagNodes); + Assert.NotNull(tagNodes); + Assert.Empty(tagNodes); } /// /// Test that GetAllTagsAsync returns single tag correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_SingleTag_ReturnsOneTagNode() { // Arrange @@ -198,16 +197,16 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_SingleTag_ReturnsOneTagNod var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.HasCount(1, tagNodes); - Assert.AreEqual("v2.0.0-beta1", tagNodes[0].Name); - Assert.AreEqual("xyz999", tagNodes[0].Target?.Oid); + Assert.NotNull(tagNodes); + Assert.Single(tagNodes); + Assert.Equal("v2.0.0-beta1", tagNodes[0].Name); + Assert.Equal("xyz999", tagNodes[0].Target?.Oid); } /// /// Test that GetAllTagsAsync handles nodes with missing name property. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_MissingNameProperty_SkipsInvalidNodes() { // Arrange @@ -236,18 +235,18 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_MissingNameProperty_SkipsI var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.HasCount(2, tagNodes); - Assert.AreEqual("v1.0.0", tagNodes[0].Name); - Assert.AreEqual("abc123", tagNodes[0].Target?.Oid); - Assert.AreEqual("v0.9.0", tagNodes[1].Name); - Assert.AreEqual("def456", tagNodes[1].Target?.Oid); + Assert.NotNull(tagNodes); + Assert.Equal(2, tagNodes.Count); + Assert.Equal("v1.0.0", tagNodes[0].Name); + Assert.Equal("abc123", tagNodes[0].Target?.Oid); + Assert.Equal("v0.9.0", tagNodes[1].Name); + Assert.Equal("def456", tagNodes[1].Target?.Oid); } /// /// Test that GetAllTagsAsync handles pagination correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllTags() { // Arrange - Create mock handler that returns different responses for different pages @@ -259,14 +258,14 @@ public async Task GitHubGraphQLClient_GetAllTagsAsync_WithPagination_ReturnsAllT var tagNodes = await client.GetAllTagsAsync("owner", "repo"); // Assert - Assert.IsNotNull(tagNodes); - Assert.HasCount(3, tagNodes); - Assert.AreEqual("v3.0.0", tagNodes[0].Name); - Assert.AreEqual("sha3", tagNodes[0].Target?.Oid); - Assert.AreEqual("v2.0.0", tagNodes[1].Name); - Assert.AreEqual("sha2", tagNodes[1].Target?.Oid); - Assert.AreEqual("v1.0.0", tagNodes[2].Name); - Assert.AreEqual("sha1", tagNodes[2].Target?.Oid); + Assert.NotNull(tagNodes); + Assert.Equal(3, tagNodes.Count); + Assert.Equal("v3.0.0", tagNodes[0].Name); + Assert.Equal("sha3", tagNodes[0].Target?.Oid); + Assert.Equal("v2.0.0", tagNodes[1].Name); + Assert.Equal("sha2", tagNodes[1].Target?.Oid); + Assert.Equal("v1.0.0", tagNodes[2].Name); + Assert.Equal("sha1", tagNodes[2].Target?.Oid); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetCommitsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetCommitsTests.cs index 09d50855..ad306b9e 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetCommitsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetCommitsTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubGraphQLClient GetCommitsAsync method. /// -[TestClass] public class GitHubGraphQLClientGetCommitsTests { /// /// Test that GetCommitsAsync returns expected commit SHAs with valid response. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommitShas() { // Arrange @@ -66,17 +65,17 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_ValidResponse_ReturnsCommi var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.HasCount(3, commitShas); - Assert.AreEqual("abc123", commitShas[0]); - Assert.AreEqual("def456", commitShas[1]); - Assert.AreEqual("ghi789", commitShas[2]); + Assert.NotNull(commitShas); + Assert.Equal(3, commitShas.Count); + Assert.Equal("abc123", commitShas[0]); + Assert.Equal("def456", commitShas[1]); + Assert.Equal("ghi789", commitShas[2]); } /// /// Test that GetCommitsAsync returns empty list when no commits are found. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_NoCommits_ReturnsEmptyList() { // Arrange @@ -105,14 +104,14 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_NoCommits_ReturnsEmptyList var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.IsEmpty(commitShas); + Assert.NotNull(commitShas); + Assert.Empty(commitShas); } /// /// Test that GetCommitsAsync returns empty list when response has missing data. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_MissingData_ReturnsEmptyList() { // Arrange @@ -129,14 +128,14 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_MissingData_ReturnsEmptyLi var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.IsEmpty(commitShas); + Assert.NotNull(commitShas); + Assert.Empty(commitShas); } /// /// Test that GetCommitsAsync returns empty list on HTTP error. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_HttpError_ReturnsEmptyList() { // Arrange @@ -149,14 +148,14 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_HttpError_ReturnsEmptyList var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.IsEmpty(commitShas); + Assert.NotNull(commitShas); + Assert.Empty(commitShas); } /// /// Test that GetCommitsAsync returns empty list on invalid JSON. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_InvalidJson_ReturnsEmptyList() { // Arrange @@ -169,14 +168,14 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_InvalidJson_ReturnsEmptyLi var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.IsEmpty(commitShas); + Assert.NotNull(commitShas); + Assert.Empty(commitShas); } /// /// Test that GetCommitsAsync returns single commit SHA correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_SingleCommit_ReturnsOneCommitSha() { // Arrange @@ -207,15 +206,15 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_SingleCommit_ReturnsOneCom var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.HasCount(1, commitShas); - Assert.AreEqual("abc123def456", commitShas[0]); + Assert.NotNull(commitShas); + Assert.Single(commitShas); + Assert.Equal("abc123def456", commitShas[0]); } /// /// Test that GetCommitsAsync handles nodes with missing oid property. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_MissingOidProperty_SkipsInvalidNodes() { // Arrange @@ -248,16 +247,16 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_MissingOidProperty_SkipsIn var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.HasCount(2, commitShas); - Assert.AreEqual("commit1", commitShas[0]); - Assert.AreEqual("commit2", commitShas[1]); + Assert.NotNull(commitShas); + Assert.Equal(2, commitShas.Count); + Assert.Equal("commit1", commitShas[0]); + Assert.Equal("commit2", commitShas[1]); } /// /// Test that GetCommitsAsync handles pagination correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllCommits() { // Arrange - Create mock handler that returns different responses for different pages @@ -269,11 +268,11 @@ public async Task GitHubGraphQLClient_GetCommitsAsync_WithPagination_ReturnsAllC var commitShas = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.IsNotNull(commitShas); - Assert.HasCount(3, commitShas); - Assert.AreEqual("page1commit", commitShas[0]); - Assert.AreEqual("page2commit", commitShas[1]); - Assert.AreEqual("page3commit", commitShas[2]); + Assert.NotNull(commitShas); + Assert.Equal(3, commitShas.Count); + Assert.Equal("page1commit", commitShas[0]); + Assert.Equal("page2commit", commitShas[1]); + Assert.Equal("page3commit", commitShas[2]); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs index f10cbe32..68bcb642 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetPullRequestsTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubGraphQLClient GetPullRequestsAsync method. /// -[TestClass] public class GitHubGraphQLClientGetPullRequestsTests { /// /// Test that GetPullRequestsAsync returns expected pull requests with valid response. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequests() { // Arrange @@ -87,29 +86,29 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_Returns var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.HasCount(2, pullRequests); - - Assert.AreEqual(1, pullRequests[0].Number); - Assert.AreEqual("Add feature A", pullRequests[0].Title); - Assert.AreEqual("https://github.com/owner/repo/pull/1", pullRequests[0].Url); - Assert.IsTrue(pullRequests[0].Merged); - Assert.AreEqual("abc123", pullRequests[0].MergeCommit?.Oid); - Assert.AreEqual("def456", pullRequests[0].HeadRefOid); - Assert.IsNotNull(pullRequests[0].Labels?.Nodes); - Assert.HasCount(1, pullRequests[0].Labels!.Nodes!); - Assert.AreEqual("feature", pullRequests[0].Labels!.Nodes![0].Name); - - Assert.AreEqual(2, pullRequests[1].Number); - Assert.AreEqual("Fix bug B", pullRequests[1].Title); - Assert.IsFalse(pullRequests[1].Merged); - Assert.IsNull(pullRequests[1].MergeCommit); + Assert.NotNull(pullRequests); + Assert.Equal(2, pullRequests.Count); + + Assert.Equal(1, pullRequests[0].Number); + Assert.Equal("Add feature A", pullRequests[0].Title); + Assert.Equal("https://github.com/owner/repo/pull/1", pullRequests[0].Url); + Assert.True(pullRequests[0].Merged); + Assert.Equal("abc123", pullRequests[0].MergeCommit?.Oid); + Assert.Equal("def456", pullRequests[0].HeadRefOid); + Assert.NotNull(pullRequests[0].Labels?.Nodes); + Assert.Single(pullRequests[0].Labels!.Nodes!); + Assert.Equal("feature", pullRequests[0].Labels!.Nodes![0].Name); + + Assert.Equal(2, pullRequests[1].Number); + Assert.Equal("Fix bug B", pullRequests[1].Title); + Assert.False(pullRequests[1].Merged); + Assert.Null(pullRequests[1].MergeCommit); } /// /// Test that GetPullRequestsAsync returns empty list when no pull requests are found. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_NoPullRequests_ReturnsEmptyList() { // Arrange @@ -134,14 +133,14 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_NoPullRequests_Return var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.IsEmpty(pullRequests); + Assert.NotNull(pullRequests); + Assert.Empty(pullRequests); } /// /// Test that GetPullRequestsAsync returns empty list when response has missing data. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_MissingData_ReturnsEmptyList() { // Arrange @@ -158,14 +157,14 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_MissingData_ReturnsEm var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.IsEmpty(pullRequests); + Assert.NotNull(pullRequests); + Assert.Empty(pullRequests); } /// /// Test that GetPullRequestsAsync returns empty list on HTTP error. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_HttpError_ReturnsEmptyList() { // Arrange @@ -178,14 +177,14 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_HttpError_ReturnsEmpt var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.IsEmpty(pullRequests); + Assert.NotNull(pullRequests); + Assert.Empty(pullRequests); } /// /// Test that GetPullRequestsAsync returns empty list on invalid JSON. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_InvalidJson_ReturnsEmptyList() { // Arrange @@ -198,14 +197,14 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_InvalidJson_ReturnsEm var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.IsEmpty(pullRequests); + Assert.NotNull(pullRequests); + Assert.Empty(pullRequests); } /// /// Test that GetPullRequestsAsync returns single pull request correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_SinglePullRequest_ReturnsOnePullRequest() { // Arrange @@ -244,16 +243,16 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_SinglePullRequest_Ret var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.HasCount(1, pullRequests); - Assert.AreEqual(42, pullRequests[0].Number); - Assert.AreEqual("Important PR", pullRequests[0].Title); + Assert.NotNull(pullRequests); + Assert.Single(pullRequests); + Assert.Equal(42, pullRequests[0].Number); + Assert.Equal("Important PR", pullRequests[0].Title); } /// /// Test that GetPullRequestsAsync handles nodes with missing number or title property. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_MissingNumberOrTitle_SkipsInvalidNodes() { // Arrange @@ -321,18 +320,18 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_MissingNumberOrTitle_ var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.HasCount(2, pullRequests); - Assert.AreEqual(1, pullRequests[0].Number); - Assert.AreEqual("Valid PR", pullRequests[0].Title); - Assert.AreEqual(4, pullRequests[1].Number); - Assert.AreEqual("Another valid PR", pullRequests[1].Title); + Assert.NotNull(pullRequests); + Assert.Equal(2, pullRequests.Count); + Assert.Equal(1, pullRequests[0].Number); + Assert.Equal("Valid PR", pullRequests[0].Title); + Assert.Equal(4, pullRequests[1].Number); + Assert.Equal("Another valid PR", pullRequests[1].Title); } /// /// Test that GetPullRequestsAsync handles pagination correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_ReturnsAllPullRequests() { // Arrange - Create mock handler that returns different responses for different pages @@ -344,20 +343,20 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_WithPagination_Return var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.HasCount(3, pullRequests); - Assert.AreEqual(1, pullRequests[0].Number); - Assert.AreEqual("PR from page 1", pullRequests[0].Title); - Assert.AreEqual(2, pullRequests[1].Number); - Assert.AreEqual("PR from page 2", pullRequests[1].Title); - Assert.AreEqual(3, pullRequests[2].Number); - Assert.AreEqual("PR from page 3", pullRequests[2].Title); + Assert.NotNull(pullRequests); + Assert.Equal(3, pullRequests.Count); + Assert.Equal(1, pullRequests[0].Number); + Assert.Equal("PR from page 1", pullRequests[0].Title); + Assert.Equal(2, pullRequests[1].Number); + Assert.Equal("PR from page 2", pullRequests[1].Title); + Assert.Equal(3, pullRequests[2].Number); + Assert.Equal("PR from page 3", pullRequests[2].Title); } /// /// Test that GetPullRequestsAsync returns pull requests with body. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_ReturnsPullRequestsWithBody() { // Arrange @@ -380,9 +379,9 @@ public async Task GitHubGraphQLClient_GetPullRequestsAsync_ValidResponse_Returns var pullRequests = await client.GetPullRequestsAsync("owner", "repo"); // Assert - Assert.IsNotNull(pullRequests); - Assert.HasCount(1, pullRequests); - Assert.AreEqual("This PR fixes a bug.\n\n```buildmark\ntype: bug\n```", pullRequests[0].Body); + Assert.NotNull(pullRequests); + Assert.Single(pullRequests); + Assert.Equal("This PR fixes a bug.\n\n```buildmark\ntype: bug\n```", pullRequests[0].Body); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetReleasesTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetReleasesTests.cs index ce3b890f..b762d56f 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetReleasesTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubGraphQLClientGetReleasesTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubGraphQLClient GetReleasesAsync method. /// -[TestClass] public class GitHubGraphQLClientGetReleasesTests { /// /// Test that GetReleasesAsync returns expected release tag names with valid response. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsReleaseTagNames() { // Arrange @@ -62,17 +61,17 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_ValidResponse_ReturnsRele var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.HasCount(3, releaseNodes); - Assert.AreEqual("v1.0.0", releaseNodes[0].TagName); - Assert.AreEqual("v0.9.0", releaseNodes[1].TagName); - Assert.AreEqual("v0.8.5", releaseNodes[2].TagName); + Assert.NotNull(releaseNodes); + Assert.Equal(3, releaseNodes.Count); + Assert.Equal("v1.0.0", releaseNodes[0].TagName); + Assert.Equal("v0.9.0", releaseNodes[1].TagName); + Assert.Equal("v0.8.5", releaseNodes[2].TagName); } /// /// Test that GetReleasesAsync returns empty list when no releases are found. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_NoReleases_ReturnsEmptyList() { // Arrange @@ -97,14 +96,14 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_NoReleases_ReturnsEmptyLi var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.IsEmpty(releaseNodes); + Assert.NotNull(releaseNodes); + Assert.Empty(releaseNodes); } /// /// Test that GetReleasesAsync returns empty list when response has missing data. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_MissingData_ReturnsEmptyList() { // Arrange @@ -121,14 +120,14 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_MissingData_ReturnsEmptyL var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.IsEmpty(releaseNodes); + Assert.NotNull(releaseNodes); + Assert.Empty(releaseNodes); } /// /// Test that GetReleasesAsync returns empty list on HTTP error. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_HttpError_ReturnsEmptyList() { // Arrange @@ -141,14 +140,14 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_HttpError_ReturnsEmptyLis var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.IsEmpty(releaseNodes); + Assert.NotNull(releaseNodes); + Assert.Empty(releaseNodes); } /// /// Test that GetReleasesAsync returns empty list on invalid JSON. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_InvalidJson_ReturnsEmptyList() { // Arrange @@ -161,14 +160,14 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_InvalidJson_ReturnsEmptyL var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.IsEmpty(releaseNodes); + Assert.NotNull(releaseNodes); + Assert.Empty(releaseNodes); } /// /// Test that GetReleasesAsync returns single release tag correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_SingleRelease_ReturnsOneTagName() { // Arrange @@ -195,15 +194,15 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_SingleRelease_ReturnsOneT var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.HasCount(1, releaseNodes); - Assert.AreEqual("v2.0.0-beta1", releaseNodes[0].TagName); + Assert.NotNull(releaseNodes); + Assert.Single(releaseNodes); + Assert.Equal("v2.0.0-beta1", releaseNodes[0].TagName); } /// /// Test that GetReleasesAsync handles nodes with missing tagName property. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_MissingTagNameProperty_SkipsInvalidNodes() { // Arrange @@ -232,16 +231,16 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_MissingTagNameProperty_Sk var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.HasCount(2, releaseNodes); - Assert.AreEqual("v1.0.0", releaseNodes[0].TagName); - Assert.AreEqual("v0.9.0", releaseNodes[1].TagName); + Assert.NotNull(releaseNodes); + Assert.Equal(2, releaseNodes.Count); + Assert.Equal("v1.0.0", releaseNodes[0].TagName); + Assert.Equal("v0.9.0", releaseNodes[1].TagName); } /// /// Test that GetReleasesAsync handles pagination correctly. /// - [TestMethod] + [Fact] public async Task GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAllReleases() { // Arrange - Create mock handler that returns different responses for different pages @@ -253,11 +252,11 @@ public async Task GitHubGraphQLClient_GetReleasesAsync_WithPagination_ReturnsAll var releaseNodes = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.IsNotNull(releaseNodes); - Assert.HasCount(3, releaseNodes); - Assert.AreEqual("v3.0.0", releaseNodes[0].TagName); - Assert.AreEqual("v2.0.0", releaseNodes[1].TagName); - Assert.AreEqual("v1.0.0", releaseNodes[2].TagName); + Assert.NotNull(releaseNodes); + Assert.Equal(3, releaseNodes.Count); + Assert.Equal("v3.0.0", releaseNodes[0].TagName); + Assert.Equal("v2.0.0", releaseNodes[1].TagName); + Assert.Equal("v1.0.0", releaseNodes[2].TagName); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs index be1187c4..7f0cdf1e 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubRepoConnectorTests.cs @@ -29,27 +29,26 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the GitHubRepoConnector class. /// -[TestClass] public class GitHubRepoConnectorTests { /// /// Test that GitHubRepoConnector can be instantiated. /// - [TestMethod] + [Fact] public void GitHubRepoConnector_Constructor_CreatesInstance() { // Create connector var connector = new GitHubRepoConnector(); // Verify instance - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that GitHubRepoConnector stores the provided configuration overrides. /// - [TestMethod] + [Fact] public void GitHubRepoConnector_Constructor_WithConfig_StoresConfigurationOverrides() { // Arrange @@ -64,23 +63,23 @@ public void GitHubRepoConnector_Constructor_WithConfig_StoresConfigurationOverri var connector = new GitHubRepoConnector(config); // Assert - Assert.IsNotNull(connector.ConfigurationOverrides); - Assert.AreEqual("example-owner", connector.ConfigurationOverrides.Owner); - Assert.AreEqual("example-repo", connector.ConfigurationOverrides.Repo); - Assert.AreEqual("https://api.github.com", connector.ConfigurationOverrides.BaseUrl); + Assert.NotNull(connector.ConfigurationOverrides); + Assert.Equal("example-owner", connector.ConfigurationOverrides.Owner); + Assert.Equal("example-repo", connector.ConfigurationOverrides.Repo); + Assert.Equal("https://api.github.com", connector.ConfigurationOverrides.BaseUrl); } /// /// Test that GitHubRepoConnector implements IRepoConnector. /// - [TestMethod] + [Fact] public void GitHubRepoConnector_ImplementsInterface_ReturnsTrue() { // Create connector var connector = new GitHubRepoConnector(); // Verify interface implementation - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } @@ -88,7 +87,7 @@ public void GitHubRepoConnector_ImplementsInterface_ReturnsTrue() /// /// Test that GetBuildInformationAsync works with mocked data. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_WithMockedData_ReturnsValidBuildInformation() { // Arrange - Create mock responses using helper methods @@ -112,18 +111,18 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithMockedData_Re var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("abc123def456", buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.Changes); - Assert.IsNotNull(buildInfo.Bugs); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("abc123def456", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); } /// /// Test that GetBuildInformationAsync correctly selects previous version and generates changelog link. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_WithMultipleVersions_SelectsCorrectPreviousVersionAndGeneratesChangelogLink() { // Arrange - Create mock responses with multiple versions @@ -153,24 +152,24 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithMultipleVersi var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit3", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("commit3", buildInfo.CurrentVersionTag.CommitHash); // Should have selected v1.1.0 as baseline (previous non-prerelease) - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit2", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit2", buildInfo.BaselineVersionTag.CommitHash); // Should have changelog link - Assert.IsNotNull(buildInfo.CompleteChangelogLink); + Assert.NotNull(buildInfo.CompleteChangelogLink); Assert.Contains("v1.1.0...v2.0.0", buildInfo.CompleteChangelogLink.TargetUrl); } /// /// Test that GetBuildInformationAsync correctly gathers changes from PRs with labels. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_WithPullRequests_GathersChangesCorrectly() { // Arrange - Create mock responses with PRs containing different label types @@ -217,29 +216,29 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithPullRequests_ var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.1.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.NotNull(buildInfo); + Assert.Equal("1.1.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); // PRs without linked issues are treated based on their labels // PR 100 with "bug" label should be in bugs - Assert.IsNotNull(buildInfo.Bugs); - Assert.IsGreaterThanOrEqualTo(1, buildInfo.Bugs.Count, $"Expected at least 1 bug, got {buildInfo.Bugs.Count}"); + Assert.NotNull(buildInfo.Bugs); + Assert.True(buildInfo.Bugs.Count >= 1, $"Expected at least 1 bug, got {buildInfo.Bugs.Count}"); var bugPR = buildInfo.Bugs.FirstOrDefault(b => b.Index == 100); - Assert.IsNotNull(bugPR, "PR 100 should be categorized as a bug"); - Assert.AreEqual("Fix critical bug", bugPR.Title); + Assert.True(bugPR != null, "PR 100 should be categorized as a bug"); + Assert.Equal("Fix critical bug", bugPR.Title); // PR 101 with "feature" label should be in changes - Assert.IsNotNull(buildInfo.Changes); - Assert.IsGreaterThanOrEqualTo(1, buildInfo.Changes.Count, $"Expected at least 1 change, got {buildInfo.Changes.Count}"); + Assert.NotNull(buildInfo.Changes); + Assert.True(buildInfo.Changes.Count >= 1, $"Expected at least 1 change, got {buildInfo.Changes.Count}"); var featurePR = buildInfo.Changes.FirstOrDefault(c => c.Index == 101); - Assert.IsNotNull(featurePR, "PR 101 should be categorized as a change"); - Assert.AreEqual("Add new feature", featurePR.Title); + Assert.True(featurePR != null, "PR 101 should be categorized as a change"); + Assert.Equal("Add new feature", featurePR.Title); } /// /// Test that GetBuildInformationAsync correctly identifies known issues. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_WithOpenIssues_IdentifiesKnownIssues() { // Arrange - Create mock responses with open and closed issues @@ -281,17 +280,17 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithOpenIssues_Id var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); + Assert.NotNull(buildInfo); // Known issues are open issues that aren't linked to any changes in this release - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo.KnownIssues); // Since we have no PRs, all open issues should be known issues - Assert.IsGreaterThanOrEqualTo(1, buildInfo.KnownIssues.Count, $"Expected at least 1 known issue, got {buildInfo.KnownIssues.Count}"); + Assert.True(buildInfo.KnownIssues.Count >= 1, $"Expected at least 1 known issue, got {buildInfo.KnownIssues.Count}"); // Verify at least one known issue is present var knownIssueTitles = buildInfo.KnownIssues.Select(i => i.Title).ToList(); var hasExpectedIssue = knownIssueTitles.Exists(t => t.Contains("Known bug") || t.Contains("Feature request")); - Assert.IsTrue(hasExpectedIssue, "Should have at least one of the open issues as a known issue"); + Assert.True(hasExpectedIssue, "Should have at least one of the open issues as a known issue"); } /// @@ -299,7 +298,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithOpenIssues_Id /// Example: 1.1.2-rc.1 (hash a1b2c3d4) and 1.1.2-beta.2 (hash a1b2c3d4) are re-tags. /// When processing 1.1.2-rc.1, it should skip 1.1.2-beta.2 and use 1.1.2-beta.1 (hash 734713bc). /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseWithSameCommitHash_SkipsToNextDifferentHash() { // Arrange - Create mock responses with multiple pre-releases on same and different hashes @@ -331,17 +330,17 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseWithSam var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("1.1.2-rc.1")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.1.2-rc.1", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("a1b2c3d4", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("1.1.2-rc.1", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("a1b2c3d4", buildInfo.CurrentVersionTag.CommitHash); // Should have skipped 1.1.2-beta.2 (same hash) and selected 1.1.2-beta.1 (different hash) - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.1.2-beta.1", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("734713bc", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.1.2-beta.1", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("734713bc", buildInfo.BaselineVersionTag.CommitHash); // Should have changelog link between beta.1 and rc.1 - Assert.IsNotNull(buildInfo.CompleteChangelogLink); + Assert.NotNull(buildInfo.CompleteChangelogLink); Assert.Contains("1.1.2-beta.1...1.1.2-rc.1", buildInfo.CompleteChangelogLink.TargetUrl); } @@ -349,7 +348,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseWithSam /// Test that release baseline selection skips all pre-release versions. /// Example: 1.1.2 should skip 1.1.2-rc.1, 1.1.2-beta.2, 1.1.2-beta.1 and use 1.1.1. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_ReleaseVersion_SkipsAllPreReleases() { // Arrange - Create mock responses with release and multiple pre-releases @@ -383,17 +382,17 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_ReleaseVersion_Sk var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("1.1.2")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.1.2", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit5", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("1.1.2", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("commit5", buildInfo.CurrentVersionTag.CommitHash); // Should have skipped all pre-releases and selected 1.1.1 - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.1.1", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit1", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.1.1", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit1", buildInfo.BaselineVersionTag.CommitHash); // Should have changelog link between 1.1.1 and 1.1.2 - Assert.IsNotNull(buildInfo.CompleteChangelogLink); + Assert.NotNull(buildInfo.CompleteChangelogLink); Assert.Contains("v1.1.1...1.1.2", buildInfo.CompleteChangelogLink.TargetUrl); } @@ -401,7 +400,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_ReleaseVersion_Sk /// Test that pre-release baseline selection works correctly when target is not in release history. /// This happens when generating build notes for a version that hasn't been tagged yet. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseNotInHistory_UsesLatestDifferentHash() { // Arrange - Create mock responses where target version doesn't exist yet @@ -429,21 +428,21 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseNotInHi var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("1.1.2-beta.2")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.1.2-beta.2", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("new-hash-123", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("1.1.2-beta.2", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("new-hash-123", buildInfo.CurrentVersionTag.CommitHash); // Should use most recent release with different hash - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.1.2-beta.1", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.AreEqual("commit2", buildInfo.BaselineVersionTag.CommitHash); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.1.2-beta.1", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.Equal("commit2", buildInfo.BaselineVersionTag.CommitHash); } /// /// Test that pre-release baseline selection returns null when all previous versions have the same hash. /// This is an edge case where all previous tags are re-tags of the current commit. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseAllPreviousSameHash_ReturnsNullBaseline() { // Arrange - Create mock responses where all versions are on the same commit @@ -473,12 +472,12 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseAllPrev var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("1.1.2-rc.1")); // Assert - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.1.2-rc.1", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("same-hash-123", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal("1.1.2-rc.1", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("same-hash-123", buildInfo.CurrentVersionTag.CommitHash); // Should have null baseline since all previous versions are on the same hash - Assert.IsNull(buildInfo.BaselineVersionTag); + Assert.Null(buildInfo.BaselineVersionTag); } /// @@ -486,7 +485,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_PreReleaseAllPrev /// the same merge commit SHA. This is a regression test for the key collision bug where /// ToDictionary would throw on duplicate keys. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_WithDuplicateMergeCommitSha_DoesNotThrow() { // Arrange - Create mock responses where two merged PRs share the same merge commit SHA. @@ -528,18 +527,18 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithDuplicateMerg var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Build info should be valid and not null - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual(sharedMergeCommitSha, buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.Changes); - Assert.IsNotNull(buildInfo.Bugs); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal(sharedMergeCommitSha, buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); } /// /// Test that a PR with a label whose name contains a known type as a substring is not incorrectly classified. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_PrWithSubstringMatchLabel_NotClassifiedAsBug() { // Arrange - PR with label "debugging" (contains "bug" as substring but is not "bug") @@ -571,15 +570,15 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_PrWithSubstringMa var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - PR with "debugging" label must NOT be classified as a bug - Assert.IsNotNull(buildInfo); + Assert.NotNull(buildInfo); var bugPR = buildInfo.Bugs.FirstOrDefault(b => b.Index == 100); - Assert.IsNull(bugPR, "PR with 'debugging' label must not be classified as a bug"); + Assert.True(bugPR == null, "PR with 'debugging' label must not be classified as a bug"); } /// /// Test that an open issue with a label whose name contains a known type as a substring is not a known issue. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_IssueWithSubstringMatchLabel_NotClassifiedAsKnownIssue() { // Arrange - Open issue with label "debugging" (contains "bug" as substring but is not "bug") @@ -608,15 +607,15 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_IssueWithSubstrin var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Open issue with "debugging" label must NOT be classified as a known issue - Assert.IsNotNull(buildInfo); + Assert.NotNull(buildInfo); var knownIssue = buildInfo.KnownIssues.FirstOrDefault(i => i.Index == 201); - Assert.IsNull(knownIssue, "Issue with 'debugging' label must not be classified as a known issue"); + Assert.True(knownIssue == null, "Issue with 'debugging' label must not be classified as a known issue"); } /// /// Test that GetBuildInformationAsync excludes items with visibility:internal in description. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_VisibilityInternal_ExcludesItem() { // Arrange @@ -640,15 +639,15 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_VisibilityInterna var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsEmpty(buildInfo.Changes); - Assert.IsEmpty(buildInfo.Bugs); + Assert.NotNull(buildInfo); + Assert.Empty(buildInfo.Changes); + Assert.Empty(buildInfo.Bugs); } /// /// Test that GetBuildInformationAsync includes items with visibility:public in description. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_VisibilityPublic_IncludesItem() { // Arrange @@ -672,16 +671,16 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_VisibilityPublic_ var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); + Assert.NotNull(buildInfo); // Item should be included in Changes (type "other" which is not bug) - Assert.HasCount(1, buildInfo.Changes); - Assert.AreEqual("Public PR", buildInfo.Changes[0].Title); + Assert.Single(buildInfo.Changes); + Assert.Equal("Public PR", buildInfo.Changes[0].Title); } /// /// Test that GetBuildInformationAsync classifies as bug when type:bug in description. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_TypeBugOverride_ClassifiesAsBug() { // Arrange @@ -705,16 +704,16 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_TypeBugOverride_C var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.HasCount(1, buildInfo.Bugs); - Assert.AreEqual("Bug PR", buildInfo.Bugs[0].Title); - Assert.IsEmpty(buildInfo.Changes); + Assert.NotNull(buildInfo); + Assert.Single(buildInfo.Bugs); + Assert.Equal("Bug PR", buildInfo.Bugs[0].Title); + Assert.Empty(buildInfo.Changes); } /// /// Test that GetBuildInformationAsync classifies as feature when type:feature in description. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_TypeFeatureOverride_ClassifiesAsFeature() { // Arrange @@ -738,11 +737,11 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_TypeFeatureOverri var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.HasCount(1, buildInfo.Changes); - Assert.AreEqual("Feature PR", buildInfo.Changes[0].Title); - Assert.AreEqual("feature", buildInfo.Changes[0].Type); - Assert.IsEmpty(buildInfo.Bugs); + Assert.NotNull(buildInfo); + Assert.Single(buildInfo.Changes); + Assert.Equal("Feature PR", buildInfo.Changes[0].Title); + Assert.Equal("feature", buildInfo.Changes[0].Type); + Assert.Empty(buildInfo.Bugs); } /// @@ -752,7 +751,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_TypeFeatureOverri /// What is being tested: GitHubRepoConnector.Configure stores rules /// What the assertions prove: Configure is callable on GitHubRepoConnector (public method inherited from base) /// - [TestMethod] + [Fact] public void GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue() { // Arrange - Create connector and define rules @@ -772,8 +771,8 @@ public void GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue() connector.Configure(rules, sections); // Assert - Connector is still a valid instance after configuration - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// @@ -784,7 +783,7 @@ public void GitHubRepoConnector_Configure_WithRules_HasRulesReturnsTrue() /// What the assertions prove: When rules are configured, items are routed into the /// correct sections and RoutedSections is populated on the returned BuildInformation. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_WithConfiguredRules_PopulatesRoutedSections() { // Arrange: set up two merged PRs with different labels — one feature, one bug @@ -822,22 +821,22 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithConfiguredRul var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert: RoutedSections is populated when rules are configured - Assert.IsNotNull(buildInfo.RoutedSections, "RoutedSections should be populated when rules are configured"); - Assert.HasCount(2, buildInfo.RoutedSections); + Assert.True(buildInfo.RoutedSections != null, "RoutedSections should be populated when rules are configured"); + Assert.Equal(2, buildInfo.RoutedSections.Count); // Verify the feature item was routed to the "features" section (first section) var featuresSection = buildInfo.RoutedSections[0]; - Assert.AreEqual("features", featuresSection.SectionId); - Assert.AreEqual("Features", featuresSection.SectionTitle); - Assert.HasCount(1, featuresSection.Items); - Assert.AreEqual("Feature PR", featuresSection.Items[0].Title); + Assert.Equal("features", featuresSection.SectionId); + Assert.Equal("Features", featuresSection.SectionTitle); + Assert.Single(featuresSection.Items); + Assert.Equal("Feature PR", featuresSection.Items[0].Title); // Verify the bug item was routed to the "bugs" section (second section) var bugsSection = buildInfo.RoutedSections[1]; - Assert.AreEqual("bugs", bugsSection.SectionId); - Assert.AreEqual("Bugs Fixed", bugsSection.SectionTitle); - Assert.HasCount(1, bugsSection.Items); - Assert.AreEqual("Bug PR", bugsSection.Items[0].Title); + Assert.Equal("bugs", bugsSection.SectionId); + Assert.Equal("Bugs Fixed", bugsSection.SectionTitle); + Assert.Single(bugsSection.Items); + Assert.Equal("Bug PR", bugsSection.Items[0].Title); } /// @@ -846,7 +845,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_WithConfiguredRul /// a bug whose affected-versions contain the build version is included; /// a bug with no affected-versions is included (fallback to open status). /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_KnownIssues_FilteredByAffectedVersions() { // Arrange - three open bugs: @@ -891,21 +890,21 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_KnownIssues_Filte var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.5.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.KnownIssues); // Bug 301 should be included (v1.5.0 is in [1.0.0,2.0.0)) - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "301"), "Bug 301 with affected-versions [1.0.0,2.0.0) should be a known issue for v1.5.0"); // Bug 302 should be excluded (v1.5.0 is NOT in [3.0.0,)) - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "302"), "Bug 302 with affected-versions [3.0.0,) should NOT be a known issue for v1.5.0"); // Bug 303 should be included (no affected-versions, fallback to open status) - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "303"), "Bug 303 with no affected-versions should be a known issue (open status fallback)"); } @@ -916,7 +915,7 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_KnownIssues_Filte /// a bug may be closed after being fixed in a newer release, yet still affect an older /// branch from which LTS releases are cut. /// - [TestMethod] + [Fact] public async Task GitHubRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchingAffectedVersions_IsKnownIssue() { // Arrange - three closed bugs: @@ -961,21 +960,21 @@ public async Task GitHubRepoConnector_GetBuildInformationAsync_ClosedBugWithMatc var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.5.0")); // Assert - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.KnownIssues); // Bug 304 is CLOSED but has AV [1.0.0,2.0.0) which contains v1.5.0 → IS a known issue - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "304"), "Closed bug 304 with AV [1.0.0,2.0.0) should be a known issue for v1.5.0 (LTS back-port gap)"); // Bug 305 is CLOSED and has AV [3.0.0,) which does NOT contain v1.5.0 → NOT a known issue - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "305"), "Closed bug 305 with AV [3.0.0,) should NOT be a known issue for v1.5.0"); // Bug 306 is CLOSED with no AV → NOT a known issue (open/closed fallback applies) - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "306"), "Closed bug 306 with no AV should NOT be a known issue (closed, no AV)"); } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubTests.cs new file mode 100644 index 00000000..655cd671 --- /dev/null +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/GitHubTests.cs @@ -0,0 +1,273 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.BuildMark.RepoConnectors; +using DemaConsulting.BuildMark.RepoConnectors.GitHub; +using DemaConsulting.BuildMark.Version; + +namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; + +/// +/// Sub-subsystem tests for the GitHub sub-subsystem. +/// These tests verify the contract exposed by the GitHub sub-subsystem as a whole, +/// exercising the connector through its public IRepoConnector interface. +/// +public class GitHubTests +{ + // ───────────────────────────────────────────────────────────────────────── + // BuildMark-GitHub-SubSystem + // ───────────────────────────────────────────────────────────────────────── + + /// + /// Test that the GitHub sub-subsystem provides a connector that implements IRepoConnector. + /// + [Fact] + public void GitHub_ImplementsInterface_ReturnsTrue() + { + // Arrange: create a GitHubRepoConnector instance from the GitHub sub-subsystem + var connector = new GitHubRepoConnector(); + + // Assert: the sub-subsystem connector satisfies the shared IRepoConnector interface + Assert.IsAssignableFrom(connector); + } + + /// + /// Test that the GitHub sub-subsystem returns valid build information from mocked API data. + /// + /// + /// What is being tested: GitHub sub-subsystem end-to-end build information retrieval + /// What the assertions prove: Build information is complete and accurate for a single release + /// + [Fact] + public async Task GitHub_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation() + { + // Arrange: set up a mocked GraphQL handler with a single release and commit + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("abc123def456") + .AddReleasesResponse(new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse() + .AddIssuesResponse() + .AddTagsResponse(new MockTag("v1.0.0", "abc123def456")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector(mockHttpClient); + connector.SetCommandResponse("git remote get-url origin", "https://github.com/test/repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "abc123def456"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act: retrieve build information for v1.0.0 + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); + + // Assert: build information is complete and accurate + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("abc123def456", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); + } + + /// + /// Test that the GitHub sub-subsystem selects the correct previous version as baseline. + /// + /// + /// What is being tested: GitHub sub-subsystem baseline version selection + /// What the assertions prove: The connector picks the most recent prior release as the baseline + /// + [Fact] + public async Task GitHub_GetBuildInformation_WithMultipleVersions_SelectsCorrectBaseline() + { + // Arrange: set up three release tags so the connector can pick v1.1.0 as baseline for v2.0.0 + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("commit3", "commit2", "commit1") + .AddReleasesResponse( + new MockRelease("v2.0.0", "2024-03-01T00:00:00Z"), + new MockRelease("v1.1.0", "2024-02-01T00:00:00Z"), + new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse() + .AddIssuesResponse() + .AddTagsResponse( + new MockTag("v2.0.0", "commit3"), + new MockTag("v1.1.0", "commit2"), + new MockTag("v1.0.0", "commit1")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector(mockHttpClient); + connector.SetCommandResponse("git remote get-url origin", "https://github.com/test/repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "commit3"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act: retrieve build information for v2.0.0 + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); + + // Assert: v1.1.0 is selected as baseline + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.True(buildInfo.BaselineVersionTag != null, "Previous version should be identified"); + Assert.Equal("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + } + + /// + /// Test that the GitHub sub-subsystem correctly categorizes pull requests into changes and bugs. + /// + /// + /// What is being tested: GitHub sub-subsystem PR classification at the sub-subsystem level + /// What the assertions prove: Feature PRs appear in Changes, bug PRs appear in Bugs + /// + [Fact] + public async Task GitHub_GetBuildInformation_WithPullRequests_GathersChanges() + { + // Arrange: two PRs — one labelled "feature", one labelled "bug" + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("commit3", "commit2", "commit1") + .AddReleasesResponse( + new MockRelease("v1.1.0", "2024-02-01T00:00:00Z"), + new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse( + new MockPullRequest( + Number: 101, + Title: "Add new feature", + Url: "https://github.com/test/repo/pull/101", + Merged: true, + MergeCommitSha: "commit3", + HeadRefOid: "feature-branch", + Labels: ["feature"]), + new MockPullRequest( + Number: 100, + Title: "Fix critical bug", + Url: "https://github.com/test/repo/pull/100", + Merged: true, + MergeCommitSha: "commit2", + HeadRefOid: "bugfix-branch", + Labels: ["bug"])) + .AddIssuesResponse() + .AddTagsResponse( + new MockTag("v1.1.0", "commit3"), + new MockTag("v1.0.0", "commit1")) + .AddResponse( + "closingIssuesReferences", + @"{""data"":{""repository"":{""pullRequest"":{""closingIssuesReferences"":{""nodes"":[],""pageInfo"":{""hasNextPage"":false,""endCursor"":null}}}}}}"); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector(mockHttpClient); + connector.SetCommandResponse("git remote get-url origin", "https://github.com/test/repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "commit3"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act: retrieve build information + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); + + // Assert: feature PR is in Changes, bug PR is in Bugs + Assert.NotNull(buildInfo); + var featurePR = buildInfo.Changes.FirstOrDefault(c => c.Index == 101); + Assert.True(featurePR != null, "Feature PR should be in Changes"); + + var bugPR = buildInfo.Bugs.FirstOrDefault(b => b.Index == 100); + Assert.True(bugPR != null, "Bug PR should be in Bugs"); + } + + /// + /// Test that the GitHub sub-subsystem correctly identifies open issues as known issues. + /// + /// + /// What is being tested: GitHub sub-subsystem known-issues identification + /// What the assertions prove: Open issues with "bug" label appear in KnownIssues + /// + [Fact] + public async Task GitHub_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues() + { + // Arrange: one open issue that is not resolved in this release + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("commit1") + .AddReleasesResponse(new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse() + .AddIssuesResponse( + new MockIssue( + Number: 201, + Title: "Known bug in feature X", + Url: "https://github.com/test/repo/issues/201", + State: "OPEN", + Labels: ["bug"])) + .AddTagsResponse(new MockTag("v1.0.0", "commit1")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector(mockHttpClient); + connector.SetCommandResponse("git remote get-url origin", "https://github.com/test/repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "commit1"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act: retrieve build information + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); + + // Assert: open issue surfaces as a known issue + Assert.NotNull(buildInfo); + Assert.True(buildInfo.KnownIssues.Count > 0, "Should have at least one known issue"); + var knownIssue = buildInfo.KnownIssues.FirstOrDefault(i => i.Index == 201); + Assert.True(knownIssue != null, "Open issue 201 should appear in KnownIssues"); + Assert.Equal("Known bug in feature X", knownIssue.Title); + } + + /// + /// Test that the GitHub sub-subsystem skips pre-releases when building the version baseline. + /// + /// + /// What is being tested: GitHub sub-subsystem pre-release handling + /// What the assertions prove: A release version uses only prior release tags as its baseline + /// + [Fact] + public async Task GitHub_GetBuildInformation_ReleaseVersion_SkipsPreReleases() + { + // Arrange: mix of release and pre-release tags + using var mockHandler = new MockGitHubGraphQLHttpMessageHandler() + .AddCommitsResponse("commit4", "commit3", "commit2", "commit1") + .AddReleasesResponse( + new MockRelease("v2.0.0", "2024-04-01T00:00:00Z"), + new MockRelease("v2.0.0-rc.1", "2024-03-15T00:00:00Z"), + new MockRelease("v1.1.0", "2024-02-01T00:00:00Z"), + new MockRelease("v1.0.0", "2024-01-01T00:00:00Z")) + .AddPullRequestsResponse() + .AddIssuesResponse() + .AddTagsResponse( + new MockTag("v2.0.0", "commit4"), + new MockTag("v2.0.0-rc.1", "commit3"), + new MockTag("v1.1.0", "commit2"), + new MockTag("v1.0.0", "commit1")); + + using var mockHttpClient = new HttpClient(mockHandler); + var connector = new MockableGitHubRepoConnector(mockHttpClient); + connector.SetCommandResponse("git remote get-url origin", "https://github.com/test/repo.git"); + connector.SetCommandResponse("git rev-parse --abbrev-ref HEAD", "main"); + connector.SetCommandResponse("git rev-parse HEAD", "commit4"); + connector.SetCommandResponse("gh auth token", "test-token"); + + // Act: retrieve build information for v2.0.0 (a release version) + var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); + + // Assert: baseline should be v1.1.0, not the pre-release v2.0.0-rc.1 + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.True(buildInfo.BaselineVersionTag != null, "Baseline version should be set"); + Assert.True(buildInfo.BaselineVersionTag.VersionTag.FullVersion == "1.1.0", "Release version should skip pre-releases when selecting baseline"); + } +} diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandlerTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandlerTests.cs index 0d8a78d0..2b95e973 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandlerTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/GitHub/MockGitHubGraphQLHttpMessageHandlerTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.GitHub; /// /// Tests for the MockGitHubGraphQLHttpMessageHandler class. /// -[TestClass] public class MockGitHubGraphQLHttpMessageHandlerTests { /// /// Test that MockGitHubGraphQLHttpMessageHandler can be configured with multiple responses. /// - [TestMethod] + [Fact] public async Task MockGitHubGraphQLHttpMessageHandler_MultipleResponses_ReturnsCorrectResponse() { // Arrange - Use helper methods for standard responses @@ -44,19 +43,19 @@ public async Task MockGitHubGraphQLHttpMessageHandler_MultipleResponses_ReturnsC // Act & Assert - GetCommitsAsync should return commit1 var commits = await client.GetCommitsAsync("owner", "repo", "main"); - Assert.HasCount(1, commits); - Assert.AreEqual("commit1", commits[0]); + Assert.Single(commits); + Assert.Equal("commit1", commits[0]); // Act & Assert - GetReleasesAsync should return different data var releases = await client.GetReleasesAsync("owner", "repo"); - Assert.HasCount(1, releases); - Assert.AreEqual("v1.0.0", releases[0].TagName); + Assert.Single(releases); + Assert.Equal("v1.0.0", releases[0].TagName); } /// /// Test that MockGitHubGraphQLHttpMessageHandler returns default response when no pattern matches. /// - [TestMethod] + [Fact] public async Task MockGitHubGraphQLHttpMessageHandler_NoPatternMatches_ReturnsDefaultResponse() { // Arrange - Use helper method for empty repository response @@ -70,14 +69,14 @@ public async Task MockGitHubGraphQLHttpMessageHandler_NoPatternMatches_ReturnsDe var commits = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Should return empty list since repository is null - Assert.IsNotNull(commits); - Assert.IsEmpty(commits); + Assert.NotNull(commits); + Assert.Empty(commits); } /// /// Test that MockGitHubGraphQLHttpMessageHandler supports method chaining. /// - [TestMethod] + [Fact] public void MockGitHubGraphQLHttpMessageHandler_MethodChaining_WorksCorrectly() { // Arrange & Act - Chain multiple AddResponse calls @@ -87,13 +86,13 @@ public void MockGitHubGraphQLHttpMessageHandler_MethodChaining_WorksCorrectly() .SetDefaultResponse("default"); // Assert - Verify handler was created - Assert.IsNotNull(mockHandler); + Assert.NotNull(mockHandler); } /// /// Test that MockGitHubGraphQLHttpMessageHandler can handle multiple commits. /// - [TestMethod] + [Fact] public async Task MockGitHubGraphQLHttpMessageHandler_MultipleCommits_ReturnsAllCommits() { // Arrange @@ -107,16 +106,16 @@ public async Task MockGitHubGraphQLHttpMessageHandler_MultipleCommits_ReturnsAll var commits = await client.GetCommitsAsync("owner", "repo", "main"); // Assert - Assert.HasCount(3, commits); - Assert.AreEqual("commit1", commits[0]); - Assert.AreEqual("commit2", commits[1]); - Assert.AreEqual("commit3", commits[2]); + Assert.Equal(3, commits.Count); + Assert.Equal("commit1", commits[0]); + Assert.Equal("commit2", commits[1]); + Assert.Equal("commit3", commits[2]); } /// /// Test that MockGitHubGraphQLHttpMessageHandler can handle multiple releases. /// - [TestMethod] + [Fact] public async Task MockGitHubGraphQLHttpMessageHandler_MultipleReleases_ReturnsAllReleases() { // Arrange @@ -133,10 +132,10 @@ public async Task MockGitHubGraphQLHttpMessageHandler_MultipleReleases_ReturnsAl var releases = await client.GetReleasesAsync("owner", "repo"); // Assert - Assert.HasCount(3, releases); - Assert.AreEqual("v1.0.0", releases[0].TagName); - Assert.AreEqual("v1.1.0", releases[1].TagName); - Assert.AreEqual("v2.0.0", releases[2].TagName); + Assert.Equal(3, releases.Count); + Assert.Equal("v1.0.0", releases[0].TagName); + Assert.Equal("v1.1.0", releases[1].TagName); + Assert.Equal("v2.0.0", releases[2].TagName); } } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/ItemRouterTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/ItemRouterTests.cs index 48a2a8b7..5470c7ed 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/ItemRouterTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/ItemRouterTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors; /// /// Tests for the ItemRouter class. /// -[TestClass] public class ItemRouterTests { /// /// Test that matching items are routed to the first configured section. /// - [TestMethod] + [Fact] public void ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection() { // Arrange @@ -60,16 +59,16 @@ public void ItemRouter_Route_MatchingRuleRoutesItemToConfiguredSection() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - Assert.HasCount(1, routedItems["changes"]); - Assert.AreEqual("1", routedItems["changes"][0].Id); - Assert.HasCount(1, routedItems["bugs"]); - Assert.AreEqual("2", routedItems["bugs"][0].Id); + Assert.Single(routedItems["changes"]); + Assert.Equal("1", routedItems["changes"][0].Id); + Assert.Single(routedItems["bugs"]); + Assert.Equal("2", routedItems["bugs"][0].Id); } /// /// Test that suppressed routes exclude matching items. /// - [TestMethod] + [Fact] public void ItemRouter_Route_SuppressedRouteOmitsMatchingItem() { // Arrange @@ -88,13 +87,13 @@ public void ItemRouter_Route_SuppressedRouteOmitsMatchingItem() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - Assert.IsEmpty(routedItems["changes"]); + Assert.Empty(routedItems["changes"]); } /// /// Test that a rule with null Match block acts as a catch-all and matches all items. /// - [TestMethod] + [Fact] public void ItemRouter_Route_WithNullMatchBlock_MatchesAllItems() { // Arrange @@ -117,14 +116,14 @@ public void ItemRouter_Route_WithNullMatchBlock_MatchesAllItems() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - catch-all routes all items to "other", leaving "changes" empty - Assert.IsEmpty(routedItems["changes"]); - Assert.HasCount(2, routedItems["other"]); + Assert.Empty(routedItems["changes"]); + Assert.Equal(2, routedItems["other"].Count); } /// /// Test that WorkItemType matching routes only items with a matching type. /// - [TestMethod] + [Fact] public void ItemRouter_Route_WithWorkItemTypeMatch_RoutesMatchingItem() { // Arrange @@ -151,16 +150,16 @@ public void ItemRouter_Route_WithWorkItemTypeMatch_RoutesMatchingItem() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - only the bug item is routed to "bugs"; feature falls through to default - Assert.HasCount(1, routedItems["changes"]); - Assert.AreEqual("1", routedItems["changes"][0].Id); - Assert.HasCount(1, routedItems["bugs"]); - Assert.AreEqual("2", routedItems["bugs"][0].Id); + Assert.Single(routedItems["changes"]); + Assert.Equal("1", routedItems["changes"][0].Id); + Assert.Single(routedItems["bugs"]); + Assert.Equal("2", routedItems["bugs"][0].Id); } /// /// Test that items with no matching rule are routed to the default (first) section. /// - [TestMethod] + [Fact] public void ItemRouter_Route_WithNoMatchingRule_RoutesToDefaultSection() { // Arrange @@ -183,15 +182,15 @@ public void ItemRouter_Route_WithNoMatchingRule_RoutesToDefaultSection() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - unmatched item lands in the first section ("changes") - Assert.HasCount(1, routedItems["changes"]); - Assert.AreEqual("1", routedItems["changes"][0].Id); - Assert.IsEmpty(routedItems["bugs"]); + Assert.Single(routedItems["changes"]); + Assert.Equal("1", routedItems["changes"][0].Id); + Assert.Empty(routedItems["bugs"]); } /// /// Test that items routed to a section not in the initial list create a new bucket entry. /// - [TestMethod] + [Fact] public void ItemRouter_Route_ItemNotInConfiguredSections_CreatesNewSection() { // Arrange @@ -210,16 +209,16 @@ public void ItemRouter_Route_ItemNotInConfiguredSections_CreatesNewSection() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - item is routed to the new section, which is created dynamically - Assert.IsEmpty(routedItems["changes"]); - Assert.IsTrue(routedItems.TryGetValue("new-section", out var newSection)); - Assert.HasCount(1, newSection); - Assert.AreEqual("1", newSection[0].Id); + Assert.Empty(routedItems["changes"]); + Assert.True(routedItems.TryGetValue("new-section", out var newSection)); + Assert.Single(newSection); + Assert.Equal("1", newSection[0].Id); } /// /// Test that label matching is case-insensitive. /// - [TestMethod] + [Fact] public void ItemRouter_Route_WithCaseInsensitiveLabelMatch_RoutesItem() { // Arrange - item type is "Bug" (capitalized) while rule label is "bug" (lowercase) @@ -245,15 +244,15 @@ public void ItemRouter_Route_WithCaseInsensitiveLabelMatch_RoutesItem() var routedItems = ItemRouter.Route(items, rules, sections); // Assert - "Bug" type matches "bug" label rule due to case-insensitive comparison - Assert.IsEmpty(routedItems["changes"]); - Assert.HasCount(1, routedItems["bugs"]); - Assert.AreEqual("1", routedItems["bugs"][0].Id); + Assert.Empty(routedItems["changes"]); + Assert.Single(routedItems["bugs"]); + Assert.Equal("1", routedItems["bugs"][0].Id); } /// /// Test that the suppressed route value is matched case-insensitively. /// - [TestMethod] + [Fact] public void ItemRouter_Route_WithCaseInsensitiveSuppressedRoute_OmitsMatchingItem() { // Arrange - route value is "SUPPRESSED" (uppercase) to verify case-insensitive comparison @@ -272,6 +271,6 @@ public void ItemRouter_Route_WithCaseInsensitiveSuppressedRoute_OmitsMatchingIte var routedItems = ItemRouter.Route(items, rules, sections); // Assert - item is omitted even when the route value uses uppercase "SUPPRESSED" - Assert.IsEmpty(routedItems["changes"]); + Assert.Empty(routedItems["changes"]); } } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockRepoConnectorTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockRepoConnectorTests.cs index b53c4ec6..774dbbde 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockRepoConnectorTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockRepoConnectorTests.cs @@ -29,34 +29,33 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors.Mock; /// /// Tests for the MockRepoConnector class. /// -[TestClass] public class MockRepoConnectorTests { /// /// Test that MockRepoConnector can be instantiated. /// - [TestMethod] + [Fact] public void MockRepoConnector_Constructor_CreatesInstance() { // Create connector var connector = new MockRepoConnector(); // Verify instance - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that MockRepoConnector implements IRepoConnector. /// - [TestMethod] + [Fact] public void MockRepoConnector_ImplementsInterface() { // Create connector var connector = new MockRepoConnector(); // Verify interface implementation - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// @@ -66,7 +65,7 @@ public void MockRepoConnector_ImplementsInterface() /// What is being tested: MockRepoConnector.GetBuildInformationAsync with explicit version /// What the assertions prove: Build information is returned with the correct version tag /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_ReturnsExpectedVersion() { // Arrange - Create connector and specify a known version @@ -77,9 +76,9 @@ public async Task MockRepoConnector_GetBuildInformationAsync_ReturnsExpectedVers var buildInfo = await connector.GetBuildInformationAsync(version); // Assert - Verify build information contains expected version - Assert.IsNotNull(buildInfo); - Assert.AreEqual(version.Tag, buildInfo.CurrentVersionTag.VersionTag.Tag); - Assert.AreEqual("mno345pqr678", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo); + Assert.Equal(version.Tag, buildInfo.CurrentVersionTag.VersionTag.Tag); + Assert.Equal("mno345pqr678", buildInfo.CurrentVersionTag.CommitHash); } /// @@ -89,7 +88,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_ReturnsExpectedVers /// What is being tested: MockRepoConnector's ability to resolve version information from tags /// What the assertions prove: Build information is correctly generated with version from tag data /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_WithValidVersionFromTags_ReturnsCorrectBaseline() { // Arrange - Create connector without explicit version, relying on tag matching @@ -100,9 +99,9 @@ public async Task MockRepoConnector_GetBuildInformationAsync_WithValidVersionFro var buildInfo = await connector.GetBuildInformationAsync(version); // Assert - Verify build information is returned successfully - Assert.IsNotNull(buildInfo, "Build information should be returned for valid version"); - Assert.AreEqual("v1.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); - Assert.IsNotNull(buildInfo.CurrentVersionTag.CommitHash, "Commit hash should be set"); + Assert.True(buildInfo != null, "Build information should be returned for valid version"); + Assert.Equal("v1.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); + Assert.True(buildInfo.CurrentVersionTag.CommitHash != null, "Commit hash should be set"); } /// @@ -112,7 +111,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_WithValidVersionFro /// What is being tested: MockRepoConnector returns complete BuildInformation structure /// What the assertions prove: All expected components (changes, bugs, known issues) are present /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_ReturnsCompleteInformation() { // Arrange - Create connector with version that has associated changes @@ -123,11 +122,11 @@ public async Task MockRepoConnector_GetBuildInformationAsync_ReturnsCompleteInfo var buildInfo = await connector.GetBuildInformationAsync(version); // Assert - Verify all expected data structures are present - Assert.IsNotNull(buildInfo, "Build information should not be null"); - Assert.IsNotNull(buildInfo.Changes, "Changes list should not be null"); - Assert.IsNotNull(buildInfo.Bugs, "Bugs list should not be null"); - Assert.IsNotNull(buildInfo.KnownIssues, "Known issues list should not be null"); - Assert.IsNotNull(buildInfo.CurrentVersionTag, "Current version tag should not be null"); + Assert.True(buildInfo != null, "Build information should not be null"); + Assert.True(buildInfo.Changes != null, "Changes list should not be null"); + Assert.True(buildInfo.Bugs != null, "Bugs list should not be null"); + Assert.True(buildInfo.KnownIssues != null, "Known issues list should not be null"); + Assert.True(buildInfo.CurrentVersionTag != null, "Current version tag should not be null"); } /// @@ -137,7 +136,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_ReturnsCompleteInfo /// What is being tested: MockRepoConnector categorization of changes by type /// What the assertions prove: Changes are correctly categorized into bugs and other changes /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_CategorizesChangesCorrectly() { // Arrange - Create connector and request version with known changes @@ -152,12 +151,12 @@ public async Task MockRepoConnector_GetBuildInformationAsync_CategorizesChangesC // - Issue #2 is a bug (type "bug") // - Issue #3 is documentation (type "documentation") var allItems = buildInfo.Changes.Concat(buildInfo.Bugs).ToList(); - Assert.IsGreaterThan(0, allItems.Count, "Should have at least one change"); + Assert.True(allItems.Count > 0, "Should have at least one change"); // Verify bugs only contain items with type "bug" foreach (var bug in buildInfo.Bugs) { - Assert.AreEqual("bug", bug.Type, $"Bug {bug.Id} should have type 'bug'"); + Assert.True(bug.Type == "bug", $"Bug {bug.Id} should have type 'bug'"); } } @@ -168,7 +167,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_CategorizesChangesC /// What is being tested: MockRepoConnector.Configure stores provided rules /// What the assertions prove: After Configure with non-empty rules, GetBuildInformationAsync returns RoutedSections /// - [TestMethod] + [Fact] public async Task MockRepoConnector_Configure_StoresRulesAndSections() { // Arrange - Create connector and define rules @@ -189,7 +188,7 @@ public async Task MockRepoConnector_Configure_StoresRulesAndSections() var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("2.0.0")); // Assert - Routing was applied (RoutedSections is populated when rules are configured) - Assert.IsNotNull(buildInfo.RoutedSections, "RoutedSections should be set when rules are configured"); + Assert.True(buildInfo.RoutedSections != null, "RoutedSections should be set when rules are configured"); } /// @@ -199,7 +198,7 @@ public async Task MockRepoConnector_Configure_StoresRulesAndSections() /// What is being tested: MockRepoConnector routes items when rules are configured /// What the assertions prove: RoutedSections contains expected section titles /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRoutedSections() { // Arrange - Create connector with routing rules @@ -218,12 +217,12 @@ public async Task MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRo var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("2.0.0")); // Assert - RoutedSections is populated with the configured sections - Assert.IsNotNull(buildInfo.RoutedSections, "RoutedSections should not be null when rules are configured"); - Assert.HasCount(2, buildInfo.RoutedSections, "Should have two configured sections"); + Assert.True(buildInfo.RoutedSections != null, "RoutedSections should not be null when rules are configured"); + Assert.Equal(2, buildInfo.RoutedSections.Count); var sectionTitles = buildInfo.RoutedSections.Select(s => s.SectionTitle).ToList(); - Assert.Contains("Features", sectionTitles, "Features section should be present"); - Assert.Contains("Bugs", sectionTitles, "Bugs section should be present"); + Assert.Contains("Features", sectionTitles); + Assert.Contains("Bugs", sectionTitles); } /// @@ -233,7 +232,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_WithRules_ReturnsRo /// What is being tested: MockRepoConnector does not route when no rules are configured /// What the assertions prove: RoutedSections is null when no rules are configured /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_WithoutRules_ReturnsNullRoutedSections() { // Arrange - Create connector without configuring rules @@ -243,7 +242,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_WithoutRules_Return var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("2.0.0")); // Assert - RoutedSections should be null (legacy mode) - Assert.IsNull(buildInfo.RoutedSections, "RoutedSections should be null when no rules are configured"); + Assert.True(buildInfo.RoutedSections == null, "RoutedSections should be null when no rules are configured"); } /// @@ -255,7 +254,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_WithoutRules_Return /// What the assertions prove: Bug with out-of-range affected-versions is excluded; /// building for an in-range version includes it /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_KnownIssues_FilteredByAffectedVersions() { // Arrange - Use version v2.0.0 (issue 5 has [5.0.0,) so it is excluded) @@ -265,14 +264,14 @@ public async Task MockRepoConnector_GetBuildInformationAsync_KnownIssues_Filtere var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - issue 4 (no affected-versions) and issue 6 (no affected-versions) are included - Assert.IsNotNull(buildInfo.KnownIssues); - Assert.IsTrue( + Assert.NotNull(buildInfo.KnownIssues); + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "4"), "Bug 4 with no affected-versions should be a known issue"); - Assert.IsFalse( + Assert.False( buildInfo.KnownIssues.Exists(i => i.Id == "5"), "Bug 5 with affected-versions [5.0.0,) should NOT be a known issue for v2.0.0"); - Assert.IsTrue( + Assert.True( buildInfo.KnownIssues.Exists(i => i.Id == "6"), "Bug 6 with no affected-versions should be a known issue"); } @@ -287,7 +286,7 @@ public async Task MockRepoConnector_GetBuildInformationAsync_KnownIssues_Filtere /// What the assertions prove: LTS back-port gap is modelled correctly — a closed bug /// with a matching AV is included; the same bug is excluded for an unaffected version /// - [TestMethod] + [Fact] public async Task MockRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchingAffectedVersions_IsKnownIssue() { // Arrange - issue 7 is closed and has AV [1.0.0,1.0.0] (only matches v1.0.0 exactly) @@ -297,8 +296,8 @@ public async Task MockRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchi var buildInfoV1 = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert - issue 7 must be a known issue for v1.0.0 - Assert.IsNotNull(buildInfoV1.KnownIssues); - Assert.IsTrue( + Assert.NotNull(buildInfoV1.KnownIssues); + Assert.True( buildInfoV1.KnownIssues.Exists(i => i.Id == "7"), "Closed bug 7 with AV [1.0.0,1.0.0] should be a known issue for v1.0.0 (LTS back-port gap)"); @@ -306,8 +305,8 @@ public async Task MockRepoConnector_GetBuildInformationAsync_ClosedBugWithMatchi var buildInfoV2 = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert - issue 7 must NOT be a known issue for v2.0.0 - Assert.IsNotNull(buildInfoV2.KnownIssues); - Assert.IsFalse( + Assert.NotNull(buildInfoV2.KnownIssues); + Assert.False( buildInfoV2.KnownIssues.Exists(i => i.Id == "7"), "Closed bug 7 with AV [1.0.0,1.0.0] should NOT be a known issue for v2.0.0"); } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockTests.cs new file mode 100644 index 00000000..b3e75168 --- /dev/null +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/Mock/MockTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.BuildMark.RepoConnectors; +using DemaConsulting.BuildMark.RepoConnectors.Mock; +using DemaConsulting.BuildMark.Version; + +namespace DemaConsulting.BuildMark.Tests.RepoConnectors.Mock; + +/// +/// Sub-subsystem tests for the Mock sub-subsystem. +/// These tests verify the contract exposed by the Mock sub-subsystem as a whole. +/// +public class MockTests +{ + // ───────────────────────────────────────────────────────────────────────── + // BuildMark-Mock-SubSystem + // ───────────────────────────────────────────────────────────────────────── + + /// + /// Test that the Mock sub-subsystem provides a connector that implements IRepoConnector. + /// + [Fact] + public void Mock_ImplementsInterface_ReturnsTrue() + { + // Arrange: create a MockRepoConnector instance from the Mock sub-subsystem + var connector = new MockRepoConnector(); + + // Assert: the sub-subsystem connector satisfies the shared IRepoConnector interface + Assert.IsAssignableFrom(connector); + } + + /// + /// Test that the Mock sub-subsystem returns build information with the expected version. + /// + /// + /// What is being tested: Mock sub-subsystem produces version-correct build information + /// What the assertions prove: The connector version tag matches the requested version + /// + [Fact] + public async Task Mock_GetBuildInformation_ReturnsExpectedVersion() + { + // Arrange: create connector and specify a known version + var connector = new MockRepoConnector(); + var version = VersionTag.Create("v2.0.0"); + + // Act: retrieve build information for the specified version + var buildInfo = await connector.GetBuildInformationAsync(version); + + // Assert: build information contains the correct version tag + Assert.NotNull(buildInfo); + Assert.Equal(version.Tag, buildInfo.CurrentVersionTag.VersionTag.Tag); + } + + /// + /// Test that the Mock sub-subsystem returns a complete BuildInformation structure. + /// + /// + /// What is being tested: Mock sub-subsystem returns a fully populated BuildInformation + /// What the assertions prove: All required collections (Changes, Bugs, KnownIssues) are present + /// + [Fact] + public async Task Mock_GetBuildInformation_ReturnsCompleteInformation() + { + // Arrange: create connector with a version that has associated changes + var connector = new MockRepoConnector(); + var version = VersionTag.Create("v2.0.0"); + + // Act: retrieve build information + var buildInfo = await connector.GetBuildInformationAsync(version); + + // Assert: all required data structures are present + Assert.True(buildInfo != null, "BuildInformation should not be null"); + Assert.True(buildInfo.Changes != null, "Changes list should not be null"); + Assert.True(buildInfo.Bugs != null, "Bugs list should not be null"); + Assert.True(buildInfo.KnownIssues != null, "KnownIssues list should not be null"); + Assert.True(buildInfo.CurrentVersionTag != null, "CurrentVersionTag should not be null"); + } +} diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs index 35dcd27c..6af56b6b 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorBaseTests.cs @@ -28,7 +28,6 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors; /// /// Tests for the RepoConnectorBase class. /// -[TestClass] public class RepoConnectorBaseTests { /// @@ -71,7 +70,7 @@ public override Task GetBuildInformationAsync(VersionTag? vers /// /// Test that Configure with rules sets HasRules to true. /// - [TestMethod] + [Fact] public void RepoConnectorBase_Configure_StoresRulesAndSections_HasRulesReturnsTrue() { // Arrange - Create testable connector and define rules @@ -89,13 +88,13 @@ public void RepoConnectorBase_Configure_StoresRulesAndSections_HasRulesReturnsTr connector.Configure(rules, sections); // Assert - HasRules is true after configuration - Assert.IsTrue(connector.ExposedHasRules, "HasRules should be true when rules are configured"); + Assert.True(connector.ExposedHasRules, "HasRules should be true when rules are configured"); } /// /// Test that Configure with empty rules sets HasRules to false. /// - [TestMethod] + [Fact] public void RepoConnectorBase_Configure_EmptyRules_HasRulesReturnsFalse() { // Arrange - Create testable connector @@ -105,13 +104,13 @@ public void RepoConnectorBase_Configure_EmptyRules_HasRulesReturnsFalse() connector.Configure([], []); // Assert - HasRules is false when no rules provided - Assert.IsFalse(connector.ExposedHasRules, "HasRules should be false when no rules are configured"); + Assert.False(connector.ExposedHasRules, "HasRules should be false when no rules are configured"); } /// /// Test that ApplyRules routes items into configured sections. /// - [TestMethod] + [Fact] public void RepoConnectorBase_ApplyRules_RoutesItemsToConfiguredSections() { // Arrange - Create testable connector with rules that route bugs to bugs and everything else to features @@ -137,25 +136,25 @@ public void RepoConnectorBase_ApplyRules_RoutesItemsToConfiguredSections() var sections = connector.ExposedApplyRules(items); // Assert - Two sections returned - Assert.HasCount(2, sections, "Should have two sections"); + Assert.Equal(2, sections.Count); // Assert - Feature item routed to features section var featuresSection = sections.FirstOrDefault(s => s.SectionId == "features"); - Assert.IsNotNull(featuresSection.SectionTitle, "Features section should be present"); - Assert.HasCount(1, featuresSection.Items, "Features section should have one item"); - Assert.AreEqual("1", featuresSection.Items[0].Id, "Feature item should be in features section"); + Assert.True(featuresSection.SectionTitle != null, "Features section should be present"); + Assert.Single(featuresSection.Items); + Assert.True(featuresSection.Items[0].Id == "1", "Feature item should be in features section"); // Assert - Bug item routed to bugs section var bugsSection = sections.FirstOrDefault(s => s.SectionId == "bugs"); - Assert.IsNotNull(bugsSection.SectionTitle, "Bugs section should be present"); - Assert.HasCount(1, bugsSection.Items, "Bugs section should have one item"); - Assert.AreEqual("2", bugsSection.Items[0].Id, "Bug item should be in bugs section"); + Assert.True(bugsSection.SectionTitle != null, "Bugs section should be present"); + Assert.Single(bugsSection.Items); + Assert.True(bugsSection.Items[0].Id == "2", "Bug item should be in bugs section"); } /// /// Test that FindVersionIndex finds the correct index when tags have different prefixes but the same semantic version. /// - [TestMethod] + [Fact] public void RepoConnectorBase_FindVersionIndex_DifferentPrefixSameVersion_ReturnsCorrectIndex() { // Arrange - Create version tags with different prefixes but same semantic version @@ -174,13 +173,13 @@ public void RepoConnectorBase_FindVersionIndex_DifferentPrefixSameVersion_Return var foundIndex = TestableRepoConnector.ExposedFindVersionIndex(versions, targetVersion); // Assert - Should find the first matching semantic version (index 1) - Assert.AreEqual(1, foundIndex, "Should find the first tag with matching semantic version 1.2.3"); + Assert.True(foundIndex == 1, "Should find the first tag with matching semantic version 1.2.3"); } /// /// Test that FindVersionIndex returns -1 when the target version is not in the list. /// - [TestMethod] + [Fact] public void RepoConnectorBase_FindVersionIndex_VersionNotInList_ReturnsMinusOne() { // Arrange - Create a version list that does not contain the target version @@ -198,6 +197,6 @@ public void RepoConnectorBase_FindVersionIndex_VersionNotInList_ReturnsMinusOne( var foundIndex = TestableRepoConnector.ExposedFindVersionIndex(versions, targetVersion); // Assert - Should return -1 when version is not found - Assert.AreEqual(-1, foundIndex, "Should return -1 when the target version is not in the list"); + Assert.True(foundIndex == -1, "Should return -1 when the target version is not in the list"); } } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorFactoryTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorFactoryTests.cs index fef86f73..79461860 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorFactoryTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorFactoryTests.cs @@ -28,40 +28,39 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors; /// /// Tests for the RepoConnectorFactory class. /// -[TestClass] public class RepoConnectorFactoryTests { /// /// Test that Create returns a connector instance. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_ReturnsConnector() { // Create a repository connector var connector = RepoConnectorFactory.Create(); // Verify connector is created successfully - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that Create returns GitHubRepoConnector for this repository. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_ReturnsGitHubConnectorForThisRepo() { // Create connector for this repository var connector = RepoConnectorFactory.Create(); // Verify GitHub connector is returned - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that Create forwards GitHub connector configuration to the created connector. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfiguration() { // Arrange @@ -80,18 +79,18 @@ public void RepoConnectorFactory_Create_WithConnectorConfig_ForwardsGitHubConfig var connector = RepoConnectorFactory.Create(config); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); var forwardedConfig = ((GitHubRepoConnector)connector).ConfigurationOverrides; - Assert.IsNotNull(forwardedConfig); - Assert.AreEqual("example-owner", forwardedConfig.Owner); - Assert.AreEqual("example-repo", forwardedConfig.Repo); - Assert.AreEqual("https://api.github.com", forwardedConfig.BaseUrl); + Assert.NotNull(forwardedConfig); + Assert.Equal("example-owner", forwardedConfig.Owner); + Assert.Equal("example-repo", forwardedConfig.Repo); + Assert.Equal("https://api.github.com", forwardedConfig.BaseUrl); } /// /// Test that Create with azure-devops type creates an AzureDevOpsRepoConnector. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsConnector() { // Arrange - create config with Azure DevOps connector type @@ -101,14 +100,14 @@ public void RepoConnectorFactory_Create_WithAzureDevOpsType_CreatesAzureDevOpsCo var connector = RepoConnectorFactory.Create(config); // Assert - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that Create forwards AzureDevOpsConnectorConfig to the created connector. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsAzureDevOpsConfiguration() { // Arrange - create config with Azure DevOps settings @@ -128,18 +127,18 @@ public void RepoConnectorFactory_Create_WithAzureDevOpsConnectorConfig_ForwardsA var connector = RepoConnectorFactory.Create(config); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); var adoConnector = (AzureDevOpsRepoConnector)connector; - Assert.IsNotNull(adoConnector.ConfigurationOverrides); - Assert.AreEqual("https://dev.azure.com/myorg", adoConnector.ConfigurationOverrides.OrganizationUrl); - Assert.AreEqual("myproject", adoConnector.ConfigurationOverrides.Project); - Assert.AreEqual("myrepo", adoConnector.ConfigurationOverrides.Repository); + Assert.NotNull(adoConnector.ConfigurationOverrides); + Assert.Equal("https://dev.azure.com/myorg", adoConnector.ConfigurationOverrides.OrganizationUrl); + Assert.Equal("myproject", adoConnector.ConfigurationOverrides.Project); + Assert.Equal("myrepo", adoConnector.ConfigurationOverrides.Repository); } /// /// Test that Create returns AzureDevOpsRepoConnector when TF_BUILD environment variable is set. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithTfBuildEnv_ReturnsAzureDevOpsConnector() { // Arrange - save and set TF_BUILD, clear GitHub env vars @@ -157,7 +156,7 @@ public void RepoConnectorFactory_Create_WithTfBuildEnv_ReturnsAzureDevOpsConnect var connector = RepoConnectorFactory.Create(); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } finally { @@ -171,7 +170,7 @@ public void RepoConnectorFactory_Create_WithTfBuildEnv_ReturnsAzureDevOpsConnect /// /// Test that Create returns GitHubRepoConnector when GITHUB_ACTIONS environment variable is set. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithGitHubActionsEnv_ReturnsGitHubConnector() { // Arrange - save and set GITHUB_ACTIONS, clear TF_BUILD env var @@ -189,7 +188,7 @@ public void RepoConnectorFactory_Create_WithGitHubActionsEnv_ReturnsGitHubConnec var connector = RepoConnectorFactory.Create(); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } finally { @@ -203,7 +202,7 @@ public void RepoConnectorFactory_Create_WithGitHubActionsEnv_ReturnsGitHubConnec /// /// Test that CreateFromRemoteUrl returns AzureDevOpsRepoConnector for dev.azure.com remote URLs. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithAzureDevOpsRemoteUrl_ReturnsAzureDevOpsConnector() { // Act - supply a fake Azure DevOps remote URL directly @@ -212,13 +211,13 @@ public void RepoConnectorFactory_Create_WithAzureDevOpsRemoteUrl_ReturnsAzureDev "https://dev.azure.com/myorg/myproject/_git/myrepo"); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that CreateFromRemoteUrl returns AzureDevOpsRepoConnector for visualstudio.com remote URLs. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithVisualStudioRemoteUrl_ReturnsAzureDevOpsConnector() { // Act - supply a fake visualstudio.com remote URL directly @@ -227,13 +226,13 @@ public void RepoConnectorFactory_Create_WithVisualStudioRemoteUrl_ReturnsAzureDe "https://myorg.visualstudio.com/myproject/_git/myrepo"); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that CreateFromRemoteUrl returns GitHubRepoConnector for github.com remote URLs. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithGitHubRemoteUrl_ReturnsGitHubConnector() { // Act - supply a fake GitHub remote URL directly @@ -242,19 +241,19 @@ public void RepoConnectorFactory_Create_WithGitHubRemoteUrl_ReturnsGitHubConnect "https://github.com/example-owner/example-repo.git"); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that CreateFromRemoteUrl defaults to GitHubRepoConnector when the remote URL is null. /// - [TestMethod] + [Fact] public void RepoConnectorFactory_Create_WithNullRemoteUrl_DefaultsToGitHubConnector() { // Act - supply null to represent an unavailable git remote var connector = RepoConnectorFactory.CreateFromRemoteUrl(null, null); // Assert - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } } diff --git a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorsTests.cs b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorsTests.cs index 6d87e0d3..718680f9 100644 --- a/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorsTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/RepoConnectors/RepoConnectorsTests.cs @@ -33,7 +33,6 @@ namespace DemaConsulting.BuildMark.Tests.RepoConnectors; /// /// Subsystem tests for the RepoConnectors subsystem. /// -[TestClass] public class RepoConnectorsTests { // ───────────────────────────────────────────────────────────────────────── @@ -43,20 +42,20 @@ public class RepoConnectorsTests /// /// Test that the GitHub connector implements the IRepoConnector interface. /// - [TestMethod] + [Fact] public void RepoConnectors_GitHubConnector_ImplementsInterface_ReturnsTrue() { // Arrange: create a GitHubRepoConnector instance var connector = new GitHubRepoConnector(); // Assert: it satisfies the public IRepoConnector interface - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that the GitHub connector returns valid build information from mocked API data. /// - [TestMethod] + [Fact] public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation() { // Arrange: set up a mocked GraphQL handler with a single release and commit @@ -78,18 +77,18 @@ public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithMockedD var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert: build information is complete and accurate - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("abc123def456", buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.Changes); - Assert.IsNotNull(buildInfo.Bugs); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("abc123def456", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); } /// /// Test that the GitHub connector selects the correct previous version as baseline. /// - [TestMethod] + [Fact] public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithMultipleVersions_SelectsCorrectBaseline() { // Arrange: set up three release tags so the connector can pick v1.1.0 as baseline for v2.0.0 @@ -117,18 +116,18 @@ public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithMultipl var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert: v1.1.0 is selected as baseline and a changelog link is generated - Assert.IsNotNull(buildInfo); - Assert.AreEqual("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.IsNotNull(buildInfo.BaselineVersionTag, "Previous version should be identified"); - Assert.AreEqual("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); - Assert.IsNotNull(buildInfo.CompleteChangelogLink, "Changelog link should be generated"); + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.True(buildInfo.BaselineVersionTag != null, "Previous version should be identified"); + Assert.Equal("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.True(buildInfo.CompleteChangelogLink != null, "Changelog link should be generated"); Assert.Contains("v1.1.0...v2.0.0", buildInfo.CompleteChangelogLink.TargetUrl); } /// /// Test that the GitHub connector correctly categorizes pull requests into changes and bugs. /// - [TestMethod] + [Fact] public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithPullRequests_GathersChanges() { // Arrange: two PRs – one labelled "feature", one labelled "bug" @@ -173,18 +172,18 @@ public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithPullReq var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert: feature PR is in Changes, bug PR is in Bugs - Assert.IsNotNull(buildInfo); + Assert.NotNull(buildInfo); var featurePR = buildInfo.Changes.FirstOrDefault(c => c.Index == 101); - Assert.IsNotNull(featurePR, "Feature PR should be in Changes"); + Assert.True(featurePR != null, "Feature PR should be in Changes"); var bugPR = buildInfo.Bugs.FirstOrDefault(b => b.Index == 100); - Assert.IsNotNull(bugPR, "Bug PR should be in Bugs"); + Assert.True(bugPR != null, "Bug PR should be in Bugs"); } /// /// Test that the GitHub connector correctly identifies open issues as known issues. /// - [TestMethod] + [Fact] public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithOpenIssues_IdentifiesKnownIssues() { // Arrange: one open issue that is not resolved in this release @@ -212,17 +211,17 @@ public async Task RepoConnectors_GitHubConnector_GetBuildInformation_WithOpenIss var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert: open issue surfaces as a known issue - Assert.IsNotNull(buildInfo); - Assert.IsGreaterThan(0, buildInfo.KnownIssues.Count, "Should have at least one known issue"); + Assert.NotNull(buildInfo); + Assert.True(buildInfo.KnownIssues.Count > 0, "Should have at least one known issue"); var knownIssue = buildInfo.KnownIssues.FirstOrDefault(i => i.Index == 201); - Assert.IsNotNull(knownIssue, "Open issue 201 should appear in KnownIssues"); - Assert.AreEqual("Known bug in feature X", knownIssue.Title); + Assert.True(knownIssue != null, "Open issue 201 should appear in KnownIssues"); + Assert.Equal("Known bug in feature X", knownIssue.Title); } /// /// Test that the GitHub connector skips pre-releases when building the version baseline. /// - [TestMethod] + [Fact] public async Task RepoConnectors_GitHubConnector_GetBuildInformation_ReleaseVersion_SkipsPreReleases() { // Arrange: mix of release and pre-release tags; the connector must skip pre-releases @@ -253,11 +252,10 @@ public async Task RepoConnectors_GitHubConnector_GetBuildInformation_ReleaseVers var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert: baseline should be v1.1.0 (the last release), not v2.0.0-rc.1 (a pre-release) - Assert.IsNotNull(buildInfo); - Assert.AreEqual("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.IsNotNull(buildInfo.BaselineVersionTag, "Baseline version should be set"); - Assert.AreEqual("1.1.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion, - "Release version should skip pre-releases when selecting baseline"); + Assert.NotNull(buildInfo); + Assert.Equal("2.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.True(buildInfo.BaselineVersionTag != null, "Baseline version should be set"); + Assert.True(buildInfo.BaselineVersionTag.VersionTag.FullVersion == "1.1.0", "Release version should skip pre-releases when selecting baseline"); } // ───────────────────────────────────────────────────────────────────────── @@ -267,31 +265,31 @@ public async Task RepoConnectors_GitHubConnector_GetBuildInformation_ReleaseVers /// /// Test that MockRepoConnector satisfies the shared IRepoConnector interface. /// - [TestMethod] + [Fact] public void RepoConnectors_ConnectorBase_MockConnector_ImplementsInterface() { // Arrange: create a MockRepoConnector var connector = new MockRepoConnector(); // Assert: MockRepoConnector derives from the base class and satisfies the shared interface - Assert.IsInstanceOfType(connector); - Assert.IsInstanceOfType(connector); - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); + Assert.IsAssignableFrom(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that GitHubRepoConnector satisfies the shared IRepoConnector interface. /// - [TestMethod] + [Fact] public void RepoConnectors_ConnectorBase_GitHubConnector_ImplementsInterface() { // Arrange: create a GitHubRepoConnector var connector = new GitHubRepoConnector(); // Assert: GitHubRepoConnector derives from the base class and satisfies the shared interface - Assert.IsInstanceOfType(connector); - Assert.IsInstanceOfType(connector); - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); + Assert.IsAssignableFrom(connector); + Assert.IsAssignableFrom(connector); } // ───────────────────────────────────────────────────────────────────────── @@ -301,35 +299,35 @@ public void RepoConnectors_ConnectorBase_GitHubConnector_ImplementsInterface() /// /// Test that MockRepoConnector can be constructed. /// - [TestMethod] + [Fact] public void RepoConnectors_MockConnector_Constructor_CreatesInstance() { // Act: create a MockRepoConnector var connector = new MockRepoConnector(); // Assert: instance is created and is of the expected type - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that MockRepoConnector implements IRepoConnector. /// - [TestMethod] + [Fact] public void RepoConnectors_MockConnector_ImplementsInterface_ReturnsTrue() { // Act: create a MockRepoConnector var connector = new MockRepoConnector(); // Assert: it implements both the base class and the public interface - Assert.IsInstanceOfType(connector); - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that MockRepoConnector returns build information with the specified version. /// - [TestMethod] + [Fact] public async Task RepoConnectors_MockConnector_GetBuildInformation_ReturnsExpectedVersion() { // Arrange: create connector and request a known version @@ -340,14 +338,14 @@ public async Task RepoConnectors_MockConnector_GetBuildInformation_ReturnsExpect var buildInfo = await connector.GetBuildInformationAsync(version); // Assert: current version tag matches the requested version - Assert.IsNotNull(buildInfo); - Assert.AreEqual("v2.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); // Actual repository tag + Assert.NotNull(buildInfo); + Assert.Equal("v2.0.0", buildInfo.CurrentVersionTag.VersionTag.Tag); // Actual repository tag } /// /// Test that MockRepoConnector returns a complete BuildInformation structure. /// - [TestMethod] + [Fact] public async Task RepoConnectors_MockConnector_GetBuildInformation_ReturnsCompleteInformation() { // Arrange: create connector @@ -358,11 +356,11 @@ public async Task RepoConnectors_MockConnector_GetBuildInformation_ReturnsComple var buildInfo = await connector.GetBuildInformationAsync(version); // Assert: all required collections are present - Assert.IsNotNull(buildInfo, "BuildInformation should not be null"); - Assert.IsNotNull(buildInfo.Changes, "Changes list should not be null"); - Assert.IsNotNull(buildInfo.Bugs, "Bugs list should not be null"); - Assert.IsNotNull(buildInfo.KnownIssues, "KnownIssues list should not be null"); - Assert.IsNotNull(buildInfo.CurrentVersionTag, "CurrentVersionTag should not be null"); + Assert.True(buildInfo != null, "BuildInformation should not be null"); + Assert.True(buildInfo.Changes != null, "Changes list should not be null"); + Assert.True(buildInfo.Bugs != null, "Bugs list should not be null"); + Assert.True(buildInfo.KnownIssues != null, "KnownIssues list should not be null"); + Assert.True(buildInfo.CurrentVersionTag != null, "CurrentVersionTag should not be null"); } // ───────────────────────────────────────────────────────────────────────── @@ -372,7 +370,7 @@ public async Task RepoConnectors_MockConnector_GetBuildInformation_ReturnsComple /// /// Test that TryRunAsync returns output when the command succeeds. /// - [TestMethod] + [Fact] public async Task RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput() { // Arrange: choose a portable echo command @@ -383,28 +381,28 @@ public async Task RepoConnectors_ProcessRunner_TryRunAsync_WithValidCommand_Retu var result = await ProcessRunner.TryRunAsync(command, arguments); // Assert: output is returned and contains the expected text - Assert.IsNotNull(result, "TryRunAsync should return output for a successful command"); - Assert.IsTrue(result.Contains("test", StringComparison.OrdinalIgnoreCase), + Assert.True(result != null, "TryRunAsync should return output for a successful command"); + Assert.True(result.Contains("test", StringComparison.OrdinalIgnoreCase), "Output should contain the echoed text"); } /// /// Test that TryRunAsync returns null when the command does not exist. /// - [TestMethod] + [Fact] public async Task RepoConnectors_ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull() { // Arrange: a command that definitely does not exist var result = await ProcessRunner.TryRunAsync("nonexistent_command_12345678", ""); // Assert: null is returned - Assert.IsNull(result, "TryRunAsync should return null for a non-existent command"); + Assert.True(result == null, "TryRunAsync should return null for a non-existent command"); } /// /// Test that TryRunAsync returns null when the command exits with a non-zero code. /// - [TestMethod] + [Fact] public async Task RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull() { // Arrange: a command that exits with code 1 @@ -415,13 +413,13 @@ public async Task RepoConnectors_ProcessRunner_TryRunAsync_WithNonZeroExitCode_R var result = await ProcessRunner.TryRunAsync(command, arguments); // Assert: null is returned for a failed command - Assert.IsNull(result, "TryRunAsync should return null when the command exits with a non-zero code"); + Assert.True(result == null, "TryRunAsync should return null when the command exits with a non-zero code"); } /// /// Test that RunAsync returns output when the command succeeds. /// - [TestMethod] + [Fact] public async Task RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput() { // Arrange: a portable echo command @@ -432,15 +430,15 @@ public async Task RepoConnectors_ProcessRunner_RunAsync_WithValidCommand_Returns var result = await ProcessRunner.RunAsync(command, arguments); // Assert: output contains the expected text - Assert.IsNotNull(result, "RunAsync should return output for a successful command"); - Assert.IsTrue(result.Contains("test123", StringComparison.OrdinalIgnoreCase), + Assert.True(result != null, "RunAsync should return output for a successful command"); + Assert.True(result.Contains("test123", StringComparison.OrdinalIgnoreCase), "Output should contain the echoed text"); } /// /// Test that RunAsync throws InvalidOperationException when the command fails. /// - [TestMethod] + [Fact] public async Task RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_ThrowsException() { // Arrange: a command that exits with code 1 @@ -460,28 +458,28 @@ public async Task RepoConnectors_ProcessRunner_RunAsync_WithFailingCommand_Throw /// /// Test that the factory creates a non-null connector instance. /// - [TestMethod] + [Fact] public void RepoConnectors_Factory_Create_ReturnsConnector() { // Act: create a connector via the factory var connector = RepoConnectorFactory.Create(); // Assert: a valid IRepoConnector instance is returned - Assert.IsNotNull(connector); - Assert.IsInstanceOfType(connector); + Assert.NotNull(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that the factory returns a GitHubRepoConnector for this repository. /// - [TestMethod] + [Fact] public void RepoConnectors_Factory_Create_ReturnsGitHubConnectorForThisRepo() { // Act: create a connector via the factory var connector = RepoConnectorFactory.Create(); // Assert: the factory selects the GitHub connector for this GitHub-hosted repository - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } // ───────────────────────────────────────────────────────────────────────── @@ -491,7 +489,7 @@ public void RepoConnectors_Factory_Create_ReturnsGitHubConnectorForThisRepo() /// /// Test that the subsystem parses "public" visibility from a buildmark block. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_VisibilityPublic_ReturnsPublicVisibility() { // Arrange: description with public visibility @@ -501,14 +499,14 @@ public void RepoConnectors_ItemControls_VisibilityPublic_ReturnsPublicVisibility var result = ItemControlsParser.Parse(description); // Assert: visibility is "public" - Assert.IsNotNull(result); - Assert.AreEqual("public", result.Visibility); + Assert.NotNull(result); + Assert.Equal("public", result.Visibility); } /// /// Test that the subsystem parses "internal" visibility from a buildmark block. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_VisibilityInternal_ReturnsInternalVisibility() { // Arrange: description with internal visibility @@ -518,14 +516,14 @@ public void RepoConnectors_ItemControls_VisibilityInternal_ReturnsInternalVisibi var result = ItemControlsParser.Parse(description); // Assert: visibility is "internal" - Assert.IsNotNull(result); - Assert.AreEqual("internal", result.Visibility); + Assert.NotNull(result); + Assert.Equal("internal", result.Visibility); } /// /// Test that the subsystem parses "bug" type from a buildmark block. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_TypeBug_ReturnsBugType() { // Arrange: description with bug type @@ -535,14 +533,14 @@ public void RepoConnectors_ItemControls_TypeBug_ReturnsBugType() var result = ItemControlsParser.Parse(description); // Assert: type is "bug" - Assert.IsNotNull(result); - Assert.AreEqual("bug", result.Type); + Assert.NotNull(result); + Assert.Equal("bug", result.Type); } /// /// Test that the subsystem parses "feature" type from a buildmark block. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_TypeFeature_ReturnsFeatureType() { // Arrange: description with feature type @@ -552,14 +550,14 @@ public void RepoConnectors_ItemControls_TypeFeature_ReturnsFeatureType() var result = ItemControlsParser.Parse(description); // Assert: type is "feature" - Assert.IsNotNull(result); - Assert.AreEqual("feature", result.Type); + Assert.NotNull(result); + Assert.Equal("feature", result.Type); } /// /// Test that the subsystem parses affected-versions from a buildmark block. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_AffectedVersions_ReturnsIntervalSet() { // Arrange: description with affected-versions field @@ -569,15 +567,15 @@ public void RepoConnectors_ItemControls_AffectedVersions_ReturnsIntervalSet() var result = ItemControlsParser.Parse(description); // Assert: affected versions interval set is parsed correctly - Assert.IsNotNull(result); - Assert.IsNotNull(result.AffectedVersions); - Assert.HasCount(2, result.AffectedVersions.Intervals); + Assert.NotNull(result); + Assert.NotNull(result.AffectedVersions); + Assert.Equal(2, result.AffectedVersions.Intervals.Count); } /// /// Test that the subsystem recognizes a buildmark block hidden in an HTML comment. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_HiddenBlock_ReturnsControls() { // Arrange: buildmark block wrapped in HTML comment delimiters @@ -587,14 +585,14 @@ public void RepoConnectors_ItemControls_HiddenBlock_ReturnsControls() var result = ItemControlsParser.Parse(description); // Assert: controls are extracted despite the HTML comment wrapping - Assert.IsNotNull(result); - Assert.AreEqual("feature", result.Type); + Assert.NotNull(result); + Assert.Equal("feature", result.Type); } /// /// Test that the subsystem returns null when no buildmark block is present. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemControls_NoBlock_ReturnsNull() { // Arrange: description with no buildmark block @@ -604,7 +602,7 @@ public void RepoConnectors_ItemControls_NoBlock_ReturnsNull() var result = ItemControlsParser.Parse(description); // Assert: null is returned - Assert.IsNull(result); + Assert.Null(result); } // ───────────────────────────────────────────────────────────────────────── @@ -614,7 +612,7 @@ public void RepoConnectors_ItemControls_NoBlock_ReturnsNull() /// /// Test that the subsystem routes items to matching sections based on rules. /// - [TestMethod] + [Fact] public void RepoConnectors_ItemRouter_MatchingRule_RoutesToSection() { // Arrange: define sections and rules, then create items to route @@ -640,16 +638,16 @@ public void RepoConnectors_ItemRouter_MatchingRule_RoutesToSection() var routed = ItemRouter.Route(items, rules, sections); // Assert: each item is routed to its matching section - Assert.HasCount(1, routed["features"]); - Assert.AreEqual("1", routed["features"][0].Id); - Assert.HasCount(1, routed["bugs"]); - Assert.AreEqual("2", routed["bugs"][0].Id); + Assert.Single(routed["features"]); + Assert.Equal("1", routed["features"][0].Id); + Assert.Single(routed["bugs"]); + Assert.Equal("2", routed["bugs"][0].Id); } /// /// Test that the subsystem suppresses items when the route is "suppressed". /// - [TestMethod] + [Fact] public void RepoConnectors_ItemRouter_SuppressedRoute_OmitsItem() { // Arrange: define a section and a suppression rule @@ -672,7 +670,7 @@ public void RepoConnectors_ItemRouter_SuppressedRoute_OmitsItem() var routed = ItemRouter.Route(items, rules, sections); // Assert: the item is suppressed and does not appear in any section - Assert.IsEmpty(routed["changes"]); + Assert.Empty(routed["changes"]); } // ───────────────────────────────────────────────────────────────────────── @@ -682,20 +680,20 @@ public void RepoConnectors_ItemRouter_SuppressedRoute_OmitsItem() /// /// Test that the Azure DevOps connector implements the IRepoConnector interface. /// - [TestMethod] + [Fact] public void RepoConnectors_AzureDevOps_ImplementsInterface_ReturnsTrue() { // Arrange: create an AzureDevOpsRepoConnector instance var connector = new AzureDevOpsRepoConnector(); // Assert: it satisfies the public IRepoConnector interface - Assert.IsInstanceOfType(connector); + Assert.IsAssignableFrom(connector); } /// /// Test that the Azure DevOps connector returns valid build information from mocked API data. /// - [TestMethod] + [Fact] public async Task RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ReturnsValidBuildInformation() { // Arrange: set up a mocked REST handler with a single tag and commit @@ -712,18 +710,18 @@ public async Task RepoConnectors_AzureDevOps_GetBuildInformation_WithMockedData_ var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert: build information is complete and accurate - Assert.IsNotNull(buildInfo); - Assert.AreEqual("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); - Assert.AreEqual("abc123", buildInfo.CurrentVersionTag.CommitHash); - Assert.IsNotNull(buildInfo.Changes); - Assert.IsNotNull(buildInfo.Bugs); - Assert.IsNotNull(buildInfo.KnownIssues); + Assert.NotNull(buildInfo); + Assert.Equal("1.0.0", buildInfo.CurrentVersionTag.VersionTag.FullVersion); + Assert.Equal("abc123", buildInfo.CurrentVersionTag.CommitHash); + Assert.NotNull(buildInfo.Changes); + Assert.NotNull(buildInfo.Bugs); + Assert.NotNull(buildInfo.KnownIssues); } /// /// Test that the Azure DevOps connector gathers changes from pull requests. /// - [TestMethod] + [Fact] public async Task RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequests_GathersChanges() { // Arrange: set up two versions with a PR merged between them @@ -748,14 +746,14 @@ public async Task RepoConnectors_AzureDevOps_GetBuildInformation_WithPullRequest var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.1.0")); // Assert: changes include the work item from the PR - Assert.IsNotNull(buildInfo); - Assert.IsNotEmpty(buildInfo.Changes, "Changes should include items from merged PRs"); + Assert.NotNull(buildInfo); + Assert.NotEmpty(buildInfo.Changes); } /// /// Test that the Azure DevOps connector identifies open work items as known issues. /// - [TestMethod] + [Fact] public async Task RepoConnectors_AzureDevOps_GetBuildInformation_WithOpenWorkItems_IdentifiesKnownIssues() { // Arrange: set up a version with an open bug from WIQL query @@ -774,14 +772,14 @@ public async Task RepoConnectors_AzureDevOps_GetBuildInformation_WithOpenWorkIte var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v1.0.0")); // Assert: known issues include the open bug - Assert.IsNotNull(buildInfo); - Assert.IsNotEmpty(buildInfo.KnownIssues, "KnownIssues should include open bug work items"); + Assert.NotNull(buildInfo); + Assert.NotEmpty(buildInfo.KnownIssues); } /// /// Test that the Azure DevOps connector skips pre-release tags for release versions. /// - [TestMethod] + [Fact] public async Task RepoConnectors_AzureDevOps_GetBuildInformation_ReleaseVersion_SkipsPreReleases() { // Arrange: set up a release version with a pre-release between it and the previous release @@ -804,9 +802,9 @@ public async Task RepoConnectors_AzureDevOps_GetBuildInformation_ReleaseVersion_ var buildInfo = await connector.GetBuildInformationAsync(VersionTag.Create("v2.0.0")); // Assert: baseline should be v1.0.0, skipping the pre-release v2.0.0-rc.1 - Assert.IsNotNull(buildInfo); - Assert.IsNotNull(buildInfo.BaselineVersionTag); - Assert.AreEqual("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); + Assert.NotNull(buildInfo); + Assert.NotNull(buildInfo.BaselineVersionTag); + Assert.Equal("1.0.0", buildInfo.BaselineVersionTag.VersionTag.FullVersion); } /// diff --git a/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs b/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs index 0ece8965..c6a884d9 100644 --- a/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/SelfTest/SelfTestTests.cs @@ -27,13 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.SelfTest; /// /// Subsystem-level tests for the SelfTest subsystem. /// -[TestClass] public class SelfTestTests { /// /// Test that the SelfTest subsystem writes TRX results when --validate and --results are specified with a .trx file. /// - [TestMethod] + [Fact] public void SelfTest_Validation_WithTrxFile_WritesResults() { // Arrange: create a temporary directory and define a TRX results file path @@ -50,7 +49,7 @@ public void SelfTest_Validation_WithTrxFile_WritesResults() Validation.Run(context); // Assert: TRX file was created and contains expected content - Assert.IsTrue(File.Exists(trxFile), "TRX file should be created"); + Assert.True(File.Exists(trxFile), "TRX file should be created"); var trxContent = File.ReadAllText(trxFile); Assert.Contains("TestRun", trxContent); Assert.Contains("BuildMark Self-Validation", trxContent); @@ -68,7 +67,7 @@ public void SelfTest_Validation_WithTrxFile_WritesResults() /// /// Test that the SelfTest subsystem writes JUnit XML results when --validate and --results are specified with an .xml file. /// - [TestMethod] + [Fact] public void SelfTest_Validation_WithXmlFile_WritesResults() { // Arrange: create a temporary directory and define an XML results file path @@ -85,7 +84,7 @@ public void SelfTest_Validation_WithXmlFile_WritesResults() Validation.Run(context); // Assert: XML file was created and contains expected content - Assert.IsTrue(File.Exists(xmlFile), "XML file should be created"); + Assert.True(File.Exists(xmlFile), "XML file should be created"); var xmlContent = File.ReadAllText(xmlFile); Assert.Contains("testsuites", xmlContent); Assert.Contains("BuildMark Self-Validation", xmlContent); @@ -103,7 +102,7 @@ public void SelfTest_Validation_WithXmlFile_WritesResults() /// /// Test that the SelfTest subsystem creates a TRX results output file. /// - [TestMethod] + [Fact] public void SelfTest_ResultsOutput_WithTrxFile_CreatesFile() { // Arrange: create a temporary directory and define a TRX results file path @@ -120,9 +119,9 @@ public void SelfTest_ResultsOutput_WithTrxFile_CreatesFile() Validation.Run(context); // Assert: results file exists and has non-zero content - Assert.IsTrue(File.Exists(trxFile), "TRX results file should be created"); + Assert.True(File.Exists(trxFile), "TRX results file should be created"); var fileInfo = new FileInfo(trxFile); - Assert.IsGreaterThan(0, fileInfo.Length, "TRX results file should have content"); + Assert.True(fileInfo.Length > 0, "TRX results file should have content"); } finally { @@ -137,7 +136,7 @@ public void SelfTest_ResultsOutput_WithTrxFile_CreatesFile() /// /// Test that the SelfTest subsystem creates a JUnit XML results output file. /// - [TestMethod] + [Fact] public void SelfTest_ResultsOutput_WithXmlFile_CreatesFile() { // Arrange: create a temporary directory and define an XML results file path @@ -154,9 +153,9 @@ public void SelfTest_ResultsOutput_WithXmlFile_CreatesFile() Validation.Run(context); // Assert: results file exists and has non-zero content - Assert.IsTrue(File.Exists(xmlFile), "XML results file should be created"); + Assert.True(File.Exists(xmlFile), "XML results file should be created"); var fileInfo = new FileInfo(xmlFile); - Assert.IsGreaterThan(0, fileInfo.Length, "XML results file should have content"); + Assert.True(fileInfo.Length > 0, "XML results file should have content"); } finally { @@ -171,7 +170,7 @@ public void SelfTest_ResultsOutput_WithXmlFile_CreatesFile() /// /// Test that the SelfTest subsystem completes self-validation without error when no --results file is specified. /// - [TestMethod] + [Fact] public void SelfTest_Qualification_WithoutResultsFile_Succeeds() { // Arrange: create a temporary directory and define a log file path (no results file) @@ -191,7 +190,7 @@ public void SelfTest_Qualification_WithoutResultsFile_Succeeds() } // Assert: validation ran and produced log output; no results file was created - Assert.IsTrue(File.Exists(logFile), "Log file should be created"); + Assert.True(File.Exists(logFile), "Log file should be created"); var logContent = File.ReadAllText(logFile); Assert.Contains("BuildMark Self-validation", logContent); Assert.Contains("Total Tests:", logContent); diff --git a/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs b/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs index 6f8ef29e..c3b17700 100644 --- a/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/SelfTest/ValidationTests.cs @@ -27,14 +27,12 @@ namespace DemaConsulting.BuildMark.Tests.SelfTest; /// /// Tests for the Validation class. /// -[TestClass] -[DoNotParallelize] public class ValidationTests { /// /// Test that Validation.Run writes TRX results file when specified. /// - [TestMethod] + [Fact] public void Validation_Run_WithTrxResultsFile_WritesTrxFile() { // Arrange @@ -62,7 +60,7 @@ public void Validation_Run_WithTrxResultsFile_WritesTrxFile() Validation.Run(context); // Assert - Verify TRX file was created - Assert.IsTrue(File.Exists(trxFile), "TRX file should be created"); + Assert.True(File.Exists(trxFile), "TRX file should be created"); // Verify TRX file contains expected content var trxContent = File.ReadAllText(trxFile); @@ -89,7 +87,7 @@ public void Validation_Run_WithTrxResultsFile_WritesTrxFile() /// /// Test that Validation.Run writes JUnit XML results file when specified. /// - [TestMethod] + [Fact] public void Validation_Run_WithXmlResultsFile_WritesJUnitFile() { // Arrange @@ -117,7 +115,7 @@ public void Validation_Run_WithXmlResultsFile_WritesJUnitFile() Validation.Run(context); // Assert - Verify XML file was created - Assert.IsTrue(File.Exists(xmlFile), "XML file should be created"); + Assert.True(File.Exists(xmlFile), "XML file should be created"); // Verify XML file contains expected content var xmlContent = File.ReadAllText(xmlFile); @@ -144,7 +142,7 @@ public void Validation_Run_WithXmlResultsFile_WritesJUnitFile() /// /// Test that Validation.Run handles unsupported results file extension. /// - [TestMethod] + [Fact] public void Validation_Run_WithUnsupportedResultsFileExtension_ShowsError() { // Arrange @@ -173,7 +171,7 @@ public void Validation_Run_WithUnsupportedResultsFileExtension_ShowsError() Assert.Contains("Unsupported results file format", output); // Assert - Verify exit code is 1 when an error is reported - Assert.AreEqual(1, context.ExitCode, "ExitCode should be 1 when unsupported results format is used"); + Assert.True(context.ExitCode == 1); } finally { @@ -194,7 +192,7 @@ public void Validation_Run_WithUnsupportedResultsFileExtension_ShowsError() /// /// Test that Validation.Run handles write failure for results file. /// - [TestMethod] + [Fact] public void Validation_Run_WithInvalidResultsFilePath_ShowsError() { // Arrange @@ -218,7 +216,7 @@ public void Validation_Run_WithInvalidResultsFilePath_ShowsError() Assert.Contains("Failed to write results file", output); // Assert - Verify exit code is 1 when a write error is reported - Assert.AreEqual(1, context.ExitCode, "ExitCode should be 1 when results file write fails"); + Assert.True(context.ExitCode == 1); } finally { @@ -230,7 +228,7 @@ public void Validation_Run_WithInvalidResultsFilePath_ShowsError() /// /// Test that Validation.Run completes successfully when no results file is specified. /// - [TestMethod] + [Fact] public void Validation_Run_WithoutResultsFile_CompletesSuccessfully() { // Arrange - no --results argument @@ -241,6 +239,6 @@ public void Validation_Run_WithoutResultsFile_CompletesSuccessfully() Validation.Run(context); // Assert - validation should complete without error - Assert.AreEqual(0, context.ExitCode, "Validation should succeed with exit code 0"); + Assert.True(context.ExitCode == 0, "Validation should succeed with exit code 0"); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Utilities/PathHelpersTests.cs b/test/DemaConsulting.BuildMark.Tests/Utilities/PathHelpersTests.cs index 6f3b1ace..993be11b 100644 --- a/test/DemaConsulting.BuildMark.Tests/Utilities/PathHelpersTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Utilities/PathHelpersTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.BuildMark.Tests.Utilities; /// /// Tests for the PathHelpers class. /// -[TestClass] public class PathHelpersTests { /// /// Test that SafePathCombine correctly combines valid paths. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_ValidPaths_CombinesCorrectly() { // Arrange @@ -42,37 +41,37 @@ 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 ArgumentNullException for null basePath. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_NullBasePath_ThrowsArgumentNullException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => + var exception = Assert.Throws(() => PathHelpers.SafePathCombine(null!, "subfolder/file.txt")); - Assert.AreEqual("basePath", exception.ParamName); + Assert.Equal("basePath", exception.ParamName); } /// /// Test that SafePathCombine throws ArgumentNullException for null relativePath. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_NullRelativePath_ThrowsArgumentNullException() { // Act & Assert - var exception = Assert.ThrowsExactly(() => + var exception = Assert.Throws(() => PathHelpers.SafePathCombine("/home/user/project", null!)); - Assert.AreEqual("relativePath", exception.ParamName); + Assert.Equal("relativePath", exception.ParamName); } /// /// Test that SafePathCombine throws ArgumentException for path traversal with double dots. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_PathTraversalWithDoubleDots_ThrowsArgumentException() { // Arrange @@ -80,7 +79,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); } @@ -88,7 +87,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 @@ -96,7 +95,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); } @@ -104,13 +103,13 @@ public void PathHelpers_SafePathCombine_DoubleDotsInMiddle_ThrowsArgumentExcepti /// /// Test that SafePathCombine throws ArgumentException for absolute paths. /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException() { // 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.Contains("Invalid path component", unixException.Message); @@ -119,7 +118,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); } @@ -128,7 +127,7 @@ public void PathHelpers_SafePathCombine_AbsolutePath_ThrowsArgumentException() /// /// Test that SafePathCombine correctly combines valid paths whose names start with dots (e.g. "..data"). /// - [TestMethod] + [Fact] public void PathHelpers_SafePathCombine_PathStartingWithDots_CombinesCorrectly() { // Arrange - "..data" is a valid directory name and must not be rejected as traversal @@ -139,7 +138,7 @@ public void PathHelpers_SafePathCombine_PathStartingWithDots_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.BuildMark.Tests/Utilities/ProcessRunnerTests.cs b/test/DemaConsulting.BuildMark.Tests/Utilities/ProcessRunnerTests.cs index c95d534d..d43be452 100644 --- a/test/DemaConsulting.BuildMark.Tests/Utilities/ProcessRunnerTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Utilities/ProcessRunnerTests.cs @@ -26,7 +26,6 @@ namespace DemaConsulting.BuildMark.Tests.Utilities; /// /// Tests for the ProcessRunner class. /// -[TestClass] public class ProcessRunnerTests { /// @@ -36,7 +35,7 @@ public class ProcessRunnerTests /// What is being tested: ProcessRunner.TryRunAsync with a valid command /// What the assertions prove: The method returns the command output when successful /// - [TestMethod] + [Fact] public async Task ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput() { // Arrange - Set up a simple echo command that will succeed @@ -49,8 +48,8 @@ public async Task ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput() var result = await ProcessRunner.TryRunAsync(command, arguments); // Assert - Verify output is returned and contains expected text - Assert.IsNotNull(result, "TryRunAsync should return output for successful command"); - Assert.IsTrue(result.Contains("test", StringComparison.OrdinalIgnoreCase), + Assert.True(result != null, "TryRunAsync should return output for successful command"); + Assert.True(result.Contains("test", StringComparison.OrdinalIgnoreCase), "Output should contain the echoed text"); } @@ -61,7 +60,7 @@ public async Task ProcessRunner_TryRunAsync_WithValidCommand_ReturnsOutput() /// What is being tested: ProcessRunner.TryRunAsync with an invalid command /// What the assertions prove: The method returns null when the command cannot be found /// - [TestMethod] + [Fact] public async Task ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull() { // Arrange - Use a command that definitely doesn't exist @@ -71,7 +70,7 @@ public async Task ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull() var result = await ProcessRunner.TryRunAsync(command); // Assert - Verify null is returned for invalid command - Assert.IsNull(result, "TryRunAsync should return null for non-existent command"); + Assert.True(result == null, "TryRunAsync should return null for non-existent command"); } /// @@ -81,7 +80,7 @@ public async Task ProcessRunner_TryRunAsync_WithInvalidCommand_ReturnsNull() /// What is being tested: ProcessRunner.TryRunAsync with a command that fails /// What the assertions prove: The method returns null when exit code is non-zero /// - [TestMethod] + [Fact] public async Task ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull() { // Arrange - Set up a command that will fail with non-zero exit code @@ -96,7 +95,7 @@ public async Task ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull() var result = await ProcessRunner.TryRunAsync(command, arguments); // Assert - Verify null is returned for failed command - Assert.IsNull(result, "TryRunAsync should return null when command exits with non-zero code"); + Assert.True(result == null, "TryRunAsync should return null when command exits with non-zero code"); } /// @@ -106,7 +105,7 @@ public async Task ProcessRunner_TryRunAsync_WithNonZeroExitCode_ReturnsNull() /// What is being tested: ProcessRunner.TryRunAsync exception handling /// What the assertions prove: The method catches exceptions and returns null instead of throwing /// - [TestMethod] + [Fact] public async Task ProcessRunner_TryRunAsync_WithException_ReturnsNull() { // Arrange - Use a command with malformed arguments that may cause issues @@ -117,7 +116,7 @@ public async Task ProcessRunner_TryRunAsync_WithException_ReturnsNull() var result = await ProcessRunner.TryRunAsync(command); // Assert - Verify null is returned when exception occurs - Assert.IsNull(result, "TryRunAsync should return null when exception occurs"); + Assert.True(result == null, "TryRunAsync should return null when exception occurs"); } /// @@ -127,7 +126,7 @@ public async Task ProcessRunner_TryRunAsync_WithException_ReturnsNull() /// What is being tested: ProcessRunner.RunAsync with a valid command /// What the assertions prove: The method returns the command output when successful /// - [TestMethod] + [Fact] public async Task ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput() { // Arrange - Set up a simple echo command that will succeed @@ -140,8 +139,8 @@ public async Task ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput() var result = await ProcessRunner.RunAsync(command, arguments); // Assert - Verify output is returned and contains expected text - Assert.IsNotNull(result, "RunAsync should return output for successful command"); - Assert.IsTrue(result.Contains("test123", StringComparison.OrdinalIgnoreCase), + Assert.True(result != null, "RunAsync should return output for successful command"); + Assert.True(result.Contains("test123", StringComparison.OrdinalIgnoreCase), "Output should contain the echoed text"); } @@ -152,7 +151,7 @@ public async Task ProcessRunner_RunAsync_WithValidCommand_ReturnsOutput() /// What is being tested: ProcessRunner.RunAsync error handling /// What the assertions prove: The method throws InvalidOperationException with details when command fails /// - [TestMethod] + [Fact] public async Task ProcessRunner_RunAsync_WithFailingCommand_ThrowsException() { // Arrange - Set up a command that will fail @@ -177,7 +176,7 @@ public async Task ProcessRunner_RunAsync_WithFailingCommand_ThrowsException() /// What the assertions prove: The method wraps the underlying error in an InvalidOperationException /// with a message identifying the missing command /// - [TestMethod] + [Fact] public async Task ProcessRunner_RunAsync_WithNonexistentCommand_ThrowsDescriptiveException() { // Arrange - Use a command that definitely doesn't exist @@ -188,7 +187,7 @@ public async Task ProcessRunner_RunAsync_WithNonexistentCommand_ThrowsDescriptiv async () => await ProcessRunner.RunAsync(command)); // Assert - Verify exception message identifies the missing command - Assert.IsTrue( + Assert.True( exception.Message.Contains(command, StringComparison.Ordinal), "Exception message should identify the command that was not found"); } diff --git a/test/DemaConsulting.BuildMark.Tests/Utilities/UtilitiesTests.cs b/test/DemaConsulting.BuildMark.Tests/Utilities/UtilitiesTests.cs index a758d2fc..286376a1 100644 --- a/test/DemaConsulting.BuildMark.Tests/Utilities/UtilitiesTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Utilities/UtilitiesTests.cs @@ -26,7 +26,6 @@ namespace DemaConsulting.BuildMark.Tests.Utilities; /// /// Subsystem-level tests for the Utilities subsystem. /// -[TestClass] public class UtilitiesTests { // ───────────────────────────────────────────────────────────────────────── @@ -36,7 +35,7 @@ public class UtilitiesTests /// /// Test that the Utilities subsystem correctly combines valid relative paths. /// - [TestMethod] + [Fact] public void Utilities_SafePaths_ValidPaths_CombinesCorrectly() { // Arrange: define a base path and relative path @@ -47,27 +46,27 @@ public void Utilities_SafePaths_ValidPaths_CombinesCorrectly() var result = PathHelpers.SafePathCombine(basePath, relativePath); // Assert: combined path ends with the relative portion - Assert.IsTrue(result.EndsWith("subdir", StringComparison.Ordinal)); + Assert.EndsWith("subdir", result); } /// /// Test that the Utilities subsystem rejects path traversal sequences. /// - [TestMethod] + [Fact] public void Utilities_SafePaths_TraversalPath_ThrowsException() { // Arrange: define a base path and a traversal relative path var basePath = Path.Combine(Path.GetTempPath(), "safe"); // Act & Assert: path traversal is rejected - Assert.ThrowsExactly( + Assert.Throws( () => PathHelpers.SafePathCombine(basePath, "../../etc/passwd")); } /// /// Test that the Utilities subsystem rejects absolute paths as relative components. /// - [TestMethod] + [Fact] public void Utilities_SafePaths_AbsolutePath_ThrowsException() { // Arrange: define a base path and an absolute relative path @@ -77,7 +76,7 @@ public void Utilities_SafePaths_AbsolutePath_ThrowsException() : "/etc/passwd"; // Act & Assert: absolute path is rejected - Assert.ThrowsExactly( + Assert.Throws( () => PathHelpers.SafePathCombine(basePath, absolutePath)); } @@ -88,7 +87,7 @@ public void Utilities_SafePaths_AbsolutePath_ThrowsException() /// /// Test that the Utilities subsystem runs a valid command and returns output. /// - [TestMethod] + [Fact] public async Task Utilities_ProcessRunner_ValidCommand_ReturnsOutput() { // Arrange: choose a portable echo command @@ -101,27 +100,27 @@ public async Task Utilities_ProcessRunner_ValidCommand_ReturnsOutput() var result = await ProcessRunner.RunAsync(command, arguments); // Assert: output contains the expected text - Assert.IsNotNull(result); - Assert.IsTrue(result.Contains("subsystem_test", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(result); + Assert.True(result.Contains("subsystem_test", StringComparison.OrdinalIgnoreCase)); } /// /// Test that the Utilities subsystem returns null for an invalid command. /// - [TestMethod] + [Fact] public async Task Utilities_ProcessRunner_InvalidCommand_ReturnsNull() { // Arrange & Act: run a non-existent command var result = await ProcessRunner.TryRunAsync("nonexistent_utility_test_12345"); // Assert: null is returned - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that the Utilities subsystem throws an exception for a failing command. /// - [TestMethod] + [Fact] public async Task Utilities_ProcessRunner_FailingCommand_ThrowsException() { // Arrange: a command that exits with code 1 diff --git a/test/DemaConsulting.BuildMark.Tests/Version/VersionComparableTests.cs b/test/DemaConsulting.BuildMark.Tests/Version/VersionComparableTests.cs index 6c1bec33..f236fa00 100644 --- a/test/DemaConsulting.BuildMark.Tests/Version/VersionComparableTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Version/VersionComparableTests.cs @@ -25,7 +25,6 @@ namespace DemaConsulting.BuildMark.Tests.Version; /// /// Tests for the VersionComparable class. /// -[TestClass] public class VersionComparableTests { private static readonly string[] SemanticVersionOrder = @@ -50,101 +49,101 @@ public class VersionComparableTests /// /// Test that VersionComparable creates instances from valid version strings. /// - [TestMethod] + [Fact] public void VersionComparable_Create_ValidVersion_ReturnsInstance() { // Arrange & Act var version = VersionComparable.Create("1.2.3"); // Assert - Assert.IsNotNull(version); - Assert.AreEqual(1, version.Major); - Assert.AreEqual(2, version.Minor); - Assert.AreEqual(3, version.Patch); + Assert.NotNull(version); + Assert.Equal(1, version.Major); + Assert.Equal(2, version.Minor); + Assert.Equal(3, version.Patch); } /// /// Test that VersionComparable parses simple version. /// - [TestMethod] + [Fact] public void VersionComparable_Create_SimpleVersion_ParsesVersion() { // Arrange & Act var version = VersionComparable.Create("1.2.3"); // Assert - Assert.AreEqual(1, version.Major); - Assert.AreEqual(2, version.Minor); - Assert.AreEqual(3, version.Patch); - Assert.AreEqual("1.2.3", version.Numbers); - Assert.IsNull(version.PreRelease); - Assert.AreEqual("1.2.3", version.CompareVersion); - Assert.IsFalse(version.IsPreRelease); + Assert.Equal(1, version.Major); + Assert.Equal(2, version.Minor); + Assert.Equal(3, version.Patch); + Assert.Equal("1.2.3", version.Numbers); + Assert.Null(version.PreRelease); + Assert.Equal("1.2.3", version.CompareVersion); + Assert.False(version.IsPreRelease); } /// /// Test that VersionComparable parses pre-release version. /// - [TestMethod] + [Fact] public void VersionComparable_Create_PreReleaseVersion_ParsesVersion() { // Arrange & Act var version = VersionComparable.Create("2.0.0-alpha.1"); // Assert - Assert.AreEqual("2.0.0", version.Numbers); - Assert.AreEqual("alpha.1", version.PreRelease); - Assert.AreEqual("2.0.0-alpha.1", version.CompareVersion); - Assert.IsTrue(version.IsPreRelease); + Assert.Equal("2.0.0", version.Numbers); + Assert.Equal("alpha.1", version.PreRelease); + Assert.Equal("2.0.0-alpha.1", version.CompareVersion); + Assert.True(version.IsPreRelease); } /// /// Test that TryCreate returns null for invalid version. /// - [TestMethod] + [Fact] public void VersionComparable_TryCreate_InvalidVersion_ReturnsNull() { // Act var version = VersionComparable.TryCreate("not-a-version"); // Assert - Assert.IsNull(version); + Assert.Null(version); } /// /// Test that TryCreate returns null for null input. /// - [TestMethod] + [Fact] public void VersionComparable_TryCreate_NullInput_ReturnsNull() { // Arrange & Act var version = VersionComparable.TryCreate(null!); // Assert - Assert.IsNull(version); + Assert.Null(version); } /// /// Test that TryCreate returns null for empty input. /// - [TestMethod] + [Fact] public void VersionComparable_TryCreate_EmptyInput_ReturnsNull() { // Arrange & Act var version = VersionComparable.TryCreate(string.Empty); // Assert - Assert.IsNull(version); + Assert.Null(version); } /// /// Test that Create throws ArgumentException for invalid version. /// - [TestMethod] + [Fact] public void VersionComparable_Create_InvalidVersion_ThrowsArgumentException() { // Act - var exception = Assert.ThrowsExactly(() => VersionComparable.Create("not-a-version")); + var exception = Assert.Throws(() => VersionComparable.Create("not-a-version")); // Assert Assert.Contains("does not match comparable version format", exception.Message); @@ -153,7 +152,7 @@ public void VersionComparable_Create_InvalidVersion_ThrowsArgumentException() /// /// Test that CompareTo works correctly with identical versions. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_SameMajorMinorPatch_ReturnsZero() { // Arrange @@ -164,13 +163,13 @@ public void VersionComparable_CompareTo_SameMajorMinorPatch_ReturnsZero() var result = version1.CompareTo(version2); // Assert - Assert.AreEqual(0, result); + Assert.Equal(0, result); } /// /// Test that CompareTo handles different major versions correctly. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_DifferentMajor_ReturnsCorrectOrder() { // Arrange @@ -181,13 +180,13 @@ public void VersionComparable_CompareTo_DifferentMajor_ReturnsCorrectOrder() var result = version1.CompareTo(version2); // Assert - Assert.IsLessThan(0, result, "1.2.3 should be less than 2.1.1"); + Assert.True(result < 0, "1.2.3 should be less than 2.1.1"); } /// /// Test that CompareTo handles different minor versions correctly. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_DifferentMinor_ReturnsCorrectOrder() { // Arrange @@ -198,13 +197,13 @@ public void VersionComparable_CompareTo_DifferentMinor_ReturnsCorrectOrder() var result = version1.CompareTo(version2); // Assert - Assert.IsLessThan(0, result, "1.2.3 should be less than 1.3.1"); + Assert.True(result < 0, "1.2.3 should be less than 1.3.1"); } /// /// Test that CompareTo handles different patch versions correctly. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_DifferentPatch_ReturnsCorrectOrder() { // Arrange @@ -215,13 +214,13 @@ public void VersionComparable_CompareTo_DifferentPatch_ReturnsCorrectOrder() var result = version1.CompareTo(version2); // Assert - Assert.IsLessThan(0, result, "1.2.3 should be less than 1.2.4"); + Assert.True(result < 0, "1.2.3 should be less than 1.2.4"); } /// /// Test that CompareTo treats pre-release vs release correctly. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_PreReleaseVsRelease_ReturnsCorrectOrder() { // Arrange @@ -232,13 +231,13 @@ public void VersionComparable_CompareTo_PreReleaseVsRelease_ReturnsCorrectOrder( var result = preRelease.CompareTo(release); // Assert - Assert.IsLessThan(0, result, "1.2.3-alpha should be less than 1.2.3"); + Assert.True(result < 0, "1.2.3-alpha should be less than 1.2.3"); } /// /// Test that CompareTo orders pre-releases lexicographically. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_PreReleaseVersions_ReturnsLexicographicOrder() { // Arrange @@ -249,13 +248,13 @@ public void VersionComparable_CompareTo_PreReleaseVersions_ReturnsLexicographicO var result = version1.CompareTo(version2); // Assert - Assert.IsLessThan(0, result, "alpha should be less than beta lexicographically"); + Assert.True(result < 0, "alpha should be less than beta lexicographically"); } /// /// Test that less-than operator works correctly. /// - [TestMethod] + [Fact] public void VersionComparable_Operators_LessThan_WorksCorrectly() { // Arrange @@ -263,14 +262,14 @@ public void VersionComparable_Operators_LessThan_WorksCorrectly() var version2 = VersionComparable.Create("1.11.2"); // Act & Assert - Assert.IsTrue(version1 < version2); - Assert.IsFalse(version2 < version1); + Assert.True(version1 < version2); + Assert.False(version2 < version1); } /// /// Test that greater-than operator works correctly. /// - [TestMethod] + [Fact] public void VersionComparable_Operators_GreaterThan_WorksCorrectly() { // Arrange @@ -278,14 +277,14 @@ public void VersionComparable_Operators_GreaterThan_WorksCorrectly() var version2 = VersionComparable.Create("1.2.3"); // Act & Assert - Assert.IsTrue(version1 > version2); - Assert.IsFalse(version2 > version1); + Assert.True(version1 > version2); + Assert.False(version2 > version1); } /// /// Test that less-than-or-equal operator works correctly. /// - [TestMethod] + [Fact] public void VersionComparable_Operators_LessThanOrEqual_WorksCorrectly() { // Arrange @@ -294,15 +293,15 @@ public void VersionComparable_Operators_LessThanOrEqual_WorksCorrectly() var version3 = VersionComparable.Create("1.11.2"); // Act & Assert - Assert.IsTrue(version1 <= version2); // Equal case - Assert.IsTrue(version1 <= version3); // Less than case - Assert.IsFalse(version3 <= version1); // False case + Assert.True(version1 <= version2); // Equal case + Assert.True(version1 <= version3); // Less than case + Assert.False(version3 <= version1); // False case } /// /// Test that greater-than-or-equal operator works correctly. /// - [TestMethod] + [Fact] public void VersionComparable_Operators_GreaterThanOrEqual_WorksCorrectly() { // Arrange @@ -311,15 +310,15 @@ public void VersionComparable_Operators_GreaterThanOrEqual_WorksCorrectly() var version3 = VersionComparable.Create("1.11.2"); // Act & Assert - Assert.IsTrue(version1 >= version2); // Equal case - Assert.IsTrue(version3 >= version1); // Greater than case - Assert.IsFalse(version1 >= version3); // False case + Assert.True(version1 >= version2); // Equal case + Assert.True(version3 >= version1); // Greater than case + Assert.False(version1 >= version3); // False case } /// /// Test that CompareTo orders semantic versions correctly. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_SemanticVersions_ReturnsCorrectOrder() { // Arrange - Test various semantic version scenarios @@ -332,14 +331,14 @@ public void VersionComparable_CompareTo_SemanticVersions_ReturnsCorrectOrder() var next = versions[i + 1]; var result = current!.CompareTo(next); - Assert.IsLessThan(0, result, $"{current.CompareVersion} should be less than {next!.CompareVersion}"); + Assert.True(result < 0, $"{current.CompareVersion} should be less than {next!.CompareVersion}"); } } /// /// Test that CompareTo handles numeric comparison correctly (e.g., 1.2.3 < 1.11.2). /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_NumericComparison_CorrectOrdering() { // Arrange @@ -350,13 +349,13 @@ public void VersionComparable_CompareTo_NumericComparison_CorrectOrdering() var result = version1.CompareTo(version2); // Assert - Assert.IsLessThan(0, result, "1.2.3 should be less than 1.11.2"); + Assert.True(result < 0, "1.2.3 should be less than 1.11.2"); } /// /// Test that CompareTo treats non-pre-release as greater than pre-release with same version. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_ReleaseGreaterThanPreRelease_CorrectOrdering() { // Arrange @@ -367,13 +366,13 @@ public void VersionComparable_CompareTo_ReleaseGreaterThanPreRelease_CorrectOrde var result = version1.CompareTo(version2); // Assert - Assert.IsGreaterThan(0, result, "1.2.3 should be greater than 1.2.3-beta.1"); + Assert.True(result > 0, "1.2.3 should be greater than 1.2.3-beta.1"); } /// /// Test that CompareTo orders pre-releases lexicographically. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_PreReleaseLexicographic_CorrectOrdering() { // Arrange @@ -384,14 +383,14 @@ public void VersionComparable_CompareTo_PreReleaseLexicographic_CorrectOrdering( var result = version1.CompareTo(version2); // Assert - Assert.IsLessThan(0, result, "alpha should be less than beta lexicographically"); + Assert.True(result < 0, "alpha should be less than beta lexicographically"); } /// /// Test that numeric pre-release identifiers are compared numerically, not lexicographically. /// This tests the core SemVer requirement that "1.0.0-alpha.5" < "1.0.0-alpha.10". /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_PreReleaseNumeric_ComparesNumerically() { // Arrange - This was the original failing case @@ -402,13 +401,13 @@ public void VersionComparable_CompareTo_PreReleaseNumeric_ComparesNumerically() var result = version5!.CompareTo(version10); // Assert - Assert.IsLessThan(0, result, "1.0.0-alpha.5 should be less than 1.0.0-alpha.10 (numeric comparison)"); + Assert.True(result < 0, "1.0.0-alpha.5 should be less than 1.0.0-alpha.10 (numeric comparison)"); } /// /// Test comprehensive SemVer pre-release comparison rules. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_PreReleaseSemVerRules_CorrectOrdering() { // Test cases from SemVer specification @@ -421,14 +420,14 @@ public void VersionComparable_CompareTo_PreReleaseSemVerRules_CorrectOrdering() var next = versions[i + 1]; var result = current!.CompareTo(next); - Assert.IsLessThan(0, result, $"{current.CompareVersion} should be less than {next!.CompareVersion}"); + Assert.True(result < 0, $"{current.CompareVersion} should be less than {next!.CompareVersion}"); } } /// /// Test that numeric identifiers are always less than non-numeric identifiers. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_NumericVsNonNumeric_NumericIsLess() { // Arrange @@ -439,13 +438,13 @@ public void VersionComparable_CompareTo_NumericVsNonNumeric_NumericIsLess() var result = numericVersion!.CompareTo(nonNumericVersion); // Assert - Assert.IsLessThan(0, result, "1.0.0-1 should be less than 1.0.0-alpha (numeric < non-numeric)"); + Assert.True(result < 0, "1.0.0-1 should be less than 1.0.0-alpha (numeric < non-numeric)"); } /// /// Test that shorter pre-release identifiers are considered less when all compared parts are equal. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_ShorterPreRelease_IsLess() { // Arrange @@ -456,13 +455,13 @@ public void VersionComparable_CompareTo_ShorterPreRelease_IsLess() var result = shorterVersion!.CompareTo(longerVersion); // Assert - Assert.IsLessThan(0, result, "1.0.0-alpha should be less than 1.0.0-alpha.1 (shorter is less)"); + Assert.True(result < 0, "1.0.0-alpha should be less than 1.0.0-alpha.1 (shorter is less)"); } /// /// Test complex multi-segment numeric and non-numeric pre-release comparison. /// - [TestMethod] + [Fact] public void VersionComparable_CompareTo_ComplexPreRelease_CorrectOrdering() { // Arrange - Test various complex scenarios @@ -472,9 +471,9 @@ public void VersionComparable_CompareTo_ComplexPreRelease_CorrectOrdering() var test4 = VersionComparable.Create("1.0.0-alpha.beta.1"); // Act & Assert - Assert.IsLessThan(0, test1!.CompareTo(test2), "alpha.1.2 < alpha.1.10 (numeric segment comparison)"); - Assert.IsLessThan(0, test2!.CompareTo(test3), "alpha.1.10 < alpha.2.1 (first numeric difference wins)"); - Assert.IsLessThan(0, test3!.CompareTo(test4), "alpha.2.1 < alpha.beta.1 (numeric < non-numeric)"); + Assert.True(test1!.CompareTo(test2) < 0, "alpha.1.2 < alpha.1.10 (numeric segment comparison)"); + Assert.True(test2!.CompareTo(test3) < 0, "alpha.1.10 < alpha.2.1 (first numeric difference wins)"); + Assert.True(test3!.CompareTo(test4) < 0, "alpha.2.1 < alpha.beta.1 (numeric < non-numeric)"); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalSetTests.cs b/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalSetTests.cs index 382c6e4f..45734865 100644 --- a/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalSetTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalSetTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.BuildMark.Tests.Version; /// /// Tests for VersionIntervalSet.Parse method. /// -[TestClass] public class VersionIntervalSetTests { /// /// Test that Parse returns one interval for a single interval token. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Parse_SingleInterval_ReturnsOneInterval() { // Arrange @@ -41,16 +40,16 @@ public void VersionIntervalSet_Parse_SingleInterval_ReturnsOneInterval() var result = VersionIntervalSet.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.HasCount(1, result.Intervals); - Assert.AreEqual("1.0.0", result.Intervals[0].LowerBound); - Assert.AreEqual("2.0.0", result.Intervals[0].UpperBound); + Assert.NotNull(result); + Assert.Single(result.Intervals); + Assert.Equal("1.0.0", result.Intervals[0].LowerBound); + Assert.Equal("2.0.0", result.Intervals[0].UpperBound); } /// /// Test that Parse returns two intervals for two interval tokens separated by comma. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Parse_TwoIntervals_ReturnsTwoIntervals() { // Arrange @@ -60,20 +59,20 @@ public void VersionIntervalSet_Parse_TwoIntervals_ReturnsTwoIntervals() var result = VersionIntervalSet.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.HasCount(2, result.Intervals); - Assert.IsNull(result.Intervals[0].LowerBound); - Assert.AreEqual("1.0.1", result.Intervals[0].UpperBound); - Assert.IsTrue(result.Intervals[0].UpperInclusive); - Assert.AreEqual("1.1.0", result.Intervals[1].LowerBound); - Assert.AreEqual("1.2.0", result.Intervals[1].UpperBound); - Assert.IsFalse(result.Intervals[1].UpperInclusive); + Assert.NotNull(result); + Assert.Equal(2, result.Intervals.Count); + Assert.Null(result.Intervals[0].LowerBound); + Assert.Equal("1.0.1", result.Intervals[0].UpperBound); + Assert.True(result.Intervals[0].UpperInclusive); + Assert.Equal("1.1.0", result.Intervals[1].LowerBound); + Assert.Equal("1.2.0", result.Intervals[1].UpperBound); + Assert.False(result.Intervals[1].UpperInclusive); } /// /// Test that a comma inside an interval is treated as a bound separator, not an interval separator. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Parse_IntervalsWithInternalComma_ParsedCorrectly() { // Arrange - two intervals each containing an internal comma between bounds @@ -83,18 +82,18 @@ public void VersionIntervalSet_Parse_IntervalsWithInternalComma_ParsedCorrectly( var result = VersionIntervalSet.Parse(text); // Assert - the internal commas must not split the intervals; two intervals expected - Assert.IsNotNull(result); - Assert.HasCount(2, result.Intervals); - Assert.AreEqual("1.0.0", result.Intervals[0].LowerBound); - Assert.AreEqual("2.0.0", result.Intervals[0].UpperBound); - Assert.AreEqual("3.0.0", result.Intervals[1].LowerBound); - Assert.AreEqual("4.0.0", result.Intervals[1].UpperBound); + Assert.NotNull(result); + Assert.Equal(2, result.Intervals.Count); + Assert.Equal("1.0.0", result.Intervals[0].LowerBound); + Assert.Equal("2.0.0", result.Intervals[0].UpperBound); + Assert.Equal("3.0.0", result.Intervals[1].LowerBound); + Assert.Equal("4.0.0", result.Intervals[1].UpperBound); } /// /// Test that Parse returns empty set for empty string. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Parse_EmptyString_ReturnsEmptySet() { // Arrange @@ -104,14 +103,14 @@ public void VersionIntervalSet_Parse_EmptyString_ReturnsEmptySet() var result = VersionIntervalSet.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.HasCount(0, result.Intervals); + Assert.NotNull(result); + Assert.Empty(result.Intervals); } /// /// Test that an invalid interval token (e.g., no comma) is silently discarded. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Parse_InvalidToken_DiscardedSilently() { // Arrange - second token has brackets but no comma, making it invalid @@ -121,16 +120,16 @@ public void VersionIntervalSet_Parse_InvalidToken_DiscardedSilently() var result = VersionIntervalSet.Parse(text); // Assert - invalid token is discarded; only the valid interval remains - Assert.IsNotNull(result); - Assert.HasCount(1, result.Intervals); - Assert.AreEqual("1.0.0", result.Intervals[0].LowerBound); - Assert.AreEqual("2.0.0", result.Intervals[0].UpperBound); + Assert.NotNull(result); + Assert.Single(result.Intervals); + Assert.Equal("1.0.0", result.Intervals[0].LowerBound); + Assert.Equal("2.0.0", result.Intervals[0].UpperBound); } /// /// Test that Contains returns true when the candidate is inside the first interval. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_StringInsideFirstInterval_ReturnsTrue() { // Arrange @@ -140,13 +139,13 @@ public void VersionIntervalSet_Contains_StringInsideFirstInterval_ReturnsTrue() var result = intervalSet.Contains("1.5.0"); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains returns true when the candidate is inside a later interval. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_StringInsideLaterInterval_ReturnsTrue() { // Arrange @@ -156,13 +155,13 @@ public void VersionIntervalSet_Contains_StringInsideLaterInterval_ReturnsTrue() var result = intervalSet.Contains("3.5.0"); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains returns false when the candidate is outside all intervals. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_StringOutsideAllIntervals_ReturnsFalse() { // Arrange @@ -172,13 +171,13 @@ public void VersionIntervalSet_Contains_StringOutsideAllIntervals_ReturnsFalse() var result = intervalSet.Contains("2.5.0"); // Assert - Assert.IsFalse(result); + Assert.False(result); } /// /// Test that Contains returns false for an empty interval set. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_EmptySet_ReturnsFalse() { // Arrange @@ -188,13 +187,13 @@ public void VersionIntervalSet_Contains_EmptySet_ReturnsFalse() var result = intervalSet.Contains("1.0.0"); // Assert - Assert.IsFalse(result); + Assert.False(result); } /// /// Test that Contains(VersionTag) delegates to the semantic version comparable. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_VersionTag_DelegatesToSemanticVersion() { // Arrange @@ -205,39 +204,39 @@ public void VersionIntervalSet_Contains_VersionTag_DelegatesToSemanticVersion() var result = intervalSet.Contains(version); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains works correctly with pre-release versions across multiple intervals. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_PreReleaseVersions_HandlesCorrectly() { // Arrange - intervals that include pre-release boundaries var intervalSet = VersionIntervalSet.Parse("[1.0.0-rc.1,1.0.0],[2.0.0-alpha.1,2.0.0-beta.5)"); // Act & Assert - first interval tests - Assert.IsTrue(intervalSet.Contains("1.0.0-rc.1")); // Lower bound of first interval - Assert.IsTrue(intervalSet.Contains("1.0.0-rc.2")); // Within first interval - Assert.IsTrue(intervalSet.Contains("1.0.0")); // Upper bound of first interval + Assert.True(intervalSet.Contains("1.0.0-rc.1")); // Lower bound of first interval + Assert.True(intervalSet.Contains("1.0.0-rc.2")); // Within first interval + Assert.True(intervalSet.Contains("1.0.0")); // Upper bound of first interval // Act & Assert - second interval tests - Assert.IsTrue(intervalSet.Contains("2.0.0-alpha.1")); // Lower bound of second interval - Assert.IsTrue(intervalSet.Contains("2.0.0-beta.1")); // Within second interval - Assert.IsFalse(intervalSet.Contains("2.0.0-beta.5")); // Exclusive upper bound - Assert.IsFalse(intervalSet.Contains("2.0.0")); // Outside second interval (release > pre-release) + Assert.True(intervalSet.Contains("2.0.0-alpha.1")); // Lower bound of second interval + Assert.True(intervalSet.Contains("2.0.0-beta.1")); // Within second interval + Assert.False(intervalSet.Contains("2.0.0-beta.5")); // Exclusive upper bound + Assert.False(intervalSet.Contains("2.0.0")); // Outside second interval (release > pre-release) // Act & Assert - outside all intervals - Assert.IsFalse(intervalSet.Contains("1.0.0-alpha.1")); // Before first interval - Assert.IsFalse(intervalSet.Contains("1.5.0")); // Between intervals - Assert.IsFalse(intervalSet.Contains("2.1.0")); // After all intervals + Assert.False(intervalSet.Contains("1.0.0-alpha.1")); // Before first interval + Assert.False(intervalSet.Contains("1.5.0")); // Between intervals + Assert.False(intervalSet.Contains("2.1.0")); // After all intervals } /// /// Test that Contains works with VersionComparable overload for pre-release versions. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Contains_VersionComparable_HandlesPreRelease() { // Arrange @@ -248,16 +247,16 @@ public void VersionIntervalSet_Contains_VersionComparable_HandlesPreRelease() var versionOutside = VersionComparable.Create("1.1.0"); // Act & Assert - Assert.IsTrue(intervalSet.Contains(preReleaseInFirst)); - Assert.IsTrue(intervalSet.Contains(releaseInFirst)); - Assert.IsTrue(intervalSet.Contains(releaseInSecond)); - Assert.IsFalse(intervalSet.Contains(versionOutside)); + Assert.True(intervalSet.Contains(preReleaseInFirst)); + Assert.True(intervalSet.Contains(releaseInFirst)); + Assert.True(intervalSet.Contains(releaseInSecond)); + Assert.False(intervalSet.Contains(versionOutside)); } /// /// Test parsing intervals with pre-release bounds in the text. /// - [TestMethod] + [Fact] public void VersionIntervalSet_Parse_PreReleaseBounds_ParsesCorrectly() { // Arrange @@ -267,20 +266,20 @@ public void VersionIntervalSet_Parse_PreReleaseBounds_ParsesCorrectly() var result = VersionIntervalSet.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.HasCount(2, result.Intervals); + Assert.NotNull(result); + Assert.Equal(2, result.Intervals.Count); // First interval - Assert.AreEqual("1.0.0-alpha.1", result.Intervals[0].LowerBound); - Assert.AreEqual("1.0.0-rc.1", result.Intervals[0].UpperBound); - Assert.IsTrue(result.Intervals[0].LowerInclusive); - Assert.IsFalse(result.Intervals[0].UpperInclusive); + Assert.Equal("1.0.0-alpha.1", result.Intervals[0].LowerBound); + Assert.Equal("1.0.0-rc.1", result.Intervals[0].UpperBound); + Assert.True(result.Intervals[0].LowerInclusive); + Assert.False(result.Intervals[0].UpperInclusive); // Second interval - Assert.AreEqual("2.0.0-beta.1", result.Intervals[1].LowerBound); - Assert.AreEqual("2.0.0", result.Intervals[1].UpperBound); - Assert.IsTrue(result.Intervals[1].LowerInclusive); - Assert.IsTrue(result.Intervals[1].UpperInclusive); + Assert.Equal("2.0.0-beta.1", result.Intervals[1].LowerBound); + Assert.Equal("2.0.0", result.Intervals[1].UpperBound); + Assert.True(result.Intervals[1].LowerInclusive); + Assert.True(result.Intervals[1].UpperInclusive); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalTests.cs b/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalTests.cs index 1f7fc2a2..f3090259 100644 --- a/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Version/VersionIntervalTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.BuildMark.Tests.Version; /// /// Tests for VersionInterval.Parse method. /// -[TestClass] public class VersionIntervalTests { /// /// Test that Parse returns LowerInclusive=true for '[' opening bracket. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_InclusiveLower_IsInclusive() { // Arrange @@ -41,15 +40,15 @@ public void VersionInterval_Parse_InclusiveLower_IsInclusive() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("1.0.0", result.LowerBound); - Assert.IsTrue(result.LowerInclusive); + Assert.NotNull(result); + Assert.Equal("1.0.0", result.LowerBound); + Assert.True(result.LowerInclusive); } /// /// Test that Parse returns LowerInclusive=false for '(' opening bracket. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_ExclusiveLower_IsExclusive() { // Arrange @@ -59,15 +58,15 @@ public void VersionInterval_Parse_ExclusiveLower_IsExclusive() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("1.0.0", result.LowerBound); - Assert.IsFalse(result.LowerInclusive); + Assert.NotNull(result); + Assert.Equal("1.0.0", result.LowerBound); + Assert.False(result.LowerInclusive); } /// /// Test that Parse returns UpperInclusive=true for ']' closing bracket. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_InclusiveUpper_IsInclusive() { // Arrange @@ -77,15 +76,15 @@ public void VersionInterval_Parse_InclusiveUpper_IsInclusive() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("2.0.0", result.UpperBound); - Assert.IsTrue(result.UpperInclusive); + Assert.NotNull(result); + Assert.Equal("2.0.0", result.UpperBound); + Assert.True(result.UpperInclusive); } /// /// Test that Parse returns UpperInclusive=false for ')' closing bracket. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_ExclusiveUpper_IsExclusive() { // Arrange @@ -95,15 +94,15 @@ public void VersionInterval_Parse_ExclusiveUpper_IsExclusive() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("2.0.0", result.UpperBound); - Assert.IsFalse(result.UpperInclusive); + Assert.NotNull(result); + Assert.Equal("2.0.0", result.UpperBound); + Assert.False(result.UpperInclusive); } /// /// Test that Parse returns LowerBound=null for empty lower bound. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_UnboundedLower_HasNullLowerBound() { // Arrange @@ -113,17 +112,17 @@ public void VersionInterval_Parse_UnboundedLower_HasNullLowerBound() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.IsNull(result.LowerBound); - Assert.IsFalse(result.LowerInclusive); - Assert.AreEqual("1.0.1", result.UpperBound); - Assert.IsTrue(result.UpperInclusive); + Assert.NotNull(result); + Assert.Null(result.LowerBound); + Assert.False(result.LowerInclusive); + Assert.Equal("1.0.1", result.UpperBound); + Assert.True(result.UpperInclusive); } /// /// Test that Parse returns UpperBound=null for empty upper bound. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_UnboundedUpper_HasNullUpperBound() { // Arrange @@ -133,17 +132,17 @@ public void VersionInterval_Parse_UnboundedUpper_HasNullUpperBound() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("3.0.0", result.LowerBound); - Assert.IsTrue(result.LowerInclusive); - Assert.IsNull(result.UpperBound); - Assert.IsFalse(result.UpperInclusive); + Assert.NotNull(result); + Assert.Equal("3.0.0", result.LowerBound); + Assert.True(result.LowerInclusive); + Assert.Null(result.UpperBound); + Assert.False(result.UpperInclusive); } /// /// Test that Parse returns an interval with both bounds present. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_BothBoundsPresent_ReturnsInterval() { // Arrange @@ -153,15 +152,15 @@ public void VersionInterval_Parse_BothBoundsPresent_ReturnsInterval() var result = VersionInterval.Parse(text); // Assert - Assert.IsNotNull(result); - Assert.AreEqual("1.0.0", result.LowerBound); - Assert.AreEqual("2.0.0", result.UpperBound); + Assert.NotNull(result); + Assert.Equal("1.0.0", result.LowerBound); + Assert.Equal("2.0.0", result.UpperBound); } /// /// Test that Parse returns null for invalid format (no brackets). /// - [TestMethod] + [Fact] public void VersionInterval_Parse_InvalidFormat_ReturnsNull() { // Arrange @@ -171,13 +170,13 @@ public void VersionInterval_Parse_InvalidFormat_ReturnsNull() var result = VersionInterval.Parse(text); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse returns null for null input. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_NullInput_ReturnsNull() { // Arrange - null input @@ -186,13 +185,13 @@ public void VersionInterval_Parse_NullInput_ReturnsNull() var result = VersionInterval.Parse(null); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Parse returns null for empty string. /// - [TestMethod] + [Fact] public void VersionInterval_Parse_EmptyString_ReturnsNull() { // Arrange @@ -202,13 +201,13 @@ public void VersionInterval_Parse_EmptyString_ReturnsNull() var result = VersionInterval.Parse(text); // Assert - Assert.IsNull(result); + Assert.Null(result); } /// /// Test that Contains returns true when the candidate equals the inclusive lower bound. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_StringEqualToInclusiveLower_ReturnsTrue() { // Arrange @@ -218,13 +217,13 @@ public void VersionInterval_Contains_StringEqualToInclusiveLower_ReturnsTrue() var result = interval!.Contains("1.0.0"); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains returns false when the candidate equals the exclusive lower bound. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_StringEqualToExclusiveLower_ReturnsFalse() { // Arrange @@ -234,13 +233,13 @@ public void VersionInterval_Contains_StringEqualToExclusiveLower_ReturnsFalse() var result = interval!.Contains("1.0.0"); // Assert - Assert.IsFalse(result); + Assert.False(result); } /// /// Test that Contains returns true when the candidate equals the inclusive upper bound. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_StringEqualToInclusiveUpper_ReturnsTrue() { // Arrange @@ -250,13 +249,13 @@ public void VersionInterval_Contains_StringEqualToInclusiveUpper_ReturnsTrue() var result = interval!.Contains("2.0.0"); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains returns false when the candidate equals the exclusive upper bound. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_StringEqualToExclusiveUpper_ReturnsFalse() { // Arrange @@ -266,13 +265,13 @@ public void VersionInterval_Contains_StringEqualToExclusiveUpper_ReturnsFalse() var result = interval!.Contains("2.0.0"); // Assert - Assert.IsFalse(result); + Assert.False(result); } /// /// Test that Contains returns true for a candidate inside an unbounded interval. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_StringInsideUnboundedInterval_ReturnsTrue() { // Arrange @@ -282,13 +281,13 @@ public void VersionInterval_Contains_StringInsideUnboundedInterval_ReturnsTrue() var result = interval!.Contains("1.0.0"); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains returns false for a candidate outside the interval. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_StringOutsideInterval_ReturnsFalse() { // Arrange @@ -298,13 +297,13 @@ public void VersionInterval_Contains_StringOutsideInterval_ReturnsFalse() var result = interval!.Contains("2.1.0"); // Assert - Assert.IsFalse(result); + Assert.False(result); } /// /// Test that the VersionInfo overload delegates to the semantic version. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_Version_DelegatesToSemanticVersion() { // Arrange @@ -315,64 +314,64 @@ public void VersionInterval_Contains_Version_DelegatesToSemanticVersion() var result = interval!.Contains(version); // Assert - Assert.IsTrue(result); + Assert.True(result); } /// /// Test that Contains correctly handles pre-release version bounds. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_PreReleaseBounds_HandlesCorrectly() { // Arrange var interval = VersionInterval.Parse("[1.2.0-rc.1,1.2.0]"); // Act & Assert - Assert.IsTrue(interval!.Contains("1.2.0-rc.1")); // Equal to inclusive lower bound - Assert.IsTrue(interval!.Contains("1.2.0-rc.2")); // Between bounds (rc.2 > rc.1) - Assert.IsTrue(interval!.Contains("1.2.0")); // Equal to inclusive upper bound - Assert.IsFalse(interval!.Contains("1.2.0-alpha.1")); // Before lower bound (alpha < rc) - Assert.IsFalse(interval!.Contains("1.2.1")); // After upper bound - Assert.IsFalse(interval!.Contains("1.1.9")); // Before lower bound + Assert.True(interval!.Contains("1.2.0-rc.1")); // Equal to inclusive lower bound + Assert.True(interval!.Contains("1.2.0-rc.2")); // Between bounds (rc.2 > rc.1) + Assert.True(interval!.Contains("1.2.0")); // Equal to inclusive upper bound + Assert.False(interval!.Contains("1.2.0-alpha.1")); // Before lower bound (alpha < rc) + Assert.False(interval!.Contains("1.2.1")); // After upper bound + Assert.False(interval!.Contains("1.1.9")); // Before lower bound } /// /// Test that Contains correctly handles pre-release to pre-release intervals. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_PreReleaseToPreRelease_HandlesCorrectly() { // Arrange var interval = VersionInterval.Parse("[1.2.0-alpha.1,1.2.0-rc.1)"); // Act & Assert - Assert.IsTrue(interval!.Contains("1.2.0-alpha.1")); // Equal to inclusive lower bound - Assert.IsTrue(interval!.Contains("1.2.0-beta.1")); // Between bounds - Assert.IsFalse(interval!.Contains("1.2.0-rc.1")); // Equal to exclusive upper bound - Assert.IsFalse(interval!.Contains("1.2.0")); // After upper bound (release > pre-release) + Assert.True(interval!.Contains("1.2.0-alpha.1")); // Equal to inclusive lower bound + Assert.True(interval!.Contains("1.2.0-beta.1")); // Between bounds + Assert.False(interval!.Contains("1.2.0-rc.1")); // Equal to exclusive upper bound + Assert.False(interval!.Contains("1.2.0")); // After upper bound (release > pre-release) } /// /// Test that Contains correctly orders pre-release versions numerically. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_PreReleaseOrdering_UsesNumericComparison() { // Arrange var interval = VersionInterval.Parse("[1.0.0-alpha.5,1.0.0-alpha.10]"); // Act & Assert - Assert.IsTrue(interval!.Contains("1.0.0-alpha.5")); // Equal to lower bound - Assert.IsTrue(interval!.Contains("1.0.0-alpha.6")); // Between bounds - Assert.IsTrue(interval!.Contains("1.0.0-alpha.10")); // Equal to upper bound - Assert.IsFalse(interval!.Contains("1.0.0-alpha.4")); // Before lower bound - Assert.IsFalse(interval!.Contains("1.0.0-alpha.11")); // After upper bound + Assert.True(interval!.Contains("1.0.0-alpha.5")); // Equal to lower bound + Assert.True(interval!.Contains("1.0.0-alpha.6")); // Between bounds + Assert.True(interval!.Contains("1.0.0-alpha.10")); // Equal to upper bound + Assert.False(interval!.Contains("1.0.0-alpha.4")); // Before lower bound + Assert.False(interval!.Contains("1.0.0-alpha.11")); // After upper bound } /// /// Test that VersionComparable overload works with pre-release versions. /// - [TestMethod] + [Fact] public void VersionInterval_Contains_VersionComparable_HandlesPreRelease() { // Arrange @@ -381,8 +380,8 @@ public void VersionInterval_Contains_VersionComparable_HandlesPreRelease() var releaseVersion = VersionComparable.Create("1.2.0"); // Act & Assert - Assert.IsTrue(interval!.Contains(preReleaseVersion)); - Assert.IsTrue(interval!.Contains(releaseVersion)); + Assert.True(interval!.Contains(preReleaseVersion)); + Assert.True(interval!.Contains(releaseVersion)); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Version/VersionSemanticTests.cs b/test/DemaConsulting.BuildMark.Tests/Version/VersionSemanticTests.cs index 214b2112..52778866 100644 --- a/test/DemaConsulting.BuildMark.Tests/Version/VersionSemanticTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Version/VersionSemanticTests.cs @@ -25,60 +25,59 @@ namespace DemaConsulting.BuildMark.Tests.Version; /// /// Tests for the VersionSemantic class. /// -[TestClass] public class VersionSemanticTests { /// /// Test that VersionSemantic creates instance with build metadata. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_WithBuildMetadata_ReturnsInstance() { // Arrange & Act var version = VersionSemantic.Create("1.2.3+build.123"); // Assert - Assert.IsNotNull(version); - Assert.AreEqual("build.123", version.Metadata); - Assert.AreEqual("1.2.3+build.123", version.FullVersion); + Assert.NotNull(version); + Assert.Equal("build.123", version.Metadata); + Assert.Equal("1.2.3+build.123", version.FullVersion); } /// /// Test that VersionSemantic creates instance without build metadata. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_WithoutBuildMetadata_ReturnsInstance() { // Arrange & Act var version = VersionSemantic.Create("1.2.3"); // Assert - Assert.IsNotNull(version); - Assert.IsNull(version.Metadata); - Assert.AreEqual("1.2.3", version.FullVersion); + Assert.NotNull(version); + Assert.Null(version.Metadata); + Assert.Equal("1.2.3", version.FullVersion); } /// /// Test that VersionSemantic properties delegate to comparable correctly. /// - [TestMethod] + [Fact] public void VersionSemantic_Properties_DelegateToComparable_Correctly() { // Arrange & Act var version = VersionSemantic.Create("1.2.3-alpha"); // Assert - Assert.AreEqual(1, version.Major); - Assert.AreEqual(2, version.Minor); - Assert.AreEqual(3, version.Patch); - Assert.AreEqual("alpha", version.PreRelease); - Assert.AreEqual("1.2.3-alpha", version.CompareVersion); + Assert.Equal(1, version.Major); + Assert.Equal(2, version.Minor); + Assert.Equal(3, version.Patch); + Assert.Equal("alpha", version.PreRelease); + Assert.Equal("1.2.3-alpha", version.CompareVersion); } /// /// Test that VersionSemantic formats completely with all components. /// - [TestMethod] + [Fact] public void VersionSemantic_ToString_FormatsCompletely_WithAllComponents() { // Arrange @@ -88,27 +87,27 @@ public void VersionSemantic_ToString_FormatsCompletely_WithAllComponents() var result = version.FullVersion; // Assert - Assert.AreEqual("1.2.3-alpha+build.123", result); + Assert.Equal("1.2.3-alpha+build.123", result); } /// /// Test that VersionSemantic PreRelease returns empty string for release versions. /// - [TestMethod] + [Fact] public void VersionSemantic_PreRelease_ReturnsEmptyStringForRelease() { // Arrange & Act var version = VersionSemantic.Create("1.2.3"); // Assert - Assert.AreEqual("", version.PreRelease); - Assert.IsFalse(version.IsPreRelease); + Assert.Equal("", version.PreRelease); + Assert.False(version.IsPreRelease); } /// /// Test that VersionSemantic parses valid semantic versions correctly. /// - [TestMethod] + [Fact] public void VersionSemantic_Parse_ValidSemanticVersions_ParsesCorrectly() { // Arrange & Act @@ -118,90 +117,90 @@ public void VersionSemantic_Parse_ValidSemanticVersions_ParsesCorrectly() var complex = VersionSemantic.Create("1.0.0-beta.2+exp.sha.5114f85"); // Assert - Assert.AreEqual("1.0.0", simple.FullVersion); - Assert.AreEqual("1.0.0-alpha.1", preRelease.FullVersion); - Assert.AreEqual("1.0.0+20130313144700", withMetadata.FullVersion); - Assert.AreEqual("1.0.0-beta.2+exp.sha.5114f85", complex.FullVersion); + Assert.Equal("1.0.0", simple.FullVersion); + Assert.Equal("1.0.0-alpha.1", preRelease.FullVersion); + Assert.Equal("1.0.0+20130313144700", withMetadata.FullVersion); + Assert.Equal("1.0.0-beta.2+exp.sha.5114f85", complex.FullVersion); } /// /// Test that VersionSemantic parses simple version without metadata. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_SimpleVersion_ParsesVersion() { // Arrange & Act var version = VersionSemantic.Create("1.2.3"); // Assert - Assert.AreEqual(1, version.Major); - Assert.AreEqual(2, version.Minor); - Assert.AreEqual(3, version.Patch); - Assert.AreEqual("1.2.3", version.Numbers); - Assert.AreEqual("", version.PreRelease); - Assert.IsNull(version.Metadata); - Assert.AreEqual("1.2.3", version.CompareVersion); - Assert.AreEqual("1.2.3", version.FullVersion); - Assert.IsFalse(version.IsPreRelease); + Assert.Equal(1, version.Major); + Assert.Equal(2, version.Minor); + Assert.Equal(3, version.Patch); + Assert.Equal("1.2.3", version.Numbers); + Assert.Equal("", version.PreRelease); + Assert.Null(version.Metadata); + Assert.Equal("1.2.3", version.CompareVersion); + Assert.Equal("1.2.3", version.FullVersion); + Assert.False(version.IsPreRelease); } /// /// Test that VersionSemantic parses version with metadata. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_VersionWithMetadata_ParsesVersion() { // Arrange & Act var version = VersionSemantic.Create("1.2.3+build.5"); // Assert - Assert.AreEqual("1.2.3", version.Numbers); - Assert.AreEqual("", version.PreRelease); - Assert.AreEqual("build.5", version.Metadata); - Assert.AreEqual("1.2.3", version.CompareVersion); - Assert.AreEqual("1.2.3+build.5", version.FullVersion); - Assert.IsFalse(version.IsPreRelease); + Assert.Equal("1.2.3", version.Numbers); + Assert.Equal("", version.PreRelease); + Assert.Equal("build.5", version.Metadata); + Assert.Equal("1.2.3", version.CompareVersion); + Assert.Equal("1.2.3+build.5", version.FullVersion); + Assert.False(version.IsPreRelease); } /// /// Test that VersionSemantic parses pre-release with metadata. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_PreReleaseWithMetadata_ParsesVersion() { // Arrange & Act var version = VersionSemantic.Create("2.0.0-alpha.1+linux.x64"); // Assert - Assert.AreEqual("2.0.0", version.Numbers); - Assert.AreEqual("alpha.1", version.PreRelease); - Assert.AreEqual("linux.x64", version.Metadata); - Assert.AreEqual("2.0.0-alpha.1", version.CompareVersion); - Assert.AreEqual("2.0.0-alpha.1+linux.x64", version.FullVersion); - Assert.IsTrue(version.IsPreRelease); + Assert.Equal("2.0.0", version.Numbers); + Assert.Equal("alpha.1", version.PreRelease); + Assert.Equal("linux.x64", version.Metadata); + Assert.Equal("2.0.0-alpha.1", version.CompareVersion); + Assert.Equal("2.0.0-alpha.1+linux.x64", version.FullVersion); + Assert.True(version.IsPreRelease); } /// /// Test that TryCreate returns null for invalid version. /// - [TestMethod] + [Fact] public void VersionSemantic_TryCreate_InvalidVersion_ReturnsNull() { // Act var version = VersionSemantic.TryCreate("not-a-version"); // Assert - Assert.IsNull(version); + Assert.Null(version); } /// /// Test that Create throws ArgumentException for invalid version. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_InvalidVersion_ThrowsArgumentException() { // Act - var exception = Assert.ThrowsExactly(() => VersionSemantic.Create("not-a-version")); + var exception = Assert.Throws(() => VersionSemantic.Create("not-a-version")); // Assert Assert.Contains("does not match semantic version format", exception.Message); @@ -210,7 +209,7 @@ public void VersionSemantic_Create_InvalidVersion_ThrowsArgumentException() /// /// Test that VersionSemantic correctly exposes comparable for comparison. /// - [TestMethod] + [Fact] public void VersionSemantic_Comparable_AllowsComparison() { // Arrange @@ -218,7 +217,7 @@ public void VersionSemantic_Comparable_AllowsComparison() var version2 = VersionSemantic.Create("1.2.4+build2"); // Act & Assert - Assert.IsTrue(version1.Comparable < version2.Comparable); + Assert.True(version1.Comparable < version2.Comparable); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Version/VersionTagTests.cs b/test/DemaConsulting.BuildMark.Tests/Version/VersionTagTests.cs index 7892162b..df4c74f6 100644 --- a/test/DemaConsulting.BuildMark.Tests/Version/VersionTagTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Version/VersionTagTests.cs @@ -25,106 +25,105 @@ namespace DemaConsulting.BuildMark.Tests.Version; /// /// Tests for the VersionTag class. /// -[TestClass] public class VersionTagTests { /// /// Test that VersionTag creates instances from valid tags. /// - [TestMethod] + [Fact] public void VersionTag_Create_ValidTag_ReturnsVersionTag() { // Arrange & Act var versionTag = VersionTag.Create("v1.2.3"); // Assert - Assert.IsNotNull(versionTag); - Assert.AreEqual("v1.2.3", versionTag.Tag); - Assert.AreEqual("1.2.3", versionTag.FullVersion); + Assert.NotNull(versionTag); + Assert.Equal("v1.2.3", versionTag.Tag); + Assert.Equal("1.2.3", versionTag.FullVersion); } /// /// Test that VersionTag parses standard tags correctly. /// - [TestMethod] + [Fact] public void VersionTag_Create_StandardTag_ParsesCorrectly() { // Arrange & Act var versionTag = VersionTag.Create("1.2.3"); // Assert - Assert.AreEqual("1.2.3", versionTag.Tag); - Assert.AreEqual("1.2.3", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); + Assert.Equal("1.2.3", versionTag.Tag); + Assert.Equal("1.2.3", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); } /// /// Test that VersionTag parses prefixed tags correctly. /// - [TestMethod] + [Fact] public void VersionTag_Create_PrefixedTag_ParsesCorrectly() { // Arrange & Act var versionTag = VersionTag.Create("v1.2.3"); // Assert - Assert.AreEqual("v1.2.3", versionTag.Tag); - Assert.AreEqual("1.2.3", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); + Assert.Equal("v1.2.3", versionTag.Tag); + Assert.Equal("1.2.3", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); } /// /// Test that VersionTag normalizes dot-separated pre-release to hyphen. /// - [TestMethod] + [Fact] public void VersionTag_Create_DotSeparatedPreRelease_NormalizesToHyphen() { // Arrange & Act var versionTag = VersionTag.Create("v1.2.3.alpha.1"); // Assert - Assert.AreEqual("v1.2.3.alpha.1", versionTag.Tag); - Assert.AreEqual("1.2.3-alpha.1", versionTag.FullVersion); - Assert.AreEqual("alpha.1", versionTag.PreRelease); + Assert.Equal("v1.2.3.alpha.1", versionTag.Tag); + Assert.Equal("1.2.3-alpha.1", versionTag.FullVersion); + Assert.Equal("alpha.1", versionTag.PreRelease); } /// /// Test that VersionTag extracts version from complex tags correctly. /// - [TestMethod] + [Fact] public void VersionTag_Create_ComplexTag_ExtractsVersionCorrectly() { // Arrange & Act var versionTag = VersionTag.Create("Release_1.2.3.beta.5+build.123"); // Assert - Assert.AreEqual("Release_1.2.3.beta.5+build.123", versionTag.Tag); - Assert.AreEqual("1.2.3-beta.5+build.123", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("beta.5", versionTag.PreRelease); - Assert.AreEqual("build.123", versionTag.Metadata); + Assert.Equal("Release_1.2.3.beta.5+build.123", versionTag.Tag); + Assert.Equal("1.2.3-beta.5+build.123", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("beta.5", versionTag.PreRelease); + Assert.Equal("build.123", versionTag.Metadata); } /// /// Test that VersionTag properties expose original and parsed versions correctly. /// - [TestMethod] + [Fact] public void VersionTag_Properties_ExposeOriginalAndParsed_Correctly() { // Arrange & Act var versionTag = VersionTag.Create("v1.2.3-alpha"); // Assert - Assert.AreEqual("v1.2.3-alpha", versionTag.Tag); // Original - Assert.AreEqual("1.2.3-alpha", versionTag.FullVersion); // Parsed - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("alpha", versionTag.PreRelease); + Assert.Equal("v1.2.3-alpha", versionTag.Tag); // Original + Assert.Equal("1.2.3-alpha", versionTag.FullVersion); // Parsed + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("alpha", versionTag.PreRelease); } /// /// Test that VersionTag ToString returns original tag. /// - [TestMethod] + [Fact] public void VersionTag_ToString_ReturnsOriginalTag() { // Arrange @@ -135,68 +134,68 @@ public void VersionTag_ToString_ReturnsOriginalTag() var result = versionTag.ToString(); // Assert - Assert.AreEqual(originalTag, result); + Assert.Equal(originalTag, result); } /// /// Test that VersionTag parses simple v-prefix version. /// - [TestMethod] + [Fact] public void VersionTag_Create_SimpleVPrefix_ParsesVersion() { // Arrange & Act var versionTag = VersionTag.Create("v1.2.3"); // Assert - Assert.AreEqual("v1.2.3", versionTag.Tag); - Assert.AreEqual("1.2.3", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("", versionTag.PreRelease); - Assert.AreEqual("1.2.3", versionTag.CompareVersion); - Assert.AreEqual("", versionTag.Metadata); - Assert.IsFalse(versionTag.IsPreRelease); + Assert.Equal("v1.2.3", versionTag.Tag); + Assert.Equal("1.2.3", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("", versionTag.PreRelease); + Assert.Equal("1.2.3", versionTag.CompareVersion); + Assert.Equal("", versionTag.Metadata); + Assert.False(versionTag.IsPreRelease); } /// /// Test that VersionTag parses complex version with prefix, pre-release, and metadata. /// - [TestMethod] + [Fact] public void VersionTag_Create_ComplexVersionWithMetadata_ParsesVersion() { // Arrange & Act var versionTag = VersionTag.Create("Rel_1.2.3.rc.4+build.5"); // Assert - Assert.AreEqual("Rel_1.2.3.rc.4+build.5", versionTag.Tag); - Assert.AreEqual("1.2.3-rc.4+build.5", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("rc.4", versionTag.PreRelease); - Assert.AreEqual("1.2.3-rc.4", versionTag.CompareVersion); - Assert.AreEqual("build.5", versionTag.Metadata); - Assert.IsTrue(versionTag.IsPreRelease); + Assert.Equal("Rel_1.2.3.rc.4+build.5", versionTag.Tag); + Assert.Equal("1.2.3-rc.4+build.5", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("rc.4", versionTag.PreRelease); + Assert.Equal("1.2.3-rc.4", versionTag.CompareVersion); + Assert.Equal("build.5", versionTag.Metadata); + Assert.True(versionTag.IsPreRelease); } /// /// Test that TryCreate returns null for invalid tag. /// - [TestMethod] + [Fact] public void VersionTag_TryCreate_InvalidTag_ReturnsNull() { // Act var versionTag = VersionTag.TryCreate("not-a-version"); // Assert - Assert.IsNull(versionTag); + Assert.Null(versionTag); } /// /// Test that Create throws ArgumentException for invalid tag. /// - [TestMethod] + [Fact] public void VersionTag_Create_InvalidTag_ThrowsArgumentException() { // Act - var exception = Assert.ThrowsExactly(() => VersionTag.Create("not-a-version")); + var exception = Assert.Throws(() => VersionTag.Create("not-a-version")); // Assert Assert.Contains("does not match version format", exception.Message); @@ -205,41 +204,41 @@ public void VersionTag_Create_InvalidTag_ThrowsArgumentException() /// /// Test that VersionTag handles no prefix. /// - [TestMethod] + [Fact] public void VersionTag_Create_NoPrefix_ParsesVersion() { // Arrange & Act var versionTag = VersionTag.Create("1.0.0"); // Assert - Assert.AreEqual("1.0.0", versionTag.Tag); - Assert.AreEqual("1.0.0", versionTag.FullVersion); - Assert.IsFalse(versionTag.IsPreRelease); + Assert.Equal("1.0.0", versionTag.Tag); + Assert.Equal("1.0.0", versionTag.FullVersion); + Assert.False(versionTag.IsPreRelease); } /// /// Test that VersionTag correctly parses hyphen-separated pre-release. /// - [TestMethod] + [Fact] public void VersionTag_Create_HyphenPreReleaseWithMetadata_ParsesVersion() { // Arrange & Act var versionTag = VersionTag.Create("Rel_1.2.3-rc.4+build.5"); // Assert - Assert.AreEqual("Rel_1.2.3-rc.4+build.5", versionTag.Tag); - Assert.AreEqual("1.2.3-rc.4+build.5", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("rc.4", versionTag.PreRelease); - Assert.AreEqual("1.2.3-rc.4", versionTag.CompareVersion); - Assert.AreEqual("build.5", versionTag.Metadata); - Assert.IsTrue(versionTag.IsPreRelease); + Assert.Equal("Rel_1.2.3-rc.4+build.5", versionTag.Tag); + Assert.Equal("1.2.3-rc.4+build.5", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("rc.4", versionTag.PreRelease); + Assert.Equal("1.2.3-rc.4", versionTag.CompareVersion); + Assert.Equal("build.5", versionTag.Metadata); + Assert.True(versionTag.IsPreRelease); } /// /// Test that VersionTag provides access to comparable version for sorting. /// - [TestMethod] + [Fact] public void VersionTag_Semantic_AllowsComparison() { // Arrange @@ -247,13 +246,13 @@ public void VersionTag_Semantic_AllowsComparison() var tag2 = VersionTag.Create("v1.11.2"); // Act & Assert - Assert.IsTrue(tag1.Semantic.Comparable < tag2.Semantic.Comparable); + Assert.True(tag1.Semantic.Comparable < tag2.Semantic.Comparable); } /// /// Test that VersionComparable equals works with different prefixes but same version. /// - [TestMethod] + [Fact] public void VersionComparable_Equals_DifferentPrefixesSameVersion_ReturnsTrue() { // Arrange @@ -263,16 +262,16 @@ public void VersionComparable_Equals_DifferentPrefixesSameVersion_ReturnsTrue() var tag4 = VersionTag.Create("release/1.2.3"); // Act & Assert - Assert.AreEqual(tag1.Semantic.Comparable, tag2.Semantic.Comparable); - Assert.AreEqual(tag1.Semantic.Comparable, tag3.Semantic.Comparable); - Assert.AreEqual(tag1.Semantic.Comparable, tag4.Semantic.Comparable); - Assert.AreEqual(tag2.Semantic.Comparable, tag3.Semantic.Comparable); + Assert.Equal(tag1.Semantic.Comparable, tag2.Semantic.Comparable); + Assert.Equal(tag1.Semantic.Comparable, tag3.Semantic.Comparable); + Assert.Equal(tag1.Semantic.Comparable, tag4.Semantic.Comparable); + Assert.Equal(tag2.Semantic.Comparable, tag3.Semantic.Comparable); } /// /// Test that VersionTag GetVersionComparable works for semantic tag comparison. /// - [TestMethod] + [Fact] public void VersionTag_GetVersionComparable_SemanticTags_ReturnsCorrectComparison() { // Arrange @@ -281,62 +280,62 @@ public void VersionTag_GetVersionComparable_SemanticTags_ReturnsCorrectCompariso var tag3 = VersionTag.Create("v1.0.0"); // Act & Assert - Test semantic version comparison rules - Assert.IsTrue(tag1.Semantic.Comparable < tag2.Semantic.Comparable, "alpha < beta"); - Assert.IsTrue(tag2.Semantic.Comparable < tag3.Semantic.Comparable, "beta < release"); - Assert.IsTrue(tag1.Semantic.Comparable < tag3.Semantic.Comparable, "alpha < release"); + Assert.True(tag1.Semantic.Comparable < tag2.Semantic.Comparable, "alpha < beta"); + Assert.True(tag2.Semantic.Comparable < tag3.Semantic.Comparable, "beta < release"); + Assert.True(tag1.Semantic.Comparable < tag3.Semantic.Comparable, "alpha < release"); } /// /// Test that VersionTag parses tags with path-separator prefix correctly. /// - [TestMethod] + [Fact] public void VersionTag_Create_PathSeparatorPrefix_ParsesCorrectly() { // Arrange & Act var versionTag = VersionTag.Create("release/1.2.3"); // Assert - Assert.AreEqual("release/1.2.3", versionTag.Tag); - Assert.AreEqual("1.2.3", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("", versionTag.PreRelease); - Assert.IsFalse(versionTag.IsPreRelease); + Assert.Equal("release/1.2.3", versionTag.Tag); + Assert.Equal("1.2.3", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("", versionTag.PreRelease); + Assert.False(versionTag.IsPreRelease); } /// /// Test that VersionTag parses tags with path-separator prefix and pre-release correctly. /// - [TestMethod] + [Fact] public void VersionTag_Create_PathSeparatorPrefixWithPreRelease_ParsesCorrectly() { // Arrange & Act var versionTag = VersionTag.Create("release/1.2.3-rc.4"); // Assert - Assert.AreEqual("release/1.2.3-rc.4", versionTag.Tag); - Assert.AreEqual("1.2.3-rc.4", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("rc.4", versionTag.PreRelease); - Assert.AreEqual("1.2.3-rc.4", versionTag.CompareVersion); - Assert.IsTrue(versionTag.IsPreRelease); + Assert.Equal("release/1.2.3-rc.4", versionTag.Tag); + Assert.Equal("1.2.3-rc.4", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("rc.4", versionTag.PreRelease); + Assert.Equal("1.2.3-rc.4", versionTag.CompareVersion); + Assert.True(versionTag.IsPreRelease); } /// /// Test that VersionTag parses tags with multi-level path-separator prefix correctly. /// - [TestMethod] + [Fact] public void VersionTag_Create_MultiLevelPathPrefix_ParsesCorrectly() { // Arrange & Act var versionTag = VersionTag.Create("builds/release/1.2.3-beta.1+build.99"); // Assert - Assert.AreEqual("builds/release/1.2.3-beta.1+build.99", versionTag.Tag); - Assert.AreEqual("1.2.3-beta.1+build.99", versionTag.FullVersion); - Assert.AreEqual("1.2.3", versionTag.Numbers); - Assert.AreEqual("beta.1", versionTag.PreRelease); - Assert.AreEqual("build.99", versionTag.Metadata); - Assert.IsTrue(versionTag.IsPreRelease); + Assert.Equal("builds/release/1.2.3-beta.1+build.99", versionTag.Tag); + Assert.Equal("1.2.3-beta.1+build.99", versionTag.FullVersion); + Assert.Equal("1.2.3", versionTag.Numbers); + Assert.Equal("beta.1", versionTag.PreRelease); + Assert.Equal("build.99", versionTag.Metadata); + Assert.True(versionTag.IsPreRelease); } } diff --git a/test/DemaConsulting.BuildMark.Tests/Version/VersionTests.cs b/test/DemaConsulting.BuildMark.Tests/Version/VersionTests.cs index 5dd26891..d6c16dd6 100644 --- a/test/DemaConsulting.BuildMark.Tests/Version/VersionTests.cs +++ b/test/DemaConsulting.BuildMark.Tests/Version/VersionTests.cs @@ -19,7 +19,6 @@ // SOFTWARE. using DemaConsulting.BuildMark.Version; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace DemaConsulting.BuildMark.Tests.Version; @@ -27,13 +26,12 @@ namespace DemaConsulting.BuildMark.Tests.Version; /// Version subsystem integration tests. /// Tests the overall behavior of the Version subsystem components working together. /// -[TestClass] public class VersionTests { /// /// Test that VersionComparable can be created from valid versions. /// - [TestMethod] + [Fact] public void VersionComparable_Create_ValidVersions_ReturnsVersionComparable() { // Arrange & Act @@ -42,18 +40,18 @@ public void VersionComparable_Create_ValidVersions_ReturnsVersionComparable() var complex = VersionComparable.Create("10.5.99-beta.10"); // Assert - Assert.IsNotNull(simple); - Assert.IsNotNull(preRelease); - Assert.IsNotNull(complex); - Assert.IsInstanceOfType(simple); - Assert.IsInstanceOfType(preRelease); - Assert.IsInstanceOfType(complex); + Assert.NotNull(simple); + Assert.NotNull(preRelease); + Assert.NotNull(complex); + Assert.IsAssignableFrom(simple); + Assert.IsAssignableFrom(preRelease); + Assert.IsAssignableFrom(complex); } /// /// Test that VersionSemantic can be created from valid semantic versions. /// - [TestMethod] + [Fact] public void VersionSemantic_Create_ValidSemanticVersion_ReturnsVersionSemantic() { // Arrange & Act @@ -62,18 +60,18 @@ public void VersionSemantic_Create_ValidSemanticVersion_ReturnsVersionSemantic() var complex = VersionSemantic.Create("1.0.0-alpha.beta.2+exp.sha.5114f85"); // Assert - Assert.IsNotNull(simple); - Assert.IsNotNull(withMetadata); - Assert.IsNotNull(complex); - Assert.IsInstanceOfType(simple); - Assert.IsInstanceOfType(withMetadata); - Assert.IsInstanceOfType(complex); + Assert.NotNull(simple); + Assert.NotNull(withMetadata); + Assert.NotNull(complex); + Assert.IsAssignableFrom(simple); + Assert.IsAssignableFrom(withMetadata); + Assert.IsAssignableFrom(complex); } /// /// Test that VersionTag can be created from valid tags. /// - [TestMethod] + [Fact] public void VersionTag_Create_ValidTag_ReturnsVersionTag() { // Arrange & Act @@ -82,18 +80,18 @@ public void VersionTag_Create_ValidTag_ReturnsVersionTag() var complex = VersionTag.Create("release-1.5.0-rc.1"); // Assert - Assert.IsNotNull(simple); - Assert.IsNotNull(prefixed); - Assert.IsNotNull(complex); - Assert.IsInstanceOfType(simple); - Assert.IsInstanceOfType(prefixed); - Assert.IsInstanceOfType(complex); + Assert.NotNull(simple); + Assert.NotNull(prefixed); + Assert.NotNull(complex); + Assert.IsAssignableFrom(simple); + Assert.IsAssignableFrom(prefixed); + Assert.IsAssignableFrom(complex); } /// /// Test that VersionInterval can be created from valid interval parameters. /// - [TestMethod] + [Fact] public void VersionInterval_Create_ValidInterval_ReturnsVersionInterval() { // Arrange & Act @@ -102,18 +100,18 @@ public void VersionInterval_Create_ValidInterval_ReturnsVersionInterval() var mixed = new VersionInterval("1.0.0", true, "2.0.0", false); // Assert - Assert.IsNotNull(inclusive); - Assert.IsNotNull(exclusive); - Assert.IsNotNull(mixed); - Assert.IsInstanceOfType(inclusive); - Assert.IsInstanceOfType(exclusive); - Assert.IsInstanceOfType(mixed); + Assert.NotNull(inclusive); + Assert.NotNull(exclusive); + Assert.NotNull(mixed); + Assert.IsAssignableFrom(inclusive); + Assert.IsAssignableFrom(exclusive); + Assert.IsAssignableFrom(mixed); } /// /// Test that VersionCommitTag can be created from valid parameters. /// - [TestMethod] + [Fact] public void VersionCommitTag_Constructor_ValidParameters_CreatesInstance() { // Arrange @@ -124,17 +122,17 @@ public void VersionCommitTag_Constructor_ValidParameters_CreatesInstance() var versionCommitTag = new VersionCommitTag(versionTag!, commitHash); // Assert - Assert.IsNotNull(versionCommitTag); - Assert.IsInstanceOfType(versionCommitTag); - Assert.AreEqual(versionTag, versionCommitTag.VersionTag); - Assert.AreEqual(commitHash, versionCommitTag.CommitHash); + Assert.NotNull(versionCommitTag); + Assert.IsAssignableFrom(versionCommitTag); + Assert.Equal(versionTag, versionCommitTag.VersionTag); + Assert.Equal(commitHash, versionCommitTag.CommitHash); } /// /// Test that the Version subsystem can create and use all version types correctly. /// This validates the subsystem-level requirement for version processing capabilities. /// - [TestMethod] + [Fact] public void Version_Subsystem_CreateAllVersionTypes_WorksCorrectly() { // Arrange - Create instances of all version types @@ -145,25 +143,25 @@ public void Version_Subsystem_CreateAllVersionTypes_WorksCorrectly() var versionCommitTag = new VersionCommitTag(versionTag!, "abc123def456"); // Assert - All types are created successfully - Assert.IsNotNull(versionComparable); - Assert.IsNotNull(versionSemantic); - Assert.IsNotNull(versionTag); - Assert.IsNotNull(versionInterval); - Assert.IsNotNull(versionCommitTag); + Assert.NotNull(versionComparable); + Assert.NotNull(versionSemantic); + Assert.NotNull(versionTag); + Assert.NotNull(versionInterval); + Assert.NotNull(versionCommitTag); // Assert - Version properties are accessible and correct - Assert.AreEqual("1.2.3-alpha.1", versionComparable.CompareVersion); - Assert.AreEqual("2.0.0-beta.2+build.1", versionSemantic.FullVersion); - Assert.AreEqual("v3.1.0", versionTag.Tag); - Assert.AreEqual("1.0.0", versionInterval.LowerBound!); - Assert.AreEqual("abc123def456", versionCommitTag.CommitHash); + Assert.Equal("1.2.3-alpha.1", versionComparable.CompareVersion); + Assert.Equal("2.0.0-beta.2+build.1", versionSemantic.FullVersion); + Assert.Equal("v3.1.0", versionTag.Tag); + Assert.Equal("1.0.0", versionInterval.LowerBound!); + Assert.Equal("abc123def456", versionCommitTag.CommitHash); } /// /// Test that version comparison operations work correctly across different version types. /// This validates semantic versioning compliance requirement. /// - [TestMethod] + [Fact] public void Version_Subsystem_SemanticVersioningCompliance_WorksCorrectly() { // Arrange - Create versions that test SemVer compliance @@ -175,18 +173,18 @@ public void Version_Subsystem_SemanticVersioningCompliance_WorksCorrectly() var version6 = VersionComparable.Create("1.0.0"); // Assert - SemVer precedence is maintained - Assert.IsLessThan(0, version1!.CompareTo(version2), "1.0.0-alpha < 1.0.0-alpha.1"); - Assert.IsLessThan(0, version2!.CompareTo(version3), "1.0.0-alpha.1 < 1.0.0-alpha.beta"); - Assert.IsLessThan(0, version3!.CompareTo(version4), "1.0.0-alpha.beta < 1.0.0-beta"); - Assert.IsLessThan(0, version4!.CompareTo(version5), "1.0.0-beta < 1.0.0-beta.2"); - Assert.IsLessThan(0, version5!.CompareTo(version6), "1.0.0-beta.2 < 1.0.0"); + Assert.True(version1!.CompareTo(version2) < 0, "1.0.0-alpha < 1.0.0-alpha.1"); + Assert.True(version2!.CompareTo(version3) < 0, "1.0.0-alpha.1 < 1.0.0-alpha.beta"); + Assert.True(version3!.CompareTo(version4) < 0, "1.0.0-alpha.beta < 1.0.0-beta"); + Assert.True(version4!.CompareTo(version5) < 0, "1.0.0-beta < 1.0.0-beta.2"); + Assert.True(version5!.CompareTo(version6) < 0, "1.0.0-beta.2 < 1.0.0"); } /// /// Test that version tags can extract comparable versions for repository operations. /// This validates tag processing integration for BuildNotes functionality. /// - [TestMethod] + [Fact] public void Version_Subsystem_TagToComparableIntegration_WorksCorrectly() { // Arrange - Create version tags with various formats @@ -200,16 +198,16 @@ public void Version_Subsystem_TagToComparableIntegration_WorksCorrectly() var comparable3 = tag3!.Semantic.Comparable; // Assert - Comparable versions are extracted correctly - Assert.IsNotNull(comparable1); - Assert.IsNotNull(comparable2); - Assert.IsNotNull(comparable3); + Assert.NotNull(comparable1); + Assert.NotNull(comparable2); + Assert.NotNull(comparable3); - Assert.AreEqual("1.2.3", comparable1.CompareVersion); - Assert.AreEqual("2.0.0-rc.1", comparable2.CompareVersion); - Assert.AreEqual("1.5.0", comparable3.CompareVersion); + Assert.Equal("1.2.3", comparable1.CompareVersion); + Assert.Equal("2.0.0-rc.1", comparable2.CompareVersion); + Assert.Equal("1.5.0", comparable3.CompareVersion); // Assert - Version ordering works as expected - Assert.IsLessThan(0, comparable1.CompareTo(comparable3)); // 1.2.3 < 1.5.0 - Assert.IsLessThan(0, comparable3.CompareTo(comparable2)); // 1.5.0 < 2.0.0-rc.1 + Assert.True(comparable1.CompareTo(comparable3) < 0); // 1.2.3 < 1.5.0 + Assert.True(comparable3.CompareTo(comparable2) < 0); // 1.5.0 < 2.0.0-rc.1 } }