diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 982cbc9..ac3fd38 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -39,7 +39,7 @@ ] }, "demaconsulting.buildmark": { - "version": "0.2.0", + "version": "0.3.0", "commands": [ "buildmark" ] diff --git a/.cspell.json b/.cspell.json index b0415f7..164e13c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -7,8 +7,10 @@ "buildmark", "BuildMark", "buildnotes", + "buildtransitive", "camelcase", "CodeQL", + "contentfiles", "copilot", "coveragexml", "cspell", @@ -26,6 +28,7 @@ "dotnet", "dotnettool", "editorconfig", + "errorlevel", "filepart", "fsproj", "Gidget", diff --git a/.editorconfig b/.editorconfig index de4966e..204f58d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,7 @@ root = true # All files [*] charset = utf-8 +end_of_line = lf indent_style = space indent_size = 4 insert_final_newline = true diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0064b81..babb67d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -351,14 +351,12 @@ jobs: contents: read steps: + # === CHECKOUT AND DOWNLOAD ARTIFACTS === + # This section retrieves the code and all necessary artifacts from previous jobs. + - name: Checkout uses: actions/checkout@v6 - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 'lts/*' - - name: Download all test results uses: actions/download-artifact@v7 with: @@ -385,6 +383,14 @@ jobs: pattern: 'version-capture-*' continue-on-error: true + # === INSTALL DEPENDENCIES === + # This section installs all required dependencies and tools for document generation. + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 'lts/*' + - name: Setup dotnet uses: actions/setup-dotnet@v5 with: @@ -405,6 +411,9 @@ jobs: - name: Restore Tools run: dotnet tool restore + # === CAPTURE TOOL VERSIONS === + # This section captures the versions of all tools used in the build process. + - name: Capture tool versions for build-docs shell: bash run: | @@ -413,6 +422,9 @@ jobs: dotnet git node npm pandoc weasyprint sarifmark reqstream buildmark versionmark echo "✓ Tool versions captured" + # === GENERATE MARKDOWN REPORTS === + # This section generates all markdown reports from various tools and sources. + - name: Generate Requirements Report and Trace Matrix run: > dotnet reqstream @@ -438,70 +450,6 @@ jobs: echo "=== CodeQL Quality Report ===" cat docs/quality/codeql-quality.md - - name: Generate HTML with Pandoc - run: > - dotnet pandoc - --defaults docs/guide/definition.yaml - --filter node_modules/.bin/mermaid-filter.cmd - --metadata version="${{ inputs.version }}" - --metadata date="$(date +'%Y-%m-%d')" - --output docs/guide/guide.html - - - name: Generate PDF with Weasyprint - run: > - dotnet weasyprint - --pdf-variant pdf/a-3u - docs/guide/guide.html - "docs/SonarMark User Guide.pdf" - - - name: Generate Requirements HTML with Pandoc - run: > - dotnet pandoc - --defaults docs/requirements/definition.yaml - --filter node_modules/.bin/mermaid-filter.cmd - --metadata version="${{ inputs.version }}" - --metadata date="$(date +'%Y-%m-%d')" - --output docs/requirements/requirements.html - - - name: Generate Requirements PDF with Weasyprint - run: > - dotnet weasyprint - --pdf-variant pdf/a-3u - docs/requirements/requirements.html - "docs/SonarMark Requirements.pdf" - - - name: Generate Trace Matrix HTML with Pandoc - run: > - dotnet pandoc - --defaults docs/tracematrix/definition.yaml - --filter node_modules/.bin/mermaid-filter.cmd - --metadata version="${{ inputs.version }}" - --metadata date="$(date +'%Y-%m-%d')" - --output docs/tracematrix/tracematrix.html - - - name: Generate Trace Matrix PDF with Weasyprint - run: > - dotnet weasyprint - --pdf-variant pdf/a-3u - docs/tracematrix/tracematrix.html - "docs/SonarMark Trace Matrix.pdf" - - - name: Generate Justifications HTML with Pandoc - run: > - dotnet pandoc - --defaults docs/justifications/definition.yaml - --filter node_modules/.bin/mermaid-filter.cmd - --metadata version="${{ inputs.version }}" - --metadata date="$(date +'%Y-%m-%d')" - --output docs/justifications/justifications.html - - - name: Generate Justifications PDF with Weasyprint - run: > - dotnet weasyprint - --pdf-variant pdf/a-3u - docs/justifications/justifications.html - "docs/SonarMark Requirements Justifications.pdf" - - name: Generate Code Quality Report with SonarMark shell: bash env: @@ -536,11 +484,6 @@ jobs: run: | echo "=== Build Notes Report ===" cat docs/buildnotes.md - # Verify buildnotes.md was created - if [ ! -f "docs/buildnotes.md" ]; then - echo "Error: buildnotes.md was not created" - exit 1 - fi - name: Publish Tool Versions shell: bash @@ -556,6 +499,45 @@ jobs: echo "=== Tool Versions Report ===" cat docs/buildnotes/versions.md + # === GENERATE HTML DOCUMENTS WITH PANDOC === + # This section converts markdown documents to HTML using Pandoc. + + - name: Generate Guide HTML with Pandoc + run: > + dotnet pandoc + --defaults docs/guide/definition.yaml + --filter node_modules/.bin/mermaid-filter.cmd + --metadata version="${{ inputs.version }}" + --metadata date="$(date +'%Y-%m-%d')" + --output docs/guide/guide.html + + - name: Generate Requirements HTML with Pandoc + run: > + dotnet pandoc + --defaults docs/requirements/definition.yaml + --filter node_modules/.bin/mermaid-filter.cmd + --metadata version="${{ inputs.version }}" + --metadata date="$(date +'%Y-%m-%d')" + --output docs/requirements/requirements.html + + - name: Generate Trace Matrix HTML with Pandoc + run: > + dotnet pandoc + --defaults docs/tracematrix/definition.yaml + --filter node_modules/.bin/mermaid-filter.cmd + --metadata version="${{ inputs.version }}" + --metadata date="$(date +'%Y-%m-%d')" + --output docs/tracematrix/tracematrix.html + + - name: Generate Justifications HTML with Pandoc + run: > + dotnet pandoc + --defaults docs/justifications/definition.yaml + --filter node_modules/.bin/mermaid-filter.cmd + --metadata version="${{ inputs.version }}" + --metadata date="$(date +'%Y-%m-%d')" + --output docs/justifications/justifications.html + - name: Generate Build Notes HTML with Pandoc shell: bash run: > @@ -566,13 +548,6 @@ jobs: --filter node_modules/.bin/mermaid-filter.cmd --output docs/buildnotes/buildnotes.html - - name: Convert Build Notes HTML to PDF with Weasyprint - run: > - dotnet weasyprint - --pdf-variant pdf/a-3u - docs/buildnotes/buildnotes.html - "docs/SonarMark Build Notes.pdf" - - name: Generate Code Quality HTML with Pandoc run: > dotnet pandoc @@ -582,6 +557,44 @@ jobs: --metadata date="$(date +'%Y-%m-%d')" --output docs/quality/quality.html + # === GENERATE PDF DOCUMENTS WITH WEASYPRINT === + # This section converts HTML documents to PDF using Weasyprint. + + - name: Generate Guide PDF with Weasyprint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/guide/guide.html + "docs/SonarMark User Guide.pdf" + + - name: Generate Requirements PDF with Weasyprint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/requirements/requirements.html + "docs/SonarMark Requirements.pdf" + + - name: Generate Trace Matrix PDF with Weasyprint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/tracematrix/tracematrix.html + "docs/SonarMark Trace Matrix.pdf" + + - name: Generate Justifications PDF with Weasyprint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/justifications/justifications.html + "docs/SonarMark Requirements Justifications.pdf" + + - name: Generate Build Notes PDF with Weasyprint + run: > + dotnet weasyprint + --pdf-variant pdf/a-3u + docs/buildnotes/buildnotes.html + "docs/SonarMark Build Notes.pdf" + - name: Generate Code Quality PDF with Weasyprint run: > dotnet weasyprint @@ -589,6 +602,9 @@ jobs: docs/quality/quality.html "docs/SonarMark Code Quality.pdf" + # === UPLOAD ARTIFACTS === + # This section uploads all generated documentation artifacts. + - name: Upload documentation uses: actions/upload-artifact@v6 with: diff --git a/.github/workflows/build_on_push.yaml b/.github/workflows/build_on_push.yaml index bba8e61..d536b8e 100644 --- a/.github/workflows/build_on_push.yaml +++ b/.github/workflows/build_on_push.yaml @@ -11,10 +11,10 @@ jobs: build: name: Build permissions: - actions: read # To read workflow artifacts - contents: read # To read repository contents - pull-requests: write # To write pull requests analysis results and artifacts - security-events: write # To upload CodeQL results + actions: read + contents: read + pull-requests: write + security-events: write uses: ./.github/workflows/build.yaml with: version: 0.0.0-run.${{ github.run_number }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c12e889..bf33269 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,19 +18,23 @@ on: - publish jobs: + # Calls the reusable build workflow to build, test, and generate documentation + # for the release version. build: name: Build permissions: - actions: read # To read workflow artifacts - contents: read # To read repository contents - pull-requests: write # To write pull requests analysis results and artifacts - security-events: write # To upload CodeQL results + actions: read + contents: read + pull-requests: write + security-events: write uses: ./.github/workflows/build.yaml with: version: ${{ inputs.version }} secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # Cuts and publishes the release, creating a GitHub release with artifacts + # and optionally publishing the NuGet package. release: name: Release runs-on: ubuntu-latest @@ -62,10 +66,6 @@ jobs: - name: Move buildnotes.md to root run: | set -e - if [ ! -f "artifacts/buildnotes.md" ]; then - echo "Error: buildnotes.md not found in artifacts" - exit 1 - fi mv artifacts/buildnotes.md buildnotes.md - name: Create GitHub Release diff --git a/.gitignore b/.gitignore index 8858df2..7f7eaf2 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,8 @@ docs/justifications/*.html docs/quality/sonar-quality.md docs/quality/codeql-quality.md docs/quality/*.html +docs/buildnotes.md docs/buildnotes/versions.md + +# VersionMark captures (generated during CI/CD) +versionmark-*.json diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 40c7607..a46ee1a 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -1,14 +1,13 @@ { - // Markdown linting configuration for markdownlint-cli2 "config": { "default": true, "MD003": { "style": "atx" }, "MD007": { "indent": 2 }, "MD013": { "line_length": 120 }, + "MD025": false, "MD033": false, "MD041": false }, - // Ignore patterns "ignores": [ "node_modules", "**/AGENT_REPORT_*.md" diff --git a/.yamllint.yaml b/.yamllint.yaml index 0d2f69f..32813ff 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -4,6 +4,11 @@ extends: default +# Ignore node_modules and other generated/third-party directories +ignore: | + node_modules/ + .git/ + rules: # Allow 'on:' in GitHub Actions workflows (not a boolean value) truthy: diff --git a/AGENTS.md b/AGENTS.md index c5720e5..b26f912 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,10 +26,28 @@ SonarQube/SonarCloud analysis results. ## Requirements (SonarMark-Specific) - All requirements MUST be linked to tests (prefer integration tests over unit tests) -- Not all tests need to be linked to requirements (tests may exist for corner cases, design testing, failure-testing, etc.) +- Not all tests need to be linked to requirements (tests may exist for corner cases, design testing, + failure-testing, etc.) - Enforced in CI: `dotnet reqstream --requirements requirements.yaml --tests "test-results/**/*.trx" --enforce` - When adding features: add requirement + link to test +## Test Source Filters + +Test links in `requirements.yaml` can include a source filter prefix to restrict which test results count as +evidence. This is critical for platform and framework requirements - **do not remove these filters**. + +- `windows@TestName` - proves the test passed on a Windows platform +- `ubuntu@TestName` - proves the test passed on a Linux (Ubuntu) platform +- `net8.0@TestName` - proves the test passed under the .NET 8 target framework +- `net9.0@TestName` - proves the test passed under the .NET 9 target framework +- `net10.0@TestName` - proves the test passed under the .NET 10 target framework +- `dotnet8.x@TestName` - proves the self-validation test ran on a machine with .NET 8.x runtime +- `dotnet9.x@TestName` - proves the self-validation test ran on a machine with .NET 9.x runtime +- `dotnet10.x@TestName` - proves the self-validation test ran on a machine with .NET 10.x runtime + +Without the source filter, a test result from any platform/framework satisfies the requirement. Adding the +filter ensures the CI evidence comes specifically from the required environment. + ## Testing (SonarMark-Specific) - **Test Naming**: `ClassName_MethodUnderTest_Scenario_ExpectedBehavior` (for requirements traceability) @@ -44,10 +62,12 @@ SonarQube/SonarCloud analysis results. ## Linting (SonarMark-Specific) -- **AI agent markdown files** (`.github/agents/*.md`): Use inline links `[text](url)` so URLs are visible in agent context +- **AI agent markdown files** (`.github/agents/*.md`): Use inline links `[text](url)` so URLs are visible in + agent context - **README.md**: Absolute URLs only (shipped in NuGet package) - **Other .md**: Reference-style links `[text][ref]` with `[ref]: url` at end -- **All linters must pass locally**: markdownlint, cspell, yamllint (see `.vscode/tasks.json` or CI workflows) +- **All linters must pass locally**: markdownlint, cspell, yamllint (see `.vscode/tasks.json` or CI + workflows) ## Build & Quality (Quick Reference) diff --git a/README.md b/README.md index d0f0c0d..dbebff4 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # SonarMark -[![GitHub forks](https://img.shields.io/github/forks/demaconsulting/SonarMark?style=plastic)](https://github.com/demaconsulting/SonarMark/network/members) -[![GitHub stars](https://img.shields.io/github/stars/demaconsulting/SonarMark?style=plastic)](https://github.com/demaconsulting/SonarMark/stargazers) -[![GitHub contributors](https://img.shields.io/github/contributors/demaconsulting/SonarMark?style=plastic)](https://github.com/demaconsulting/SonarMark/graphs/contributors) -[![License](https://img.shields.io/github/license/demaconsulting/SonarMark?style=plastic)](https://github.com/demaconsulting/SonarMark/blob/main/LICENSE) -[![Build](https://img.shields.io/github/actions/workflow/status/demaconsulting/SonarMark/build_on_push.yaml)](https://github.com/demaconsulting/SonarMark/actions/workflows/build_on_push.yaml) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=demaconsulting_SonarMark&metric=alert_status)](https://sonarcloud.io/dashboard?id=demaconsulting_SonarMark) -[![Security](https://sonarcloud.io/api/project_badges/measure?project=demaconsulting_SonarMark&metric=security_rating)](https://sonarcloud.io/dashboard?id=demaconsulting_SonarMark) -[![NuGet](https://img.shields.io/nuget/v/DemaConsulting.SonarMark?style=plastic)](https://www.nuget.org/packages/DemaConsulting.SonarMark) +[![GitHub forks][badge-forks]][link-forks] +[![GitHub stars][badge-stars]][link-stars] +[![GitHub contributors][badge-contributors]][link-contributors] +[![License][badge-license]][link-license] +[![Build][badge-build]][link-build] +[![Quality Gate][badge-quality]][link-quality] +[![Security][badge-security]][link-security] +[![NuGet][badge-nuget]][link-nuget] Code Quality Reporting Tool for SonarQube/SonarCloud @@ -209,12 +209,13 @@ Please see our [Contributing Guide](https://github.com/demaconsulting/SonarMark/ development setup, coding standards, and submission guidelines. Also review our [Code of Conduct](https://github.com/demaconsulting/SonarMark/blob/main/CODE_OF_CONDUCT.md) for community guidelines. -For bug reports, feature requests, and questions, please use [GitHub Issues](https://github.com/demaconsulting/SonarMark/issues). +For bug reports, feature requests, and questions, please use +[GitHub Issues](https://github.com/demaconsulting/SonarMark/issues). ## License -This project is licensed under the MIT License - see the [LICENSE](https://github.com/demaconsulting/SonarMark/blob/main/LICENSE) -file for details. +This project is licensed under the MIT License - see the +[LICENSE](https://github.com/demaconsulting/SonarMark/blob/main/LICENSE) file for details. ## Support @@ -237,3 +238,23 @@ SonarMark is built with the following open-source projects: - [SonarQube](https://www.sonarqube.org/) - Continuous code quality inspection - [SonarCloud](https://sonarcloud.io/) - Cloud-based code quality and security service - [DemaConsulting.TestResults](https://github.com/demaconsulting/TestResults) - Test results parsing library + + +[badge-forks]: https://img.shields.io/github/forks/demaconsulting/SonarMark?style=plastic +[badge-stars]: https://img.shields.io/github/stars/demaconsulting/SonarMark?style=plastic +[badge-contributors]: https://img.shields.io/github/contributors/demaconsulting/SonarMark?style=plastic +[badge-license]: https://img.shields.io/github/license/demaconsulting/SonarMark?style=plastic +[badge-build]: https://img.shields.io/github/actions/workflow/status/demaconsulting/SonarMark/build_on_push.yaml?style=plastic +[badge-quality]: https://sonarcloud.io/api/project_badges/measure?project=demaconsulting_SonarMark&metric=alert_status +[badge-security]: https://sonarcloud.io/api/project_badges/measure?project=demaconsulting_SonarMark&metric=security_rating +[badge-nuget]: https://img.shields.io/nuget/v/DemaConsulting.SonarMark?style=plastic + + +[link-forks]: https://github.com/demaconsulting/SonarMark/network/members +[link-stars]: https://github.com/demaconsulting/SonarMark/stargazers +[link-contributors]: https://github.com/demaconsulting/SonarMark/graphs/contributors +[link-license]: https://github.com/demaconsulting/SonarMark/blob/main/LICENSE +[link-build]: https://github.com/demaconsulting/SonarMark/actions/workflows/build_on_push.yaml +[link-quality]: https://sonarcloud.io/dashboard?id=demaconsulting_SonarMark +[link-security]: https://sonarcloud.io/dashboard?id=demaconsulting_SonarMark +[link-nuget]: https://www.nuget.org/packages/DemaConsulting.SonarMark diff --git a/docs/guide/guide.md b/docs/guide/guide.md index 62372a2..c56c3f9 100644 --- a/docs/guide/guide.md +++ b/docs/guide/guide.md @@ -1,15 +1,27 @@ -# SonarMark Usage Guide +# Introduction -This guide provides comprehensive documentation for using SonarMark to generate code quality reports from -SonarQube/SonarCloud analysis results. +This document provides comprehensive documentation for using SonarMark to generate +code quality reports from SonarQube/SonarCloud analysis results. -## Introduction +## Purpose -SonarMark is a .NET command-line tool that fetches quality gate status, issues, and security hot-spots from -SonarQube/SonarCloud and generates comprehensive markdown reports. It's designed to integrate seamlessly into CI/CD -pipelines for automated quality reporting. +SonarMark is a .NET command-line tool that fetches quality gate status, issues, and +security hot-spots from SonarQube/SonarCloud and generates comprehensive markdown +reports. It is designed to integrate seamlessly into CI/CD pipelines for automated +quality reporting. -### Key Features +## Scope + +This user guide covers: + +- Installation and setup of SonarMark +- Command-line options and usage +- Report generation and format +- CI/CD integration patterns +- Self-validation testing +- Troubleshooting common issues + +## Key Features - **Quality Gate Reporting**: Fetch and report quality gate status with detailed conditions - **Issue Analysis**: Retrieve and categorize issues by type and severity @@ -18,13 +30,13 @@ pipelines for automated quality reporting. - **CI/CD Integration**: Support for enforcement mode to fail builds on quality gate failures - **Multi-Platform**: Works on Windows, Linux, and macOS with .NET 8, 9, or 10 -## Installation +# Installation -### Prerequisites +## Prerequisites - [.NET SDK][dotnet-download] 8.0, 9.0, or 10.0 -### Global Installation +## Global Installation Install SonarMark as a global .NET tool for system-wide access: @@ -38,7 +50,7 @@ Verify the installation: sonarmark --version ``` -### Local Installation +## Local Installation For team projects, install SonarMark as a local tool to ensure version consistency: @@ -56,7 +68,7 @@ Run the locally installed tool: dotnet sonarmark --version ``` -### Update +## Update To update to the latest version: @@ -68,9 +80,9 @@ dotnet tool update --global DemaConsulting.SonarMark dotnet tool update DemaConsulting.SonarMark ``` -## Getting Started +# Getting Started -### Basic Usage +## Basic Usage The most basic usage requires specifying the SonarQube/SonarCloud server URL and project key: @@ -78,7 +90,7 @@ The most basic usage requires specifying the SonarQube/SonarCloud server URL and sonarmark --server https://sonarcloud.io --project-key my-org_my-project ``` -### With Authentication +## With Authentication For private projects, provide an authentication token: @@ -88,7 +100,7 @@ sonarmark --server https://sonarcloud.io \ --token $SONAR_TOKEN ``` -### Generating a Report +## Generating a Report To generate a markdown report file: @@ -99,11 +111,11 @@ sonarmark --server https://sonarcloud.io \ --report quality-report.md ``` -## Command-Line Options +# Command-Line Options -### Display Options +## Display Options -#### `--version`, `-v` +### `--version`, `-v` Display version information and exit. @@ -111,7 +123,7 @@ Display version information and exit. sonarmark --version ``` -#### `--help`, `-h`, `-?` +### `--help`, `-h`, `-?` Display help message with all available options. @@ -119,9 +131,9 @@ Display help message with all available options. sonarmark --help ``` -### Output Control +## Output Control -#### `--silent` +### `--silent` Suppress console output. Useful in automated scripts where only the exit code matters. @@ -131,7 +143,7 @@ sonarmark --server https://sonarcloud.io \ --silent ``` -#### `--log ` +### `--log ` Write all output to a log file in addition to console output. @@ -141,9 +153,9 @@ sonarmark --server https://sonarcloud.io \ --log analysis.log ``` -### SonarQube/SonarCloud Connection +## SonarQube/SonarCloud Connection -#### `--server ` (Required) +### `--server ` (Required) SonarQube or SonarCloud server URL. @@ -157,7 +169,7 @@ Examples: --server https://sonar.example.com ``` -#### `--project-key ` (Required) +### `--project-key ` (Required) Project key in SonarQube/SonarCloud. This is the unique identifier for your project. @@ -165,7 +177,7 @@ Project key in SonarQube/SonarCloud. This is the unique identifier for your proj --project-key my-organization_my-project ``` -#### `--branch ` +### `--branch ` Branch name to query. If not specified, uses the main branch configured in SonarQube/SonarCloud. @@ -176,7 +188,7 @@ sonarmark --server https://sonarcloud.io \ --branch feature/new-feature ``` -#### `--token ` +### `--token ` Personal access token for authentication. Can also be provided via the `SONAR_TOKEN` environment variable. @@ -193,9 +205,9 @@ sonarmark --server https://sonarcloud.io \ --token $SONAR_TOKEN ``` -### Report Generation +## Report Generation -#### `--report ` +### `--report ` Export quality results to a markdown file. The file will contain quality gate status, conditions, issues, and security hot-spots. @@ -206,7 +218,7 @@ sonarmark --server https://sonarcloud.io \ --report quality-report.md ``` -#### `--report-depth ` +### `--report-depth ` Set the markdown header depth for the report. Default is 1. Use this when embedding the report in larger documents. @@ -218,9 +230,9 @@ sonarmark --server https://sonarcloud.io \ --report-depth 2 ``` -### Quality Enforcement +## Quality Enforcement -#### `--enforce` +### `--enforce` Return a non-zero exit code if the quality gate fails. Essential for CI/CD pipelines to fail builds on quality issues. @@ -230,9 +242,9 @@ sonarmark --server https://sonarcloud.io \ --enforce ``` -### Self-Validation +## Self-Validation -#### `--validate` +### `--validate` Run built-in self-validation tests. These tests verify SonarMark functionality without requiring access to a real SonarQube/SonarCloud server. @@ -241,7 +253,7 @@ SonarQube/SonarCloud server. sonarmark --validate ``` -#### `--results ` +### `--results ` Write validation results to a file. Supports TRX (`.trx`) and JUnit XML (`.xml`) formats. Requires `--validate`. @@ -253,9 +265,9 @@ sonarmark --validate --results validation-results.trx sonarmark --validate --results validation-results.xml ``` -## Common Use Cases +# Common Use Cases -### CI/CD Integration +## CI/CD Integration Integrate SonarMark into your CI/CD pipeline to automatically check code quality: @@ -278,7 +290,7 @@ Integrate SonarMark into your CI/CD pipeline to automatically check code quality path: quality-report.md ``` -### Branch Analysis +## Branch Analysis Analyze specific branches during pull request reviews: @@ -291,7 +303,7 @@ sonarmark --server https://sonarcloud.io \ --report pr-quality.md ``` -### Quality Gate Monitoring +## Quality Gate Monitoring Monitor quality gate status without generating a full report: @@ -304,7 +316,7 @@ sonarmark --server https://sonarcloud.io \ --silent ``` -### Automated Reporting +## Automated Reporting Generate daily/weekly quality reports: @@ -319,11 +331,11 @@ sonarmark --server https://sonarcloud.io \ --log "analysis-${TIMESTAMP}.log" ``` -## Report Format +# Report Format The generated markdown report includes the following sections: -### Project Header +## Project Header The report begins with the project name and a link to the SonarQube/SonarCloud dashboard: @@ -333,7 +345,7 @@ The report begins with the project name and a link to the SonarQube/SonarCloud d **Dashboard:** ``` -### Quality Gate Status +## Quality Gate Status Shows whether the project passed or failed the quality gate. Possible values are OK, ERROR, WARN, or NONE: @@ -347,7 +359,7 @@ or **Quality Gate Status:** ERROR ``` -### Conditions +## Conditions If quality gate conditions exist, they are displayed in a table with the following columns: @@ -367,7 +379,7 @@ If quality gate conditions exist, they are displayed in a table with the followi | Duplications | OK | LT | 3 | 2.1 | ``` -### Issues +## Issues The issues section shows a count of issues found and lists each issue in compiler-style format: @@ -397,7 +409,7 @@ If no issues are found: Found no issues ``` -### Security Hot-Spots +## Security Hot-Spots The security hot-spots section shows a count and lists each hot-spot in compiler-style format: @@ -425,18 +437,18 @@ If no security hot-spots are found: Found no security hot-spots ``` -## Running Self-Validation +# Running Self-Validation SonarMark includes built-in self-validation tests to verify functionality without requiring access to a real SonarQube/SonarCloud server. The validation uses mock data to test core features. -### Running Validation +## Running Validation ```bash sonarmark --validate ``` -### Validation Tests +## Validation Tests The self-validation suite includes the following tests that verify core functionality: @@ -457,7 +469,7 @@ These tests provide evidence of the tool's functionality and are particularly us **Note**: The test names with the `SonarMark_` prefix are designed for clear identification in test result files (TRX/JUnit) when integrating with larger projects or test frameworks. -### Validation Output +## Validation Output Example output: @@ -478,7 +490,7 @@ Passed: 4 Failed: 0 ``` -### Saving Validation Results +## Saving Validation Results Save results in TRX or JUnit XML format for integration with test reporting tools: @@ -490,37 +502,37 @@ sonarmark --validate --results validation-results.trx sonarmark --validate --results validation-results.xml ``` -## Best Practices +# Best Practices -### Authentication +## Authentication - **Store tokens securely**: Use environment variables or secret management systems - **Rotate tokens regularly**: Follow your organization's security policies - **Use read-only tokens**: SonarMark only needs read access to the API - **Don't commit tokens**: Never commit tokens to version control -### CI/CD Best Practices +## CI/CD Best Practices - **Use enforcement mode**: Always use `--enforce` in CI/CD to fail builds on quality gate failures - **Archive reports**: Save quality reports as build artifacts for historical tracking - **Set timeouts**: Configure reasonable timeouts for API calls in CI/CD environments - **Handle failures gracefully**: Use appropriate error handling in your CI/CD scripts -### Report Best Practices +## Report Best Practices - **Use meaningful filenames**: Include timestamps, branch names, or build numbers in report filenames - **Adjust header depth**: Use `--report-depth` when embedding reports in larger documents - **Combine with logging**: Use `--log` to capture detailed execution information -### Performance +## Performance - **Cache dependencies**: Use local tool installation to speed up execution in CI/CD - **Minimize API calls**: Only fetch data when needed (e.g., don't generate reports if not required) - **Use silent mode**: Suppress unnecessary output in automated scripts with `--silent` -## Troubleshooting +# Troubleshooting -### Authentication Issues +## Authentication Issues **Problem**: `401 Unauthorized` error @@ -531,7 +543,7 @@ sonarmark --validate --results validation-results.xml - Check if the project exists and you have access to it - For SonarCloud, verify you're using a user token, not a project analysis token -### Connection Issues +## Connection Issues **Problem**: Cannot connect to SonarQube/SonarCloud server @@ -543,7 +555,7 @@ sonarmark --validate --results validation-results.xml - Verify SSL/TLS certificates are valid - For self-hosted SonarQube, check if the server is running -### Project Not Found +## Project Not Found **Problem**: `404 Not Found` or project doesn't exist error @@ -554,7 +566,7 @@ sonarmark --validate --results validation-results.xml - Check if you have access to the project - For branches, verify the branch exists in SonarQube/SonarCloud -### Branch Issues +## Branch Issues **Problem**: Branch not found or incorrect data @@ -565,7 +577,7 @@ sonarmark --validate --results validation-results.xml - Check if the branch is a long-lived or short-lived branch - Use the exact branch name as shown in SonarQube/SonarCloud UI -### Quality Gate Failures +## Quality Gate Failures **Problem**: Quality gate fails unexpectedly with `--enforce` @@ -576,7 +588,7 @@ sonarmark --validate --results validation-results.xml - Verify quality gate configuration in SonarQube/SonarCloud - Consider if the failure is expected (e.g., new issues introduced) -### Report Generation Issues +## Report Generation Issues **Problem**: Report file is not generated or is empty @@ -587,7 +599,7 @@ sonarmark --validate --results validation-results.xml - Ensure there's enough disk space - Check the log output for specific error messages -### Validation Failures +## Validation Failures **Problem**: Self-validation tests fail @@ -597,7 +609,7 @@ sonarmark --validate --results validation-results.xml - Check if there are any known issues in the GitHub repository - Report the issue with full validation output if problem persists -### Performance Issues +## Performance Issues **Problem**: SonarMark takes too long to execute @@ -608,7 +620,7 @@ sonarmark --validate --results validation-results.xml - Consider caching results if running frequently - For large projects, be patient as data retrieval may take time -### Exit Codes +## Exit Codes SonarMark uses the following exit codes: @@ -629,7 +641,7 @@ else fi ``` -## Additional Resources +# Additional Resources - [GitHub Repository][github] - [Issue Tracker][issues] diff --git a/docs/tracematrix/introduction.md b/docs/tracematrix/introduction.md index 1aff9ec..d901022 100644 --- a/docs/tracematrix/introduction.md +++ b/docs/tracematrix/introduction.md @@ -7,6 +7,15 @@ This document contains the requirements trace matrix for the SonarMark project. The trace matrix links requirements to their corresponding test cases, ensuring complete test coverage and traceability from requirements to implementation. +## Scope + +This trace matrix document covers: + +- Linkage between requirements and test cases +- Test coverage status for each requirement +- Both unit/integration tests and self-validation tests +- Platform-specific test execution results + ## Test Sources Requirements traceability in SonarMark uses two types of tests: diff --git a/requirements.yaml b/requirements.yaml index d67b312..3a4b494 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -1,16 +1,32 @@ --- # SonarMark Requirements # -# Test Execution Strategy: -# This file links requirements to tests across multiple execution contexts: -# - Unit tests: Run locally via 'dotnet test' (validates most requirements) -# - Self-validation tests: Run via 'sonarmark --validate' in CI integration-test job -# (validates SONAR-003, SONAR-004, SONAR-005, RPT-001, VAL-001) -# - Platform tests: Run across OS/runtime matrix in CI integration-test job -# (validates PLT-001 through PLT-005) +# This project uses three categories of tests to verify requirements: +# +# 1. Unit Tests - Run locally via "dotnet test" +# 2. Self-Validation Tests - Run locally via "--validate" +# 3. Platform Tests - Run via CI/CD across OS/runtime matrix +# +# NOTE: Running "reqstream --enforce" with only local test results (unit tests +# and local self-validation) is expected to show some unsatisfied requirements. +# Platform-specific requirements require test results from CI/CD runs across +# the full OS and runtime matrix. +# +# Test links can include a source filter prefix (e.g. "windows@", "ubuntu@", "net8.0@", +# "dotnet8.x@") to restrict which test results count as evidence for a requirement. This +# is critical for platform and framework requirements - removing these filters invalidates +# the evidence-based proof. +# +# Source filter prefixes: +# windows@TestName - proves the test passed on a Windows platform +# ubuntu@TestName - proves the test passed on a Linux (Ubuntu) platform +# net8.0@TestName - proves the test passed under the .NET 8 target framework +# net9.0@TestName - proves the test passed under the .NET 9 target framework +# net10.0@TestName - proves the test passed under the .NET 10 target framework +# dotnet8.x@TestName - proves the self-validation test ran with .NET 8.x runtime +# dotnet9.x@TestName - proves the self-validation test ran with .NET 9.x runtime +# dotnet10.x@TestName - proves the self-validation test ran with .NET 10.x runtime # -# Note: Running 'dotnet reqstream --enforce' locally will show some unsatisfied requirements. -# This is expected - full validation requires the complete CI pipeline. sections: - title: SonarMark Requirements sections: diff --git a/src/DemaConsulting.SonarMark/Context.cs b/src/DemaConsulting.SonarMark/Context.cs index af9ec0a..8ca31ed 100644 --- a/src/DemaConsulting.SonarMark/Context.cs +++ b/src/DemaConsulting.SonarMark/Context.cs @@ -132,9 +132,11 @@ public static Context Create(string[] args) /// Thrown when arguments are invalid. public static Context Create(string[] args, Func? httpClientFactory) { + // Parse command-line arguments into structured form var parser = new ArgumentParser(); parser.ParseArguments(args); + // Create context with parsed arguments var result = new Context { Version = parser.Version, @@ -171,7 +173,9 @@ private void OpenLogFile(string logFile) { _logWriter = new StreamWriter(logFile, append: false); } - // Generic catch is justified here to wrap any file system exception with context + // Generic catch is justified here to wrap any file system exception with context. + // Expected exceptions include IOException, UnauthorizedAccessException, ArgumentException, + // NotSupportedException, and other file system-related exceptions. catch (Exception ex) { throw new InvalidOperationException($"Failed to open log file '{logFile}': {ex.Message}", ex); @@ -183,25 +187,78 @@ private void OpenLogFile(string logFile) /// private sealed class ArgumentParser { + /// + /// Gets a value indicating whether the version flag was specified. + /// public bool Version { get; private set; } + + /// + /// Gets a value indicating whether the help flag was specified. + /// public bool Help { get; private set; } + + /// + /// Gets a value indicating whether the silent flag was specified. + /// public bool Silent { get; private set; } + + /// + /// Gets a value indicating whether the validate flag was specified. + /// public bool Validate { get; private set; } + + /// + /// Gets a value indicating whether the enforce flag was specified. + /// public bool Enforce { get; private set; } + + /// + /// Gets the report file path. + /// public string? ReportFile { get; private set; } + + /// + /// Gets the report markdown depth. + /// public int ReportDepth { get; private set; } = 1; + + /// + /// Gets the personal access token for SonarQube/SonarCloud authentication. + /// public string? Token { get; private set; } + + /// + /// Gets the SonarQube/SonarCloud server URL. + /// public string? Server { get; private set; } + + /// + /// Gets the SonarQube/SonarCloud project key. + /// public string? ProjectKey { get; private set; } + + /// + /// Gets the branch name to query. + /// public string? Branch { get; private set; } + + /// + /// Gets the log file path. + /// public string? LogFile { get; private set; } + + /// + /// Gets the validation results file path. + /// public string? ResultsFile { get; private set; } /// /// Parses command-line arguments /// + /// Command-line arguments. public void ParseArguments(string[] args) { + // Iterate through all arguments, processing each one int i = 0; while (i < args.Length) { @@ -288,6 +345,7 @@ private int ParseArgument(string arg, string[] args, int index) /// All arguments /// Current index /// Description of what's required + /// Argument value private static string GetRequiredStringArgument(string arg, string[] args, int index, string description) { if (index >= args.Length) @@ -301,6 +359,10 @@ private static string GetRequiredStringArgument(string arg, string[] args, int i /// /// Gets a required positive integer argument value /// + /// Argument name + /// All arguments + /// Current index + /// Argument value private static int GetRequiredIntArgument(string arg, string[] args, int index) { if (index >= args.Length) diff --git a/src/DemaConsulting.SonarMark/Program.cs b/src/DemaConsulting.SonarMark/Program.cs index 43ee4b0..8c49963 100644 --- a/src/DemaConsulting.SonarMark/Program.cs +++ b/src/DemaConsulting.SonarMark/Program.cs @@ -154,19 +154,21 @@ private static void PrintHelp(Context context) /// The context containing command line arguments and program state. private static void ProcessSonarAnalysis(Context context) { - // Validate required parameters + // Validate that required server parameter is provided if (string.IsNullOrWhiteSpace(context.Server)) { context.WriteError("Error: --server parameter is required"); return; } + // Validate that required project key parameter is provided if (string.IsNullOrWhiteSpace(context.ProjectKey)) { context.WriteError("Error: --project-key parameter is required"); return; } + // Display configuration information context.WriteLine($"Server: {context.Server}"); context.WriteLine($"Project Key: {context.ProjectKey}"); if (!string.IsNullOrWhiteSpace(context.Branch)) @@ -174,10 +176,11 @@ private static void ProcessSonarAnalysis(Context context) context.WriteLine($"Branch: {context.Branch}"); } - // Get quality results from SonarQube/SonarCloud + // Create SonarQube client using factory or default constructor context.WriteLine("Fetching quality results from server..."); using var client = context.HttpClientFactory?.Invoke(context.Token) ?? new SonarQubeClient(context.Token); + // Fetch quality results from SonarQube/SonarCloud server SonarQualityResult qualityResult; try { @@ -185,6 +188,8 @@ private static void ProcessSonarAnalysis(Context context) context.Server, context.ProjectKey, context.Branch).GetAwaiter().GetResult(); + + // Display quality gate status and issue counts context.WriteLine($"Quality Gate Status: {qualityResult.QualityGateStatus}"); context.WriteLine($"Issues: {qualityResult.Issues.Count}"); context.WriteLine($"Hot-Spots: {qualityResult.HotSpots.Count}"); @@ -195,13 +200,13 @@ private static void ProcessSonarAnalysis(Context context) return; } - // Check enforcement if requested + // Check quality gate enforcement if requested if (context.Enforce && qualityResult.QualityGateStatus == "ERROR") { context.WriteError("Error: Quality gate failed"); } - // Export quality report if requested + // Generate markdown report if requested if (context.ReportFile != null) { context.WriteLine($"Writing quality report to {context.ReportFile}..."); diff --git a/src/DemaConsulting.SonarMark/SonarQualityResult.cs b/src/DemaConsulting.SonarMark/SonarQualityResult.cs index e0f2b34..6c7aad5 100644 --- a/src/DemaConsulting.SonarMark/SonarQualityResult.cs +++ b/src/DemaConsulting.SonarMark/SonarQualityResult.cs @@ -49,16 +49,19 @@ internal sealed record SonarQualityResult( /// Thrown when depth is not between 1 and 6 public string ToMarkdown(int depth) { + // Validate that depth is in valid range for markdown headers if (depth < 1 || depth > 6) { throw new ArgumentOutOfRangeException(nameof(depth), depth, "Depth must be between 1 and 6"); } + // Calculate heading strings based on requested depth var heading = new string('#', depth); var subHeadingDepth = Math.Min(depth + 1, 6); var subHeading = new string('#', subHeadingDepth); var sb = new System.Text.StringBuilder(); + // Build markdown document by appending all sections AppendHeader(sb, heading); AppendConditionsSection(sb, subHeading); AppendIssuesSection(sb, subHeading); @@ -70,6 +73,8 @@ public string ToMarkdown(int depth) /// /// Appends the header section with project name, dashboard link, and quality gate status /// + /// String builder to append to + /// Heading prefix (e.g., "#" or "##") private void AppendHeader(System.Text.StringBuilder sb, string heading) { // Add project name as main heading @@ -89,6 +94,8 @@ private void AppendHeader(System.Text.StringBuilder sb, string heading) /// /// Appends the conditions section if there are any conditions /// + /// String builder to append to + /// Sub-heading prefix (e.g., "##" or "###") private void AppendConditionsSection(System.Text.StringBuilder sb, string subHeading) { if (Conditions.Count == 0) @@ -115,6 +122,8 @@ private void AppendConditionsSection(System.Text.StringBuilder sb, string subHea /// /// Appends a single condition row to the table /// + /// String builder to append to + /// Condition to format private void AppendConditionRow(System.Text.StringBuilder sb, SonarQualityCondition condition) { // Use friendly name if available, otherwise fall back to metric key @@ -132,6 +141,8 @@ private void AppendConditionRow(System.Text.StringBuilder sb, SonarQualityCondit /// /// Appends the issues section with count and details /// + /// String builder to append to + /// Sub-heading prefix (e.g., "##" or "###") private void AppendIssuesSection(System.Text.StringBuilder sb, string subHeading) { sb.AppendLine($"{subHeading} Issues"); @@ -156,6 +167,8 @@ private void AppendIssuesSection(System.Text.StringBuilder sb, string subHeading /// /// Appends the security hot-spots section with count and details /// + /// String builder to append to + /// Sub-heading prefix (e.g., "##" or "###") private void AppendHotSpotsSection(System.Text.StringBuilder sb, string subHeading) { sb.AppendLine($"{subHeading} Security Hot-Spots"); diff --git a/src/DemaConsulting.SonarMark/SonarQubeClient.cs b/src/DemaConsulting.SonarMark/SonarQubeClient.cs index e5bfb9a..b5512ee 100644 --- a/src/DemaConsulting.SonarMark/SonarQubeClient.cs +++ b/src/DemaConsulting.SonarMark/SonarQubeClient.cs @@ -121,25 +121,31 @@ public async Task GetQualityResultByBranchAsync( /// Project key /// Cancellation token /// Project name + /// Thrown when response is invalid private async Task GetProjectNameByKeyAsync( string serverUrl, string projectKey, CancellationToken cancellationToken) { + // Build API URL for component information var url = $"{serverUrl.TrimEnd('/')}/api/components/show?component={projectKey}"; + // Fetch component data from server var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); + // Parse JSON response var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonDoc = JsonDocument.Parse(content); + // Extract component element from response var root = jsonDoc.RootElement; if (!root.TryGetProperty("component", out var component)) { throw new InvalidOperationException("Invalid component response: missing 'component' property"); } + // Extract project name from component if (!component.TryGetProperty("name", out var nameElement)) { throw new InvalidOperationException("Invalid component response: missing 'name' property"); @@ -157,6 +163,7 @@ private async Task GetProjectNameByKeyAsync( /// Branch name (optional) /// Cancellation token /// Quality gate status and conditions + /// Thrown when response is invalid private async Task<(string QualityGateStatus, List Conditions)> GetQualityGateStatusByBranchAsync( string serverUrl, @@ -164,25 +171,29 @@ private async Task GetProjectNameByKeyAsync( string? branch, CancellationToken cancellationToken) { + // Build API URL with project key and optional branch parameter var url = $"{serverUrl.TrimEnd('/')}/api/qualitygates/project_status?projectKey={projectKey}"; if (!string.IsNullOrWhiteSpace(branch)) { url += $"&branch={Uri.EscapeDataString(branch)}"; } + // Fetch quality gate status from server var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); + // Parse JSON response var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonDoc = JsonDocument.Parse(content); + // Extract project status element from response var root = jsonDoc.RootElement; if (!root.TryGetProperty("projectStatus", out var projectStatus)) { throw new InvalidOperationException("Invalid quality gate response: missing 'projectStatus' property"); } - // Parse quality gate status + // Extract quality gate status value if (!projectStatus.TryGetProperty("status", out var statusElement)) { throw new InvalidOperationException("Invalid quality gate response: missing 'status' property"); @@ -190,7 +201,7 @@ private async Task GetProjectNameByKeyAsync( var qualityGateStatus = statusElement.GetString() ?? "NONE"; - // Parse conditions + // Parse quality gate conditions from response var conditions = ParseQualityGateConditions(projectStatus); return (qualityGateStatus, conditions); @@ -238,6 +249,7 @@ private static SonarQualityCondition ParseQualityGateCondition(JsonElement condi /// Branch name (optional) /// Cancellation token /// List of issues + /// Thrown when response is invalid /// /// Page size is limited to 500 as per requirements. For projects with more than 500 issues, /// only the first 500 will be returned. Future enhancements could implement pagination. @@ -248,6 +260,7 @@ private async Task> GetIssuesAsync( string? branch, CancellationToken cancellationToken) { + // Build API URL with project key, issue statuses filter, and page size limit // Note: Page size is limited to 500 as per requirements var url = $"{serverUrl.TrimEnd('/')}/api/issues/search?componentKeys={projectKey}&issueStatuses=OPEN,CONFIRMED&ps=500"; @@ -256,12 +269,15 @@ private async Task> GetIssuesAsync( url += $"&branch={Uri.EscapeDataString(branch)}"; } + // Fetch issues from server var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); + // Parse JSON response var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonDoc = JsonDocument.Parse(content); + // Extract issues array from response var root = jsonDoc.RootElement; if (!root.TryGetProperty("issues", out var issuesElement)) { @@ -314,6 +330,7 @@ private static SonarIssue ParseIssue(JsonElement issue) /// Branch name (optional) /// Cancellation token /// List of hot-spots + /// Thrown when response is invalid /// /// Page size is limited to 500 as per requirements. For projects with more than 500 hot-spots, /// only the first 500 will be returned. Future enhancements could implement pagination. @@ -324,6 +341,7 @@ private async Task> GetHotSpotsAsync( string? branch, CancellationToken cancellationToken) { + // Build API URL with project key and page size limit // Note: Page size is limited to 500 as per requirements var url = $"{serverUrl.TrimEnd('/')}/api/hotspots/search?projectKey={projectKey}&ps=500"; if (!string.IsNullOrWhiteSpace(branch)) @@ -331,12 +349,15 @@ private async Task> GetHotSpotsAsync( url += $"&branch={Uri.EscapeDataString(branch)}"; } + // Fetch hot-spots from server var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); + // Parse JSON response var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonDoc = JsonDocument.Parse(content); + // Extract hot-spots array from response var root = jsonDoc.RootElement; if (!root.TryGetProperty("hotspots", out var hotSpotsElement)) { @@ -386,24 +407,30 @@ private static SonarHotSpot ParseHotSpot(JsonElement hotSpot) /// Server URL /// Cancellation token /// Dictionary mapping metric keys to friendly names + /// Thrown when response is invalid private async Task> GetMetricNamesByServerAsync( string serverUrl, CancellationToken cancellationToken) { + // Build API URL for metrics search var url = $"{serverUrl.TrimEnd('/')}/api/metrics/search"; + // Fetch metrics from server var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); + // Parse JSON response var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var jsonDoc = JsonDocument.Parse(content); + // Extract metrics array from response var root = jsonDoc.RootElement; if (!root.TryGetProperty("metrics", out var metricsElement)) { throw new InvalidOperationException("Invalid metrics response: missing 'metrics' property"); } + // Build dictionary of metric keys to friendly names var metricNames = new Dictionary(); if (metricsElement.ValueKind == JsonValueKind.Array) { diff --git a/src/DemaConsulting.SonarMark/Validation.cs b/src/DemaConsulting.SonarMark/Validation.cs index 81d3e60..255956e 100644 --- a/src/DemaConsulting.SonarMark/Validation.cs +++ b/src/DemaConsulting.SonarMark/Validation.cs @@ -254,16 +254,18 @@ private static void RunValidationTest( string? reportFileName, Func validator) { + // Record test start time for duration calculation var startTime = DateTime.UtcNow; var test = CreateTestResult(testName); try { + // Create temporary directory for test artifacts using var tempDir = new TemporaryDirectory(); var logFile = Path.Combine(tempDir.DirectoryPath, $"{testName}.log"); var reportFile = reportFileName != null ? Path.Combine(tempDir.DirectoryPath, reportFileName) : null; - // Build command line arguments + // Build command line arguments for test execution var args = new List { "--silent", @@ -278,7 +280,7 @@ private static void RunValidationTest( args.Add(reportFile); } - // Run the program + // Execute the program with mock HTTP client int exitCode; using (var testContext = Context.Create([.. args], mockFactory)) { @@ -286,16 +288,16 @@ private static void RunValidationTest( exitCode = testContext.ExitCode; } - // Check if execution succeeded + // Verify that program executed successfully if (exitCode == 0) { - // Read log and report contents + // Read generated log and report files var logContent = File.ReadAllText(logFile); var reportContent = reportFile != null && File.Exists(reportFile) ? File.ReadAllText(reportFile) : null; - // Validate the results + // Validate test results using provided validator function var errorMessage = validator(logContent, reportContent); if (errorMessage == null) diff --git a/test/DemaConsulting.SonarMark.Tests/DemaConsulting.SonarMark.Tests.csproj b/test/DemaConsulting.SonarMark.Tests/DemaConsulting.SonarMark.Tests.csproj index d0fd323..a571dd0 100644 --- a/test/DemaConsulting.SonarMark.Tests/DemaConsulting.SonarMark.Tests.csproj +++ b/test/DemaConsulting.SonarMark.Tests/DemaConsulting.SonarMark.Tests.csproj @@ -1,37 +1,47 @@ + + net8.0;net9.0;net10.0 12 enable enable - true - true - true - latest false true True + + + true + true + true + latest + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/test/DemaConsulting.SonarMark.Tests/ProgramTests.cs b/test/DemaConsulting.SonarMark.Tests/ProgramTests.cs index e6979e3..63cb235 100644 --- a/test/DemaConsulting.SonarMark.Tests/ProgramTests.cs +++ b/test/DemaConsulting.SonarMark.Tests/ProgramTests.cs @@ -10,18 +10,25 @@ public class ProgramTests /// Test that Version property is not empty /// [TestMethod] - public void Program_Version_NotEmpty_IsNotEmpty() + public void Program_Version_WhenAccessed_ReturnsNonEmptyString() { - // Assert - Assert.IsFalse(string.IsNullOrWhiteSpace(Program.Version)); + // Arrange - no setup needed + + // Act - access the Version property + var version = Program.Version; + + // Assert - verify version is not empty + // This test proves that the Program.Version property returns a valid non-empty version string + Assert.IsFalse(string.IsNullOrWhiteSpace(version)); } /// /// Test that Run method with version flag outputs only version /// [TestMethod] - public void Program_Run_VersionFlag_OutputsVersion() + public void Program_Run_WithVersionFlag_OutputsVersionString() { + // Arrange - capture console output var originalOut = Console.Out; using var output = new StringWriter(); Console.SetOut(output); @@ -29,8 +36,12 @@ public void Program_Run_VersionFlag_OutputsVersion() try { using var context = Context.Create(["--version"]); + + // Act - run the program with version flag Program.Run(context); + // Assert - verify only version is output + // This test proves that --version flag outputs the version string and nothing else Assert.AreEqual(Program.Version + Environment.NewLine, output.ToString()); } finally @@ -43,8 +54,9 @@ public void Program_Run_VersionFlag_OutputsVersion() /// Test that Run method with help flag outputs banner and help /// [TestMethod] - public void Program_Run_HelpFlag_OutputsBannerAndHelp() + public void Program_Run_WithHelpFlag_OutputsBannerAndHelpText() { + // Arrange - capture console output var originalOut = Console.Out; using var output = new StringWriter(); Console.SetOut(output); @@ -52,8 +64,12 @@ public void Program_Run_HelpFlag_OutputsBannerAndHelp() try { using var context = Context.Create(["--help"]); + + // Act - run the program with help flag Program.Run(context); + // Assert - verify banner and help text are present + // This test proves that --help flag outputs the banner and complete help information var outputText = output.ToString(); Assert.Contains("SonarMark version", outputText); Assert.Contains("Usage: sonarmark", outputText); @@ -67,15 +83,19 @@ public void Program_Run_HelpFlag_OutputsBannerAndHelp() } /// - /// Test that Run method with validate flag outputs not implemented message + /// Test that Run method with validate flag runs validation successfully /// [TestMethod] - public void Program_Run_ValidateFlag_OutputsNotImplemented() + public void Program_Run_WithValidateFlag_RunsValidationSuccessfully() { + // Arrange - create context with validate flag using var context = Context.Create(["--validate"]); + + // Act - run the program with validate flag Program.Run(context); - // Just verify it doesn't throw and succeeds + // Assert - verify validation completes successfully + // This test proves that --validate flag triggers self-validation and completes successfully Assert.AreEqual(0, context.ExitCode); } @@ -83,8 +103,9 @@ public void Program_Run_ValidateFlag_OutputsNotImplemented() /// Test that Run method with no flags outputs banner and error for missing server /// [TestMethod] - public void Program_Run_NoFlags_OutputsBannerAndRequiresServer() + public void Program_Run_WithNoArguments_OutputsBannerAndRequiresServerError() { + // Arrange - capture console output var originalOut = Console.Out; using var output = new StringWriter(); Console.SetOut(output); @@ -92,8 +113,12 @@ public void Program_Run_NoFlags_OutputsBannerAndRequiresServer() try { using var context = Context.Create([]); + + // Act - run the program with no arguments Program.Run(context); + // Assert - verify banner is shown and error about missing --server parameter + // This test proves that running without required parameters shows appropriate error var outputText = output.ToString(); Assert.Contains("SonarMark version", outputText); Assert.Contains("--server parameter is required", outputText); @@ -109,8 +134,9 @@ public void Program_Run_NoFlags_OutputsBannerAndRequiresServer() /// Test that Run method with server but no project key outputs error /// [TestMethod] - public void Program_Run_ServerWithoutProjectKey_OutputsError() + public void Program_Run_WithServerButNoProjectKey_OutputsProjectKeyRequiredError() { + // Arrange - capture console output var originalOut = Console.Out; using var output = new StringWriter(); Console.SetOut(output); @@ -118,8 +144,12 @@ public void Program_Run_ServerWithoutProjectKey_OutputsError() try { using var context = Context.Create(["--server", "https://sonarcloud.io"]); + + // Act - run the program with server but no project key Program.Run(context); + // Assert - verify error about missing --project-key parameter + // This test proves that --server requires --project-key to also be specified var outputText = output.ToString(); Assert.Contains("--project-key parameter is required", outputText); Assert.AreEqual(1, context.ExitCode); @@ -134,8 +164,9 @@ public void Program_Run_ServerWithoutProjectKey_OutputsError() /// Test that Run method with silent flag suppresses banner /// [TestMethod] - public void Program_Run_SilentFlag_SuppressesBanner() + public void Program_Run_WithSilentFlag_SuppressesBannerOutput() { + // Arrange - capture console output var originalOut = Console.Out; using var output = new StringWriter(); Console.SetOut(output); @@ -143,8 +174,12 @@ public void Program_Run_SilentFlag_SuppressesBanner() try { using var context = Context.Create(["--silent"]); + + // Act - run the program with silent flag Program.Run(context); + // Assert - verify banner is not shown + // This test proves that --silent flag suppresses the banner output var outputText = output.ToString(); Assert.DoesNotContain("SonarMark version", outputText); }