diff --git a/.cspell.yaml b/.cspell.yaml index cdf4235..773150c 100644 --- a/.cspell.yaml +++ b/.cspell.yaml @@ -17,13 +17,14 @@ words: - acmecorp - acmenator - Alsos + - behaviour - buildmark + - centralised - Cyper - Dema - Doap - fileassert - hotspots - - mstest - Neko - netstandard - NOASSERTION @@ -31,6 +32,8 @@ words: - NTIA - opencover - pandoc + - Postconditions + - Preconditions - Protecode - reqstream - reviewmark @@ -39,9 +42,17 @@ words: - sonarmark - SPDXID - SPDXJSON + - subclassing + - subfolders + - throwingly + - uninitialised + - unioned + - Unrecognised + - unvalidated - versionmark - weasyprint - Weasy + - xunit - yamlfix # Exclude common build artifacts, dependencies, and vendored third-party code diff --git a/.fileassert.yaml b/.fileassert.yaml index c0ad89c..53468d8 100644 --- a/.fileassert.yaml +++ b/.fileassert.yaml @@ -1,7 +1,7 @@ --- # FileAssert document validation tests for SpdxModel. # 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 @@ -12,265 +12,298 @@ tests: # --- BUILD NOTES --- - name: Pandoc_BuildNotesHtml - description: "Build Notes HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "Build Notes" + - contains: Build Notes - name: WeasyPrint_BuildNotesPdf - description: "Build Notes PDF was generated by WeasyPrint" + description: Build Notes PDF was generated by WeasyPrint tags: [build-notes] files: - - pattern: "docs/SpdxModel Build Notes.pdf" + - pattern: docs/generated/SpdxModel Build Notes.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "SpdxModel" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "Build notes" + - field: Title + contains: SpdxModel Build Notes + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Build Notes pages: min: 1 text: - - contains: "Build Notes" + - contains: Build Notes # --- CODE QUALITY --- - name: Pandoc_CodeQualityHtml - description: "Code Quality HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "CodeQL" + - contains: Code Quality - name: WeasyPrint_CodeQualityPdf - description: "Code Quality PDF was generated by WeasyPrint" + description: Code Quality PDF was generated by WeasyPrint tags: [code-quality] files: - - pattern: "docs/SpdxModel Code Quality.pdf" + - pattern: docs/generated/SpdxModel Code Quality Report.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "Code Quality" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "Code Quality" + - field: Title + contains: Code Quality + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Code Quality pages: min: 1 text: - - contains: "CodeQL" + - contains: Code Quality # --- CODE REVIEW PLAN --- - name: Pandoc_ReviewPlanHtml - description: "Code Review Plan HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "Review Plan" + - contains: Review Plan - name: WeasyPrint_ReviewPlanPdf - description: "Code Review Plan PDF was generated by WeasyPrint" + description: Code Review Plan PDF was generated by WeasyPrint tags: [code-review] files: - - pattern: "docs/SpdxModel Review Plan.pdf" + - pattern: docs/generated/SpdxModel Code Review Plan.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "Review Plan" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "Review Plan" + - field: Title + contains: Review Plan + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Review Plan pages: min: 1 text: - - contains: "Review Plan" + - contains: Review Plan # --- CODE REVIEW REPORT --- - name: Pandoc_ReviewReportHtml - description: "Code Review Report HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "Review Report" + - contains: Review Report - name: WeasyPrint_ReviewReportPdf - description: "Code Review Report PDF was generated by WeasyPrint" + description: Code Review Report PDF was generated by WeasyPrint tags: [code-review] files: - - pattern: "docs/SpdxModel Review Report.pdf" + - pattern: docs/generated/SpdxModel Code Review Report.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "Review Report" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "Review Report" + - field: Title + contains: Review Report + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Review Report pages: min: 1 text: - - contains: "Review Report" + - contains: Review Report # --- DESIGN DOCUMENT --- - name: Pandoc_DesignHtml - description: "Design HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "Design" + - contains: Design - name: WeasyPrint_DesignPdf - description: "Design PDF was generated by WeasyPrint" + description: Design PDF was generated by WeasyPrint tags: [design] files: - - pattern: "docs/SpdxModel Software Design.pdf" + - pattern: docs/generated/SpdxModel Software Design Document.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "Design" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "design document" + - field: Title + contains: Design + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Design pages: min: 3 text: - - contains: "Design" + - 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/SpdxModel Verification Design Document.pdf + count: 1 + pdf: + metadata: + - field: Title + contains: Verification + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Verification + pages: + min: 3 + text: + - contains: Verification # --- USER GUIDE --- - name: Pandoc_UserGuideHtml - description: "User Guide HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "User Guide" + - contains: User Guide - name: WeasyPrint_UserGuidePdf - description: "User Guide PDF was generated by WeasyPrint" + description: User Guide PDF was generated by WeasyPrint tags: [user-guide] files: - - pattern: "docs/SpdxModel User Guide.pdf" + - pattern: docs/generated/SpdxModel User Guide.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "User Guide" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "serializing" + - field: Title + contains: User Guide + - field: Author + contains: DEMA Consulting + - field: Subject + contains: User Guide pages: min: 3 text: - - contains: "User Guide" + - contains: User Guide # --- REQUIREMENTS DOCUMENT --- # Note: these tests run after ReqStream and do not contribute to OTS requirements evidence. - name: Pandoc_RequirementsHtml - description: "Requirements HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "Requirements" + - contains: Requirements - name: WeasyPrint_RequirementsPdf - description: "Requirements PDF was generated by WeasyPrint" + description: Requirements PDF was generated by WeasyPrint tags: [requirements] files: - - pattern: "docs/SpdxModel Requirements.pdf" + - pattern: docs/generated/SpdxModel Requirements Document.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "Requirements" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "Requirements" + - field: Title + contains: Requirements + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Requirements pages: min: 1 text: - - contains: "Requirements" + - contains: Requirements # --- TRACE MATRIX --- # Note: these tests run after ReqStream and do not contribute to OTS requirements evidence. - name: Pandoc_TraceMatrixHtml - description: "Trace Matrix HTML was generated by Pandoc" + 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" + - query: //head/title count: 1 text: - - contains: "Trace Matrix" + - contains: Trace Matrix - name: WeasyPrint_TraceMatrixPdf - description: "Trace Matrix PDF was generated by WeasyPrint" + description: Trace Matrix PDF was generated by WeasyPrint tags: [requirements] files: - - pattern: "docs/SpdxModel Trace Matrix.pdf" + - pattern: docs/generated/SpdxModel Trace Matrix.pdf count: 1 pdf: metadata: - - field: "Title" - contains: "Trace Matrix" - - field: "Author" - contains: "DEMA Consulting" - - field: "Subject" - contains: "Traceability" + - field: Title + contains: Trace Matrix + - field: Author + contains: DEMA Consulting + - field: Subject + contains: Trace Matrix pages: min: 1 text: - - contains: "Trace Matrix" + - contains: Trace Matrix diff --git a/.github/agents/developer.agent.md b/.github/agents/developer.agent.md index 35f5dda..5f208eb 100644 --- a/.github/agents/developer.agent.md +++ b/.github/agents/developer.agent.md @@ -14,6 +14,10 @@ Perform software development tasks by determining and applying appropriate stand 2. **Read relevant standards** using the selection matrix in AGENTS.md 3. **Pre-flight verification** before making any changes: - List files that will be created, modified, or deleted + - For each file to be **created**, check whether a counterpart exists in the + template (URL in the `# Reference Template` section of `AGENTS.md`). + If one exists, fetch it as the starting point; adjust placeholder names and heading + depth to match the target path before writing the file - For each modified file, identify which companion artifacts need updating (requirements, design docs, tests, review-sets) - Include companion artifact updates in the work plan @@ -21,7 +25,7 @@ Perform software development tasks by determining and applying appropriate stand 5. **Formatting**: Run `pwsh ./fix.ps1` to silently apply all available auto-fixers (dotnet format, markdown, YAML) before committing 6. **Build and test** (code changes only): Run `pwsh ./build.ps1` and confirm it - passes — report FAILED if the build or any tests fail + passes - report FAILED if the build or any tests fail 7. **Generate completion report** per the AGENTS.md reporting requirements - save to `.agent-logs/{agent-name}-{subject}-{unique-id}.md` and return the summary to the caller diff --git a/.github/agents/formal-review.agent.md b/.github/agents/formal-review.agent.md index 88b0691..7dd8e84 100644 --- a/.github/agents/formal-review.agent.md +++ b/.github/agents/formal-review.agent.md @@ -20,6 +20,8 @@ Before reviewing, read these standards to inform review judgments: hierarchy and categorization review judgments - **`design-documentation.md`** - defines mandatory sections, structural conventions, and coverage expected at each level; informs all design documentation review judgments +- **`verification-documentation.md`** - defines mandatory sections, structural conventions, + and coverage expected at each level; informs all verification design review judgments For review sets that include source code or tests, also consult the relevant standards from the selection matrix in AGENTS.md. diff --git a/.github/agents/lint-fix.agent.md b/.github/agents/lint-fix.agent.md index 83ad8cb..549e751 100644 --- a/.github/agents/lint-fix.agent.md +++ b/.github/agents/lint-fix.agent.md @@ -36,7 +36,12 @@ submission, not during normal development. - **markdownlint MD013 (line length)**: Wrap long lines at natural break points, after commas, before conjunctions, or at sentence boundaries. Do not break - in the middle of a code span or URL. + in the middle of a code span or URL. **Pipe-tables that cannot be wrapped + without breaking structure** are a special case - convert them to a bullet + list if the data reads naturally that way, or rewrite as a + [grid table](https://pandoc.org/MANUAL.html#tables) if a tabular layout is + essential. Do not get stuck trying to squeeze a wide pipe-table into 120 + characters. - **markdownlint other rules**: Apply the specific fix indicated in the output (e.g., missing blank lines, heading levels, code fence languages). diff --git a/.github/agents/quality.agent.md b/.github/agents/quality.agent.md index da467d4..380d11f 100644 --- a/.github/agents/quality.agent.md +++ b/.github/agents/quality.agent.md @@ -54,21 +54,13 @@ Priority-ordered list of issues that MUST be resolved for the next retry: ## Requirements Compliance: (PASS|FAIL|N/A) -- Were requirements updated to reflect functional changes? -- Were new requirements created for new features? -- Do requirement IDs follow semantic naming standards? -- Do requirement files follow kebab-case naming convention? -- Are requirement files organized under `docs/reqstream/` with proper folder structure? -- Are OTS requirements properly placed in `docs/reqstream/ots/` subfolder? -- Were source filters applied appropriately for platform-specific requirements? +- Were requirements created/updated for all functional changes? +- Were source filters applied for platform-specific requirements? - Is requirements traceability maintained to tests? ## Design Documentation Compliance: (PASS|FAIL|N/A) -- Were design documents updated for architectural changes? -- Were new design artifacts created for new components? -- Do design folder names use kebab-case convention matching source structure? -- Are design files properly named ({subsystem-name}.md, {unit-name}.md patterns)? +- Were design artifacts created/updated for all new or changed components? - Is `docs/design/introduction.md` present with required Software Structure section? - Are design decisions documented with rationale? - Is system/subsystem/unit categorization maintained? @@ -76,55 +68,50 @@ Priority-ordered list of issues that MUST be resolved for the next retry: ## Code Quality Compliance: (PASS|FAIL|N/A) -- Are language-specific standards followed (from applicable standards files)? -- Are quality checks from standards files satisfied? -- Is code properly categorized (system/subsystem/unit/OTS)? -- Is appropriate separation of concerns maintained? -- Was language-specific build tooling executed and passing? +- Do language-specific quality checks from loaded standards pass? +- Is code properly categorized (system/subsystem/unit/OTS/Shared Package)? +- Does the build pass? ## Testing Compliance: (PASS|FAIL|N/A) - Were tests created/updated for all functional changes? - Is test coverage maintained for all requirements? -- Are testing standards followed (AAA pattern, etc.)? -- Do tests respect software item hierarchy boundaries (System/Subsystem/Unit scope)? +- Do tests respect software item hierarchy boundaries? - Are cross-hierarchy test dependencies documented in design docs? -- Does test categorization align with code structure? -- Do all tests pass without failures? +- Do all tests pass? ## Review Management Compliance: (PASS|FAIL|N/A) -- Were review-sets updated for structural changes (new/deleted systems, subsystems, or units)? -- Do file patterns follow include-then-exclude approach? +- Were review-sets updated for structural changes? - Is review scope appropriate for change magnitude? -- Was ReviewMark tooling executed and passing? -- Were review artifacts generated correctly? +- Does ReviewMark pass? ## Documentation Compliance: (PASS|FAIL|N/A) -- Was README.md updated for user-facing changes? -- Were user guides updated for feature changes? +- Were README.md and user guides updated for user-facing changes? - Does API documentation reflect code changes? - Was compliance documentation generated? -- Does documentation follow standards formatting? -- Is documentation organized under `docs/` following standard folder structure? -- Do Pandoc collections include proper `introduction.md` with Purpose and Scope sections? - Are auto-generated markdown files left unmodified? -- Do README.md files use absolute URLs and include concrete examples? -- Is documentation integrated into ReviewMark review-sets for formal review? +- Is documentation integrated into ReviewMark review-sets? ## Software Item Completeness: (PASS|FAIL|N/A) +- Load `software-items.md` before evaluating this section. + - Does every identified software unit have its own requirements file? - Does every identified software unit have its own design document? - Does every identified subsystem have its own requirements file? - Does every identified subsystem have its own design document? +## Repository Structure Compliance: (PASS|FAIL|N/A) + +- Load `repository-map.md` from the template URL in the `# Reference Template` + section of `AGENTS.md` before evaluating this section. + +- Are parallel artifact trees in sync (reqstream/design/verification/src/test)? +- Does the repository conform to the template `repository-map.md`? + ## Process Compliance: (PASS|FAIL|N/A) -- Was Continuous Compliance workflow followed? -- Did all quality gates execute successfully? -- Were appropriate tools used for validation? -- Were standards consistently applied across work? -- Was compliance evidence generated and preserved? +- Was compliance evidence (test results, review artifacts, generated docs) generated and preserved? ``` diff --git a/.github/agents/repo-consistency.agent.md b/.github/agents/repo-consistency.agent.md deleted file mode 100644 index 0b66277..0000000 --- a/.github/agents/repo-consistency.agent.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -name: repo-consistency -description: > - Ensures downstream repositories remain consistent with the TemplateDotNetLibrary - template patterns and best practices. -user-invocable: true ---- - -# Repo Consistency Agent - -Maintain consistency between downstream projects and the TemplateDotNetLibrary 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/agents/software-architect.agent.md b/.github/agents/software-architect.agent.md index 494568d..de5efa2 100644 --- a/.github/agents/software-architect.agent.md +++ b/.github/agents/software-architect.agent.md @@ -13,7 +13,7 @@ Interview the user and produce evolving architecture documentation with prioriti # Standards Read `.github/standards/software-items.md` before starting. Use its definitions -(Software Package, System, Subsystem, Unit, OTS) as vocabulary throughout. +(Software Package, System, Subsystem, Unit, OTS, Shared Package) as vocabulary throughout. # Approach diff --git a/.github/agents/template-sync.agent.md b/.github/agents/template-sync.agent.md new file mode 100644 index 0000000..c013503 --- /dev/null +++ b/.github/agents/template-sync.agent.md @@ -0,0 +1,109 @@ +--- +name: template-sync +description: Audits or synchronizes repository files against the canonical template. + Supports four modes - Audit, Sync, Scaffold, and Recreate. +user-invocable: true +--- + +# Template Sync Agent + +This agent is an orchestrator supporting four modes: + +- **Audit** - report structural deviations; no changes +- **Sync** - patch missing sections into existing files +- **Scaffold** - create files that do not yet exist; skip existing files +- **Recreate** - rebuild existing files from the template, migrating old content + +Read the template URL and `repository-map.md` from the `# Reference Template` +section in `AGENTS.md`, then map the requested scope onto the work groups below. +Delegate each group to a sub-agent. + +# Work Groups + +- **Root config files** - all non-collection files at the repository root +- **One group per flat `docs/` folder** - e.g. `docs/build_notes/`, `docs/user_guide/` +- **One group per system subtree** in `docs/design/`, `docs/verification/`, `docs/reqstream/` - + each subtree and all its descendants is one group + +# Orchestration + +For each group intersecting the requested scope, call a sub-agent with: + +- **context**: + - Group scope and template URL from the `# Reference Template` section in `AGENTS.md` + - Applicable standards from the `# Standards Application` matrix in `AGENTS.md` + for the file types in the group scope + - Project-specific names substitute for placeholders at matching path depth + (e.g. `MySystem` → `{SystemName}`, `my-system` → `{system-name}`) + - For files within `{system-name}/` subtrees in `docs/design/`, `docs/verification/`, + and `docs/reqstream/`: consult `docs/design/introduction.md` to determine whether + each item is a subsystem or unit, then select the appropriate template + (`subsystem-name.*` or `unit-name.*`) regardless of the item's folder depth — + do not infer item type from path depth alone + - If a template counterpart cannot be fetched, skip the file and report it +- **goal**: + - Based on the given mode: + - **Audit** - fetch each template counterpart; compare headings; report missing + sections and depth mismatches; no changes + - **Sync** - as Audit, then insert each missing section; run `pwsh ./fix.ps1` + - **Scaffold** - fetch `repository-map.md` from the template URL in `AGENTS.md` + to identify files that should exist but don't; for each, fetch the template, + populate all sections, write the file; run `pwsh ./fix.ps1` + - **Recreate** - fetch the template and use it as the blueprint for a + freshly authored document: + - Work through the template section by section; for each section, find + any `TEMPLATE-DIRECTIVE` blocks (both `` + in markdown and `# ` in YAML) — execute + each directive (read specified standards, apply structural guidance, + substitute content), then **remove the directive block entirely** from + the output; gather the relevant technical details from all available + sources — the old file, README, related docs, sibling files, and any + other repo context — to populate that section correctly; the old file's + structure and headings are irrelevant; only its factual content is mined + as a source + - **Gap-check**: after all template sections are filled, scan the old + file once more for any technical information not yet captured; if + found, preserve it by appending new relevant sections at the end + - **Before writing**: do a mandatory self-check — for every section that + has a `TEMPLATE-DIRECTIVE` block in the template, explicitly state what + format the directive requires, then verify the drafted content matches + that format exactly (e.g. if the directive says "no sub-headings", + confirm there are no `###` headings inside that section; if it says + "bold-name paragraph blocks", confirm each entry is `**Name**: prose` + with no sub-heading); fix any mismatches before writing the file + - Write the rebuilt file; run `pwsh ./fix.ps1` + - When writing any section: `TEMPLATE-DIRECTIVE` blocks are directives — + execute them (read specified standards, apply structural guidance, substitute + content) and **remove the block entirely** from the written file; inline + `TODO:` placeholders in YAML string values (e.g. `title:`, `justification:`) + are content placeholders — always resolve them to real content; infer from + README, related files, sibling docs, and path; if confident write directly; + if ambiguous offer 2–3 concrete options and ask the user; keep asking until + they answer - never leave a TODO or TEMPLATE-DIRECTIVE in the output unless + the user explicitly requests it + +Collect sub-agent results and assemble the final report. + +# Report Template + +```markdown +# Template Sync Report + +**Result**: (SUCCEEDED|FAILED) +**Mode**: (Audit|Sync|Scaffold|Recreate) + +## Files + +### {file-path} + +- **Template**: {template path} +- **Missing sections**: {list or "none"} +- **Heading depth issues**: {list or "none"} +- **Content format issues**: {list of sections where intra-section content did not + match the template comment's prescribed format, or "none"} *(Recreate only)* +- **Action**: (Reported | Sections added | Created | Rebuilt | No template found) + +## Summary + +- **Conformant**: {count} | **Deviations**: {count} | **Updated**: {count} +``` diff --git a/.github/standards/coding-principles.md b/.github/standards/coding-principles.md index 213c031..6797c61 100644 --- a/.github/standards/coding-principles.md +++ b/.github/standards/coding-principles.md @@ -3,11 +3,6 @@ name: Coding Principles description: Follow these standards when developing any software code. --- -# Coding Principles Standards - -This document defines universal coding principles and quality standards for software development within -Continuous Compliance environments. - # Core Principles ## Literate Coding @@ -20,11 +15,34 @@ 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 — + not just the public interface, because reviewers and auditors must verify every + implementation detail. Access-level specifics 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 @@ -55,13 +73,13 @@ All code MUST follow literate programming principles: ## Universal Anti-Patterns -- **Skip Literate Coding**: Don't skip literate programming comments - they are required for maintainability -- **Ignore Compiler Warnings**: Don't ignore compiler warnings - they exist for quality enforcement +- **Skip Literate Coding**: Don't skip literate programming comments +- **Ignore Compiler Warnings**: Don't ignore compiler warnings - **Hidden Dependencies**: Don't create untestable code with hidden dependencies - **Hidden Functionality**: Don't implement functionality without requirement traceability because untraced functionality cannot be validated during audits - **Monolithic Functions**: Don't write monolithic functions with multiple responsibilities -- **Overcomplicated Solutions**: Don't make solutions more complex than necessary - favor simplicity and clarity +- **Overcomplicated Solutions**: Don't make solutions more complex than necessary - **Premature Optimization**: Don't optimize for performance before establishing correctness - **Copy-Paste Programming**: Don't duplicate logic - extract common functionality into reusable components - **Magic Numbers**: Don't use unexplained constants - either name them or add clear comments diff --git a/.github/standards/csharp-language.md b/.github/standards/csharp-language.md index 707b0f9..ec05a25 100644 --- a/.github/standards/csharp-language.md +++ b/.github/standards/csharp-language.md @@ -4,37 +4,60 @@ 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` - -# Literate Coding Example +# API Documentation and Literate Coding Example ```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)); -} + // 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); -// Transform input data using the configured processing pipeline -var processedData = ProcessingPipeline.Transform(input); + // Apply the calibration offset to convert raw counts to physical units + var corrected = reading + calibration.Offset; -// Apply business rules and validation logic -var validatedResults = BusinessRuleEngine.ValidateAndProcess(processedData); - -// Return formatted results matching the expected output contract -return OutputFormatter.Format(validatedResults); + // Clamp to the operational range so consumers can rely on the documented contract + return Math.Clamp(corrected, calibration.Minimum, calibration.Maximum); +} ``` +Key qualities demonstrated above: + +- **``** is a brief one-liner explaining *what* the method does +- **``** sits directly after summary and carries the extended intent - + *why* it exists, design decisions, thread-safety, and side-effect disclosures +- **`` tags** state constraints (finite, non-null) so callers know what + is valid without reading the body +- **``** documents the boundary guarantee so consumers can rely on the + contract +- **`` tags** name every thrown exception and the condition that + triggers each one +- **Inline block comments** follow the Literate Coding principles from + `coding-principles.md`, separating logical steps so reviewers can verify each + step against design intent + # Code Formatting - **Format entire solution**: `dotnet format` diff --git a/.github/standards/csharp-testing.md b/.github/standards/csharp-testing.md index 1591eeb..629ab9b 100644 --- a/.github/standards/csharp-testing.md +++ b/.github/standards/csharp-testing.md @@ -4,115 +4,77 @@ 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: +/// +/// 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: internal class MyTests -// ✅ Good: public class MyTests + // Act / Assert: email with no domain throws + Assert.Throws(() => validator.ValidateEmail("not-an-email")); +} ``` -# Avoid Assert.IsTrue for Collection Count +# xUnit v3 Specifics -Use `Assert.HasCount` for count assertions: - -```csharp -// ❌ Bad: Assert.IsTrue(collection.Count == 3); -// ✅ Good: Assert.HasCount(3, collection); -``` +These are non-obvious v3 behaviors that differ from v2 or common assumptions: -# Avoid Assert.IsTrue for String Prefix Checks +- **`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]` -Use `Assert.StartsWith` instead, as it produces clearer failure messages: +# Repository-Specific Exceptions -```csharp -// ❌ Bad: Assert.IsTrue(value.StartsWith("prefix")); -// ✅ Good: Assert.StartsWith("prefix", value); -``` +No active exceptions. The `DemaConsulting.SpdxModel.Tests` project was migrated from MSTest +to xUnit v3 and now follows this standard. # Quality Checks -Before submitting C# tests, verify: - - [ ] All tests follow AAA pattern with clear section comments -- [ ] Test names follow hierarchical patterns defined in Test Naming Standards section -- [ ] Each test verifies single, specific behavior (no shared state) +- [ ] Test names follow hierarchical naming pattern above +- [ ] Each test verifies single, specific behavior (no shared state between tests) - [ ] Both success and failure scenarios covered including edge cases -- [ ] External dependencies mocked with NSubstitute or equivalent +- [ ] External dependencies mocked with NSubstitute (when mocking is needed) - [ ] Tests linked to requirements with source filters where needed -- [ ] Test results generate TRX format for ReqStream compatibility -- [ ] MSTest V4 anti-patterns avoided (proper assertions, public visibility, etc.) +- [ ] Test results generated in TRX format for ReqStream compatibility (`dotnet test --logger trx`) diff --git a/.github/standards/design-documentation.md b/.github/standards/design-documentation.md index 30becb5..e5b7bf9 100644 --- a/.github/standards/design-documentation.md +++ b/.github/standards/design-documentation.md @@ -4,185 +4,112 @@ description: Follow these standards when creating design documentation. globs: ["docs/design/**/*.md"] --- -# Design Documentation Standards - -This document defines standards for design documentation within Continuous -Compliance environments, extending the general technical documentation -standards with specific requirements for software design artifacts. - -## Required Standards - -Read these standards first before applying this standard: +# Required Standards - **`technical-documentation.md`** - General technical documentation standards -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) - -# Core Principles - -Design documentation serves as the bridge between requirements and -implementation, providing detailed technical specifications that enable: - -- **Formal Code Review**: Reviewers can verify implementation matches design -- **Compliance Evidence**: Auditors can trace requirements through design to code -- **Maintenance Support**: Developers can understand system structure and interactions -- **Quality Assurance**: Testing teams can validate against detailed specifications +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) -# Required Structure and Documents - -Design documentation must be organized under `docs/design/` with folder structure -mirroring source code organization because reviewers need clear navigation from -design to implementation: +# Folder Structure ```text docs/design/ -├── introduction.md # Design overview with software structure -└── {system-name}/ # System-level design folder (one per system) - ├── {system-name}.md # System-level design documentation - ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively - │ ├── {subsystem-name}.md # Subsystem overview and design - │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) - │ └── {unit-name}.md # Unit-level design documents - └── {unit-name}.md # Top-level unit design documents (if not in subsystem) +├── introduction.md # heading depth # +├── {system-name}.md # heading depth # +├── {system-name}/ +│ ├── {subsystem-name}.md # heading depth ## +│ ├── {subsystem-name}/ +│ │ └── {unit-name}.md # heading depth ### +│ └── {unit-name}.md # heading depth ## +├── ots.md # heading depth # (if OTS items exist) +├── ots/ +│ └── {ots-name}.md # heading depth ## +├── shared.md # heading depth # (if Shared Packages exist) +└── shared/ + └── {package-name}.md # heading depth ## ``` -## introduction.md (MANDATORY) +All sections in every file are mandatory; write "N/A - {justification}" rather than removing any. +Determine subsystem vs. unit classification from `docs/design/introduction.md` — folder depth does not determine classification. +Do not record version numbers anywhere in design documentation — version information is managed in SBOMs. -The `introduction.md` file serves as the design entry point and MUST include -these sections because auditors need clear scope boundaries and architectural -overview: +# introduction.md (MANDATORY) -### Purpose Section +Must include: -Clear statement of the design document's purpose, audience, and regulatory -or compliance drivers. +- **Purpose**: audience and compliance drivers +- **Scope**: items covered and explicitly excluded (no test projects) +- **Software Structure**: text tree showing all Systems/Subsystems/Units/OTS/Shared items +- **Folder Layout**: text tree showing source folder structure +- **Companion Artifact Structure**: parallel paths for requirements, design, verification, source, tests +- **References** _(if applicable)_: external standards or specifications - only in `introduction.md` -### Scope Section +# System Design (MANDATORY) -Define what software items are covered and what is explicitly excluded. -Design documentation must NOT include test projects, test classes, or test -infrastructure because design documentation documents the architecture of -shipping product code, not ancillary content used to validate it. +Create `{system-name}.md` (`#` heading) and `{system-name}/` folder: -### Software Structure Section (MANDATORY) +- **Architecture**: software items, relationships, and collaboration +- **External Interfaces**: name, direction, format, constraints +- **Dependencies**: OTS and Shared Packages used; cross-reference their design docs +- **Risk Control Measures**: segregation required for risk control (IEC 62304 §5.3.3) +- **Data Flow**: inputs to outputs +- **Design Constraints**: platform, performance, security, regulatory -Include a text-based tree diagram showing the software organization across -System, Subsystem, and Unit levels. Agents MUST read `software-items.md` -to understand these classifications before creating this section. +# Subsystem Design (MANDATORY) -Example format: +Place `{subsystem-name}.md` in the **parent** folder; create `{subsystem-name}/` for children: -```text -Project1Name (System) -├── ComponentA (Subsystem) -│ ├── SubComponentP (Subsystem) -│ │ └── ClassW (Unit) -│ ├── ClassX (Unit) -│ └── ClassY (Unit) -├── ComponentB (Subsystem) -│ └── ClassZ (Unit) -└── UtilityClass (Unit) - -Project2Name (System) -└── HelperClass (Unit) -``` +- **Overview**: responsibility, boundaries, contained units +- **Interfaces**: what it exposes and consumes +- **Design**: how internal units collaborate -### Folder Layout Section (MANDATORY) +# Unit Design (MANDATORY) -Include a text-based tree diagram showing how the source code folders -mirror the software structure, with file paths and brief descriptions. +Place `{unit-name}.md` in the **parent** folder: -Example format: +- **Purpose**: single responsibility +- **Data Model**: fields, properties, types, invariants (IEC 62304 §5.4.2) +- **Key Methods**: name, purpose, algorithm, preconditions, postconditions, parameter types +- **Error Handling**: detection and handling; what is propagated vs. handled locally +- **Dependencies**: other units, subsystems, OTS items, and shared packages used +- **Callers**: units or subsystems that call or consume this unit -```text -src/Project1Name/ -├── ComponentA/ -│ ├── SubComponentP/ -│ │ └── ClassW.cs - Specialized processing engine -│ ├── ClassX.cs - Core business logic handler -│ └── ClassY.cs - Data validation service -├── ComponentB/ -│ └── ClassZ.cs - Integration interface -└── UtilityClass.cs - Common utility functions - -src/Project2Name/ -└── HelperClass.cs - Helper functions -``` +# OTS Integration Design (when OTS items exist) -### Companion Artifact Structure (RECOMMENDED) +Create `docs/design/ots.md` (`#` heading) covering the overall OTS integration strategy. -Include a brief note explaining that each software item has parallel artifacts -across the repository, so agents and reviewers can navigate from any one -artifact to all related files: +For each OTS item, create `docs/design/ots/{ots-name}.md` (`##` heading) with sections: -Example format: +- **Purpose**: why chosen and what it provides to the local system +- **Features Used**: which specific features, APIs, or capabilities are consumed +- **Integration Pattern**: how it is consumed; initialization, configuration, disposal requirements -```text -Each software item in the structure above has corresponding artifacts in -parallel directory trees: - -- Requirements: `docs/reqstream/{system}/.../{item}.yaml` (kebab-case) -- Design docs: `docs/design/{system}/.../{item}.md` (kebab-case) -- Source code: `src/{System}/.../{Item}.{ext}` (cased per language - see `software-items.md`) -- Tests: `test/{System}.Tests/.../{Item}Tests.{ext}` (cased per language - see `software-items.md`) -- Review-sets: defined in `.reviewmark.yaml` -``` - -## System Design Documentation (MANDATORY) - -For each system identified in the repository: - -- Create a kebab-case folder matching the system name -- Include `{system-name}.md` with system-level design documentation such as: - - System architecture and major components - - External interfaces and dependencies - - Data flow and control flow - - System-wide design constraints and decisions - - Integration patterns and communication protocols - -## Subsystem and Unit Design Documents - -For each subsystem identified in the software structure: +# Shared Package Integration Design (when Shared Packages exist) -- Create a kebab-case folder matching the subsystem name (enables automated tooling) -- Include `{subsystem-name}.md` with subsystem overview and design -- Include unit design documents for ALL units within the subsystem +Create `docs/design/shared.md` (`#` heading) covering the overall consumption strategy. -For every unit identified in the software structure: +For each Shared Package, create `docs/design/shared/{package-name}.md` (`##` heading) with sections: -- Document data models, algorithms, and key methods -- Describe interactions with other units -- Include sufficient detail for formal code review -- Place in appropriate subsystem folder or at design root level - -# Software Items Integration (CRITICAL) - -Read `software-items.md` before creating design documentation - correct -System/Subsystem/Unit categorization is required for software structure -diagrams and folder layout. +- **Advertised Features Consumed**: which features the local system relies on +- **Integration Pattern**: how the package is referenced, initialized, and consumed +- **Assumptions**: any assumptions the local system makes about the package's behavior # Writing Guidelines -Design documentation must be technical and specific because it serves as the -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 - -# Mermaid Diagram Integration - -Use Mermaid diagrams to supplement text descriptions (diagrams must not replace text content). +- Use Mermaid diagrams to supplement (not replace) text +- Use verbal cross-references ("see _Parser Design_") - not markdown hyperlinks (break in PDF) +- Provide sufficient detail for formal code review # Quality Checks -Before submitting design documentation, verify: - -- [ ] `introduction.md` includes both Software Structure and Folder Layout sections -- [ ] Software structure correctly categorizes items as System/Subsystem/Unit per `software-items.md` -- [ ] Folder layout mirrors software structure organization -- [ ] Design documents provide sufficient detail for code review -- [ ] System documentation provides comprehensive system-level design -- [ ] Subsystem documentation folders use kebab-case names while mirroring source subsystem names and structure -- [ ] All documents follow technical documentation formatting standards -- [ ] Content is current with implementation and requirements -- [ ] Documents are integrated into ReviewMark review-sets for formal review +- [ ] `introduction.md` includes Software Structure, Folder Layout, and Companion Artifact Structure +- [ ] Software structure correctly categorizes items per `software-items.md` +- [ ] Each file's heading depth matches its folder depth +- [ ] All folders use kebab-case mirroring source structure +- [ ] System design includes all mandatory sections (Architecture, External Interfaces, Dependencies, + Risk Control Measures, Data Flow, Design Constraints) +- [ ] Subsystem design includes all mandatory sections (Overview, Interfaces, Design) +- [ ] Unit design includes all mandatory sections (Purpose, Data Model, Key Methods, Error Handling, Dependencies, Callers) +- [ ] Non-applicable mandatory sections contain "N/A - {justification}" +- [ ] `docs/design/ots.md` and `docs/design/ots/{ots-name}.md` exist when OTS items are present +- [ ] `docs/design/shared.md` and `docs/design/shared/{package-name}.md` exist when Shared Packages are present +- [ ] Documents are integrated into ReviewMark review-sets diff --git a/.github/standards/reqstream-usage.md b/.github/standards/reqstream-usage.md index ae5e565..2371164 100644 --- a/.github/standards/reqstream-usage.md +++ b/.github/standards/reqstream-usage.md @@ -9,7 +9,7 @@ globs: ["requirements.yaml", "docs/reqstream/**/*.yaml"] Read these standards first before applying this standard: - **`requirements-principles.md`** - Requirements principles and unidirectionality -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) # Requirements Organization @@ -18,54 +18,83 @@ because ReqStream discovers files via the includes chain in `requirements.yaml` and organizes report output by this hierarchy: ```text -requirements.yaml # Root file (includes only) +requirements.yaml # Root file (includes only) docs/reqstream/ -├── {system-name}/ # System-level requirements folder (one per system) -│ ├── {system-name}.yaml # System-level requirements +├── {system-name}.yaml # System-level requirements +├── {system-name}/ # System folder (one per system) │ ├── platform-requirements.yaml # Platform support requirements -│ ├── {subsystem-name}/ # Subsystem (kebab-case); may nest recursively -│ │ ├── {subsystem-name}.yaml # Requirements for this subsystem -│ │ ├── {child-subsystem}/ # Child subsystem (same structure as parent) -│ │ └── {unit-name}.yaml # Requirements for units within this subsystem -│ └── {unit-name}.yaml # Requirements for top-level units (outside subsystems) -└── ots/ # OTS items appear as a distinct section in reports - └── {ots-name}.yaml # Requirements for OTS components +│ ├── {subsystem-name}.yaml # Subsystem requirements +│ ├── {subsystem-name}/ # Subsystem folder (kebab-case); may nest recursively +│ │ ├── {subsystem-name}.yaml # Child subsystem requirements +│ │ ├── {subsystem-name}/ # Child subsystem folder +│ │ └── {unit-name}.yaml # Unit requirements +│ └── {unit-name}.yaml # System-level unit requirements +├── ots/ # OTS items appear as a distinct section in reports +│ └── {ots-name}.yaml # Requirements for OTS components +└── shared/ # Shared Packages appear as a distinct section in reports + └── {package-name}.yaml # Requirements for Shared Package dependencies ``` +Local items have matching relative paths across `docs/reqstream/`, `docs/design/`, and `docs/verification/`: + +- Requirements: `{system-name}[/{subsystem-name}...]/{item-name}.yaml` +- Design: `{system-name}[/{subsystem-name}...]/{item-name}.md` +- Verification: `{system-name}[/{subsystem-name}...]/{item-name}.md` + # Requirements File Format -```yaml -sections: - - title: Functional Requirements - requirements: - - id: System-Component-Feature # Used as-is in all reports - make it readable - title: The system shall perform the required function. - justification: | - Business rationale and any regulatory references. - # ReqStream extracts this field into the justifications report (--justifications) - children: # ReqStream validates this decomposition chain - - ChildSystem-Feature-Behavior # Downward links only (see requirements-principles.md) - tests: # ReqStream matches these by method name in test results - - TestMethodName - - windows@PlatformSpecificTest # Only test runs on Windows count as evidence -``` +Each file adds requirements at exactly one level of the hierarchy. The file spells out +its full ancestry as nested `{ItemName} Requirements` sections down to that level, then +places requirements there. ReqStream merges identical section title paths across included +files automatically. Always determine item classification from `docs/design/introduction.md` - +folder depth does not determine whether an item is a subsystem or unit. + +Valid section nestings (names in `{braces}` are placeholders): -# OTS Software Requirements +```text +{SystemName} Requirements # system-level requirements +├── {SubsystemName} Requirements # root subsystem requirements +│ ├── {SubsystemName} Requirements # nested subsystem (may recurse) +│ │ └── {UnitName} Requirements # unit under a nested subsystem +│ └── {UnitName} Requirements # unit under a root subsystem +└── {UnitName} Requirements # unit directly under the system +OTS Software Requirements # OTS root section (fixed title) +└── {OtsName} Requirements # requirements for one OTS item +Shared Package Requirements # shared package root section (fixed title) +└── {PackageName} Requirements # requirements for one shared package +``` -Use nested sections in `docs/reqstream/ots/` because ReqStream renders the `ots/` -subtree as a distinct section in generated reports, separate from in-house -system requirements: +Each file implements one path through this tree: ```yaml sections: - - title: OTS Software Requirements + - title: '{SystemName} Requirements' sections: - - title: System.Text.Json + - title: '{SubsystemName} Requirements' requirements: - - id: TemplateTool-SystemTextJson-ReadJson - title: System.Text.Json shall be able to read JSON files. - tests: - - JsonReaderTests.TestReadValidJson + - id: System-Subsystem-Feature # Used as-is in all reports - make it readable + title: The subsystem shall perform the required function. + justification: | # ReqStream extracts this into the justifications report (--justifications) + Business rationale and any regulatory references. + tags: # Optional: categorize for filtering with --filter + - security + children: # Optional: ReqStream validates this decomposition chain + - System-Subsystem-Unit-Feat # Downward links only (see requirements-principles.md) + tests: # ReqStream matches these by method name in test results + - TestMethodName + - windows@PlatformSpecificTest # Only test runs on Windows count as evidence +``` + +# Tags (OPTIONAL) + +Tags are free-form - no mandatory vocabulary. Common tags: `security`, `safety`, `performance`, +`compliance`, `reliability`, `critical`. Use `--filter` to selectively export or enforce subsets +(OR logic across comma-separated tags): + +```bash +dotnet reqstream --requirements requirements.yaml \ + --filter security,critical \ + --report docs/requirements_doc/generated/security_requirements.md ``` # Semantic IDs (MANDATORY) @@ -104,28 +133,25 @@ 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 Before submitting requirements, verify: -- [ ] All requirements have semantic IDs (`System-Section-Feature` pattern) -- [ ] Every requirement links to at least one passing test +- [ ] All requirements have semantic IDs (`System-Component-Feature` pattern) +- [ ] Every requirement has a justification explaining business/regulatory need +- [ ] Every requirement links to at least one test - [ ] Platform-specific requirements use source filters (`platform@TestName`) -- [ ] Comprehensive justification explains business/regulatory need -- [ ] Files organized under `docs/reqstream/` following folder structure patterns -- [ ] Subsystem folders use kebab-case naming matching source code -- [ ] OTS requirements placed in `ots/` subfolder -- [ ] Valid YAML syntax passes yamllint validation -- [ ] Test result formats compatible (TRX, JUnit XML) +- [ ] All files and folders use kebab-case names matching source code structure +- [ ] All files are organized under `docs/reqstream/` following the folder structure above diff --git a/.github/standards/requirements-principles.md b/.github/standards/requirements-principles.md index 7d2d572..b6cf136 100644 --- a/.github/standards/requirements-principles.md +++ b/.github/standards/requirements-principles.md @@ -29,6 +29,10 @@ implementation code. - **Valid**: "The parser shall report the line number of the first syntax error." - **Not a requirement (design decision)**: "The parser shall use a `TokenStream` class." +A unit may use its own name freely - that is identity, not HOW. What is +forbidden is describing *internal construction*: class names, method signatures, +algorithms, or data structures. + # Requirements at Every Level (MANDATORY) Every identified subsystem and unit MUST have its own requirements file because diff --git a/.github/standards/reviewmark-usage.md b/.github/standards/reviewmark-usage.md index 5d6219e..b521433 100644 --- a/.github/standards/reviewmark-usage.md +++ b/.github/standards/reviewmark-usage.md @@ -9,7 +9,7 @@ description: Follow these standards when configuring file reviews with ReviewMar Read these standards first before applying this standard: -- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS) +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) ## Purpose @@ -20,7 +20,7 @@ review, organizes them into review-sets, and generates review plans and reports. - **Lint Configuration**: `dotnet reviewmark --lint` - **Elaborate Review-Set**: `dotnet reviewmark --elaborate {review-set}` -- **Generate Plan**: `dotnet reviewmark --plan docs/code_review_plan/plan.md --enforce` +- **Generate Plan**: `dotnet reviewmark --plan docs/code_review_plan/generated/plan.md --enforce` > **Note**: `--enforce` causes the plan to fail with a non-zero exit code if any repository > files are not covered by a review-set. Uncovered files indicate a gap in review-set @@ -31,7 +31,8 @@ review, organizes them into review-sets, and generates review plans and reports. Required repository items for ReviewMark operation: - `.reviewmark.yaml` - Configuration for review-sets, file-patterns, and review evidence-source. -- `docs/code_review_plan/` - Review planning artifacts +- `docs/code_review_plan/generated/` - Generated review plan (build output, do not edit) +- `docs/code_review_report/generated/` - Generated review report (build output, do not edit) # Review Definition Structure @@ -55,10 +56,22 @@ needs-review: - "README.md" # Root level README - "docs/user_guide/**/*.md" # User guide - "docs/design/**/*.md" # Design documentation + - "docs/verification/**/*.md" # Verification design documentation # Source of review evidence evidence-source: type: none + +# Review-sets (each focuses on a single compliance question) +reviews: + - id: Purpose + title: Review of user-facing capabilities and system promises + paths: + - "README.md" + - "docs/user_guide/**/*.md" + - "docs/reqstream/{system-name}.yaml" + - "docs/design/introduction.md" + - "docs/design/{system-name}.md" ``` # Review-Set Design Principles @@ -75,12 +88,11 @@ When constructing review-sets, follow these principles to maintain manageable sc # Review-Set Organization -Organize review-sets using these standard patterns to ensure comprehensive coverage -while keeping each review manageable in scope: - -**Naming conventions**: See `software-items.md` - kebab-case placeholders -(e.g., `{system-name}`) are always kebab-case; cased placeholders -(e.g., `{SystemName}`) follow your language's convention. +**Naming conventions**: Placeholders in documentation, requirements, design, and +verification file paths are kebab-case (e.g., `{system-name}`). Placeholders in +source and test file paths may use the casing conventional for the project's +source language or repository layout (e.g., `{SystemName}`). Review-set name +placeholders are always PascalCase (e.g., `{SystemName}`). ## `Purpose` Review (only one per repository) @@ -93,74 +105,124 @@ Reviews user-facing capabilities and system promises: - **File Path Patterns**: - README: `README.md` - User guide: `docs/user_guide/**/*.md` - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - - System design: `docs/design/{system-name}/{system-name}.md` + - System design: `docs/design/{system-name}.md` -## `{System}-Architecture` Review (one per system) +## `{SystemName}-Architecture` Review (one per system) Reviews system architecture and operational validation: - **Purpose**: Proves that the system is designed and tested to satisfy its requirements -- **Title**: "Review that {System} Architecture Satisfies Requirements" +- **Title**: "Review that {SystemName} Architecture Satisfies Requirements" - **Scope**: Excludes subsystem and unit files, relying on system-level design to describe what subsystems and units it uses - **File Path Patterns**: - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Design introduction: `docs/design/introduction.md` - - System design: `docs/design/{system-name}/{system-name}.md` + - System design: `docs/design/{system-name}.md` + - Verification introduction: `docs/verification/introduction.md` + - System verification design: `docs/verification/{system-name}.md` - System integration tests: `test/{SystemName}.Tests/{SystemName}Tests.{ext}` -## `{System}-Design` Review (one per system) +## `{SystemName}-Design` Review (one per system) Reviews architectural and design consistency: - **Purpose**: Proves the system design is consistent and complete -- **Title**: "Review that {System} Design is Consistent and Complete" +- **Title**: "Review that {SystemName} Design is Consistent and Complete" - **Scope**: Only brings in top-level requirements and relies on brevity of design documentation - **File Path Patterns**: - - System requirements: `docs/reqstream/{system-name}/{system-name}.yaml` + - System requirements: `docs/reqstream/{system-name}.yaml` - Platform requirements: `docs/reqstream/{system-name}/platform-requirements.yaml` - Design introduction: `docs/design/introduction.md` + - System design: `docs/design/{system-name}.md` - System design files: `docs/design/{system-name}/**/*.md` + - OTS overview: `docs/design/ots.md` _(only if OTS items exist)_ + - Shared Package overview: `docs/design/shared.md` _(only if Shared Package items exist)_ -## `{System}-AllRequirements` Review (one per system) +## `{SystemName}-Verification` Review (one per system) + +Reviews verification completeness and consistency: + +- **Purpose**: Proves the system verification design is consistent and covers all requirements +- **Title**: "Review that {SystemName} Verification is Consistent and Complete" +- **Scope**: Only brings in top-level requirements and all verification docs for the system +- **File Path Patterns**: + - System requirements: `docs/reqstream/{system-name}.yaml` + - Verification introduction: `docs/verification/introduction.md` + - System verification: `docs/verification/{system-name}.md` + - System verification files: `docs/verification/{system-name}/**/*.md` + - OTS overview: `docs/verification/ots.md` _(only if OTS items exist)_ + - Shared Package overview: `docs/verification/shared.md` _(only if Shared Package items exist)_ + +## `{SystemName}-AllRequirements` Review (one per system) Reviews requirements quality and traceability: - **Purpose**: Proves the requirements are consistent and complete -- **Title**: "Review that All {System} Requirements are Complete" +- **Title**: "Review that All {SystemName} Requirements are Complete" - **Scope**: Only brings in requirements files to keep review manageable - **File Path Patterns**: - Root requirements: `requirements.yaml` - - System requirements: `docs/reqstream/{system-name}/**/*.yaml` - - OTS requirements: `docs/reqstream/ots/**/*.yaml` (if applicable) + - System requirements: `docs/reqstream/{system-name}.yaml` + - Subsystem/unit requirements: `docs/reqstream/{system-name}/**/*.yaml` -## `{System}-{Subsystem[-Child...]}` Review (one per subsystem at any depth) +## `{SystemName}-{SubsystemName}[-{SubsystemName}...]` Review (one per subsystem at any depth) Reviews subsystem architecture and interfaces: - **Purpose**: Proves that the subsystem is designed and tested to satisfy its requirements -- **Title**: "Review that {System} {Subsystem} Satisfies Subsystem Requirements" +- **Title**: "Review that {SystemName} {SubsystemName} Satisfies Subsystem Requirements" - **Scope**: Excludes units under the subsystem, relying on subsystem design to describe what units it uses - **File Path Patterns**: - - Requirements: `docs/reqstream/{system-name}/.../{subsystem-name}/{subsystem-name}.yaml` - - Design: `docs/design/{system-name}/.../{subsystem-name}/{subsystem-name}.md` - - Tests: `test/{SystemName}.Tests/.../{SubsystemName}/{SubsystemName}Tests.{ext}` + - 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) +## `{SystemName}-{SubsystemName}[-{SubsystemName}...]-{UnitName}` Review (one per unit) Reviews individual software unit implementation: - **Purpose**: Proves the unit is designed, implemented, and tested to satisfy its requirements -- **Title**: "Review that {System} {Subsystem} {Unit} Implementation is Correct" +- **Title**: "Review that {SystemName} {SubsystemName} {UnitName} Implementation is Correct" - **Scope**: Complete unit review including all artifacts - **File Path Patterns**: - - Requirements: `docs/reqstream/{system-name}/.../{unit-name}.yaml` - - Design: `docs/design/{system-name}/.../{unit-name}.md` - - Source: `src/{SystemName}/.../{UnitName}.{ext}` - - Tests: `test/{SystemName}.Tests/.../{UnitName}Tests.{ext}` + - Requirements: `docs/reqstream/{system-name}[/{subsystem-name}...]/{unit-name}.yaml` + - Design: `docs/design/{system-name}[/{subsystem-name}...]/{unit-name}.md` + - Verification design: `docs/verification/{system-name}[/{subsystem-name}...]/{unit-name}.md` + - Source (C# example): `src/{SystemName}[/{SubsystemName}...]/{UnitName}.cs` + - Tests (C# example): `test/{SystemName}.Tests[/{SubsystemName}...]/{UnitName}Tests.cs` + - Source (snake_case C++ example): `src/{system_name}[/{subsystem_name}...]/{unit_name}.cpp` + - Tests (snake_case C++ example): `test/{system_name}_tests[/{subsystem_name}...]/{unit_name}_tests.cpp` + +## `OTS-{OtsName}` Review (one per OTS item) + +Reviews OTS item integration design, requirements, and verification evidence: + +- **Purpose**: Proves that the OTS item provides the required functionality and is correctly integrated +- **Title**: "Review that {OtsName} Provides Required Functionality" +- **Scope**: No local source code; review covers integration design, requirements, and verification evidence +- **File Path Patterns**: + - OTS requirements: `docs/reqstream/ots/{ots-name}.yaml` + - OTS integration design: `docs/design/ots/{ots-name}.md` + - OTS verification: `docs/verification/ots/{ots-name}.md` + - Tests (if applicable): `test/OtsSoftwareTests/...` (C#) or `test/ots_software_tests/...` + (Python/other) — fixed repo-level name, no system prefix + +## `Shared-{PackageName}` Review (one per Shared Package) + +Reviews Shared Package integration design, requirements, and verification evidence: + +- **Purpose**: Proves that the Shared Package provides the required advertised features and is correctly integrated +- **Title**: "Review that {PackageName} Provides Required Features" +- **Scope**: No local source code; review covers integration design, requirements, and verification evidence +- **File Path Patterns**: + - Shared Package requirements: `docs/reqstream/shared/{package-name}.yaml` + - Shared Package integration design: `docs/design/shared/{package-name}.md` + - Shared Package verification: `docs/verification/shared/{package-name}.md` **Note**: File path patterns use `{ext}` as a placeholder for language-specific extensions (`.cs`, `.cpp`/`.hpp`, `.py`, etc.). Adapt to your repository's languages. @@ -171,10 +233,6 @@ Before submitting ReviewMark configuration, verify: - [ ] `.reviewmark.yaml` exists at repository root with proper structure - [ ] Review-set organization follows the standard hierarchy patterns -- [ ] Purpose review-set includes README.md, user guide, system requirements, design introduction, and system design files -- [ ] 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 - [ ] Each review-set focuses on a single compliance question (single focus principle) - [ ] File patterns use correct glob syntax and match intended files - [ ] Review-set file counts remain manageable (context management principle) diff --git a/.github/standards/software-items.md b/.github/standards/software-items.md index bb67b1d..6c29525 100644 --- a/.github/standards/software-items.md +++ b/.github/standards/software-items.md @@ -3,20 +3,16 @@ name: Software Items description: Follow these standards when categorizing software components. --- -# Software Items Definition Standards - -This document defines standards for categorizing software items within -Continuous Compliance environments because proper categorization determines -requirements management approach, testing strategy, and review scope. - # Software Item Categories -Categorize all software into five primary groups: +Categorize all software into six primary groups: - **Software Package**: Distributable unit delivered to end users or dependent systems, containing one software system with all its components. All software - systems are delivered as a software package. When consumed by another system, - our software package is treated as an OTS Software Item by that system. + systems are delivered as a software package. When consumed by a system outside + the producing program, our software package is treated as an OTS Software Item + by that system. When consumed by another repository within the same program, + it is treated as a Shared Package. - **Software System**: Complete deliverable product including all components and external interfaces, contained within a software package - **Software Subsystem**: Major architectural component with well-defined @@ -24,7 +20,11 @@ Categorize all software into five primary groups: - **Software Unit**: Individual class, function, or tightly coupled set of functions that can be tested in isolation - **OTS Software Item**: Third-party component (library, framework, tool, or - published software package) providing functionality not developed in-house + published software package) providing functionality not developed within the program +- **Shared Package**: A software package produced by a different repository within + the same program, consumed as a dependency. Referenced by its advertised features + rather than internal design; traceability to program-level requirements runs + through the top-level project. **Naming**: When names collide in hierarchy, add descriptive suffix to higher-level entity: @@ -34,17 +34,28 @@ Categorize all software into five primary groups: # Naming Conventions in File Path Patterns -Two placeholder styles appear in path patterns across these standards: +Three placeholder forms appear in path patterns across these standards: -- **Kebab-case** (`{system-name}`, `{unit-name}`): always kebab-case - - used in documentation and requirements paths -- **Cased** (`{SystemName}`, `{UnitName}`): follow your language's convention - - `PascalCase` for C#/Java, `snake_case` for C++/Python - - used in source and test file paths +- **Kebab-case** (`{system-name}`, `{unit-name}`): always kebab-case — + documentation and requirements file paths +- **PascalCase IDs** (`{SystemName}`, `{UnitName}`): always PascalCase — + requirements IDs, ReviewMark IDs, and other documentation identifiers +- **Language-cased** (`{SystemName}` or `{system_name}`): follow your language's + convention — `PascalCase` for C#/Java, `snake_case` for C++/Python — + source and test file/folder names -# Categorization Guidelines +## Nesting Depth Notation -Choose the appropriate category based on scope and testability: +Subsystems nest to any depth. Patterns use bracket-ellipsis to express this without +enumerating levels — `[/{subsystem-name}...]` in paths, `[-{SubsystemName}...]` in +dash-separated IDs. Examples covering all three forms: + +- `{SystemName}[-{SubsystemName}...]-{UnitName}-Feature` (PascalCase ID) +- `docs/design/{system-name}[/{subsystem-name}...]/{unit-name}.md` (kebab-case doc path) +- `src/{SystemName}[/{SubsystemName}...]/{UnitName}.cs` (C# source path) +- `src/{system_name}[/{subsystem_name}...]/{unit_name}.cpp` (C++/Python source path) + +# Categorization Guidelines ## Software Package @@ -75,20 +86,47 @@ Choose the appropriate category based on scope and testability: ## OTS Software Item -- External dependency not developed in-house - typically a third-party published +- External dependency from outside the program - typically a third-party published software package (NuGet, npm, etc.), hosted service, or tool -- Our own published software package becomes an OTS item to any system that - consumes it +- A package produced by an unrelated program (inside or outside the organization) + is treated as OTS by any consuming system - Tested through integration tests proving required functionality works - Examples: System.Text.Json, Entity Framework, third-party APIs +- **Artifact locations** (OTS items have no internal design documentation): + - Requirements: `docs/reqstream/ots/{ots-name}.yaml` + - Design: `docs/design/ots/{ots-name}.md` (integration/usage design) + - Verification: `docs/verification/ots/{ots-name}.md` + - These folders sit parallel to system folders (not inside any system folder) +- System design documentation records which OTS items each system depends on +- **OTS test project**: If no other verification evidence is available (e.g., vendor test results, + published compliance reports), a dedicated test project holds OTS integration tests - one test + file per OTS item requiring tests. OTS items are repo-level (not per-system), so the project + uses a fixed repo-level name: `test/OtsSoftwareTests/` (C#) or `test/ots_software_tests/` + (Python/other) — never prefixed with a system or project name. + +## Shared Package + +- A software package produced by a different repository within the same program +- The consuming repository references advertised features, not internal design or source +- Traceability to program-level requirements runs through the top-level project, + not directly between repositories +- Verified through any appropriate approach in the consuming repository - most commonly + downstream integration tests that transitively prove the advertised features are functional +- **Artifact locations** (no internal design documentation in the consuming repository): + - Requirements: `docs/reqstream/shared/{package-name}.yaml` + - Design: `docs/design/shared/{package-name}.md` (integration/usage design) + - Verification: `docs/verification/shared/{package-name}.md` + - These folders sit parallel to system and OTS folders # 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) -- **Source code** - The implementation of the design (in-house units only) +- **Design** - HOW the item satisfies its requirements (full design for local items: system, + subsystem, unit; integration/usage design for OTS and Shared Package) +- **Verification Design** - HOW the requirements will be tested (applies to all item types) +- **Source code** - The implementation of the design (local units only; not applicable to OTS or Shared Package) - **Tests** - PROOF the item does WHAT it is required to do (applies to all item types) diff --git a/.github/standards/technical-documentation.md b/.github/standards/technical-documentation.md index 455b2fd..23893bd 100644 --- a/.github/standards/technical-documentation.md +++ b/.github/standards/technical-documentation.md @@ -1,14 +1,11 @@ --- 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 -This document defines standards for technical documentation within Continuous -Compliance environments. - # Core Principles Technical documentation serves as compliance evidence and must be structured @@ -23,63 +20,28 @@ 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 +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. -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. +The `generated/` folder is **never committed** to the repository - it is created +locally and in CI by the build pipeline. Do not flag its absence as a conformance +issue. **`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,27 +68,47 @@ Include regulatory or business drivers where applicable. Define what is covered and what is explicitly excluded from this documentation. Specify version, system boundaries, and applicability constraints. + +## References + +- [REF-1] Document Title, Author, Version, Date +- [REF-2] Standard Name (e.g., IEEE 12207, ISO 9001) ``` +The `Purpose`, `Scope`, and `References` sections are **unique to `introduction.md`** and must +**not** be replicated in other markdown files within the same document collection. Including them +elsewhere causes duplicate sections in the compiled PDF. + ## Document Ordering -List documents in logical reading order in Pandoc configuration because -readers need coherent information flow from general to specific topics. +List documents in logical reading order in `definition.yaml`. + +## Heading Depth Rule (MANDATORY) + +A file's top-level heading depth must equal its folder depth under the document +collection root - this ensures Pandoc can concatenate all files in `definition.yaml` +order and produce a coherent outline with no heading-shift configuration: + +| Folder depth | Top heading | +| --- | --- | +| 0 - collection root | `#` | +| 1 - one subfolder deep | `##` | +| 2 - two subfolders deep | `###` | +| N - N subfolders deep | `#` × (N+1) | + +Internal sections use the next heading level down (e.g. a `##` file uses `###` +for *Overview*, *Interfaces*, etc.). Deeply nested files have fewer heading levels +available - keep internal structure flat to avoid excessive nesting. # Writing Guidelines Write technical documentation for clarity and compliance verification: - **Clear and Concise**: Use direct language and avoid unnecessary complexity. - Regulatory reviewers must understand content quickly. -- **Structured Sections**: Use consistent heading hierarchy and section - organization. Enables automated processing and review. -- **Specific Examples**: Include concrete examples with actual values rather - than placeholders. Supports implementation verification. +- **Structured Sections**: Use consistent heading hierarchy and section organization. +- **Specific Examples**: Include concrete examples with actual values rather than placeholders. - **Current Information**: Keep documentation synchronized with code changes. - Outdated documentation invalidates compliance evidence. -- **Traceable Content**: Link documentation to requirements and implementation - where applicable for audit trails. +- **Traceable Content**: Link documentation to requirements and implementation where applicable. ## References Sections @@ -135,37 +117,37 @@ 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. + # Markdown Format Requirements -Markdown documentation in this repository must follow the formatting standards -defined in `.markdownlint-cli2.yaml` (subject to any exclusions configured there) -for consistency and professional presentation: - -- **120 Character Line Limit**: Keep lines 120 characters or fewer for readability. - Break long lines naturally at punctuation or logical breaks. -- **No Trailing Whitespace**: Remove all trailing spaces and tabs from line - endings to prevent formatting inconsistencies. -- **Blank Lines Around Headings**: Include a blank line both before and after - each heading to improve document structure and readability. -- **Blank Lines Around Lists**: Include a blank line both before and after - numbered and bullet lists to ensure proper rendering and visual separation. -- **ATX-Style Headers**: Use `#` syntax for headers instead of underline style - for consistency across all documentation. -- **Consistent List Indentation**: Use 2-space indentation for nested list - items to maintain uniform formatting. +Follow `.markdownlint-cli2.yaml` formatting standards: + +- **120 Character Line Limit**: Keep lines 120 characters or fewer; break at punctuation or logical breaks. +- **No Trailing Whitespace**: Remove all trailing spaces and tabs. +- **Blank Lines Around Headings**: Include a blank line before and after each heading. +- **Blank Lines Around Lists**: Include a blank line before and after numbered and bullet lists. +- **ATX-Style Headers**: Use `#` syntax, not underline style. +- **Consistent List Indentation**: Use 2-space indentation for nested list items. # 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 -- **Tool Integration**: Generated content integrates with CI/CD pipelines and - manual changes disrupt automation +- **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 # README.md Best Practices @@ -188,20 +170,12 @@ Structure README.md for both human readers and AI agent processing: - **Code Block Languages**: Specify language for syntax highlighting and tool processing - **Clear Prerequisites**: List exact version requirements and dependencies -## Quality Guidelines - -- **Scannable Structure**: Use bullet points, headings, and short paragraphs -- **Current Examples**: Verify all code examples work with current version -- **Link Validation**: Ensure all external links are accessible and current -- **Consistent Tone**: Professional, helpful tone appropriate for technical audience - # Quality Checks Before submitting technical documentation, verify: - [ ] Documentation organized under `docs/` following standard folder structure - [ ] Pandoc collections include `introduction.md` with Purpose and Scope sections -- [ ] Content follows clear and concise writing guidelines with specific examples - [ ] No modifications made to auto-generated markdown files in compliance folders - [ ] README.md includes all required sections with absolute URLs and concrete examples - [ ] Documentation integrated into ReviewMark review-sets for formal review diff --git a/.github/standards/testing-principles.md b/.github/standards/testing-principles.md index 73974ff..917463e 100644 --- a/.github/standards/testing-principles.md +++ b/.github/standards/testing-principles.md @@ -3,11 +3,6 @@ name: Testing Principles description: Follow these standards when developing any software tests. --- -# Testing Principles Standards - -This document defines universal testing principles and quality standards for test development within -Continuous Compliance environments. - # Test Dependency Boundaries (MANDATORY) Respect software item hierarchy boundaries to ensure review-sets can validate proper architectural scope. diff --git a/.github/standards/verification-documentation.md b/.github/standards/verification-documentation.md new file mode 100644 index 0000000..494e40f --- /dev/null +++ b/.github/standards/verification-documentation.md @@ -0,0 +1,101 @@ +--- +name: Verification Documentation +description: Follow these standards when creating software verification design documentation. +globs: ["docs/verification/**/*.md"] +--- + +# Required Standards + +- **`technical-documentation.md`** - General technical documentation standards +- **`software-items.md`** - Software categorization (System/Subsystem/Unit/OTS/Shared Package) + +# Folder Structure + +```text +docs/verification/ +├── introduction.md # heading depth # +├── {system-name}.md # heading depth # +├── {system-name}/ +│ ├── {subsystem-name}.md # heading depth ## +│ ├── {subsystem-name}/ +│ │ └── {unit-name}.md # heading depth ### +│ └── {unit-name}.md # heading depth ## +├── ots.md # heading depth # (if OTS items exist) +├── ots/ +│ └── {ots-name}.md # heading depth ## +├── shared.md # heading depth # (if Shared Packages exist) +└── shared/ + └── {package-name}.md # heading depth ## +``` + +All sections in every file are mandatory; write "N/A - {justification}" rather than removing any. +Determine subsystem vs. unit classification from `docs/design/introduction.md` — folder depth does not determine classification. + +# introduction.md (MANDATORY) + +Must include: + +- **Purpose**: audience and compliance drivers +- **Scope**: items covered and explicitly excluded (no test projects) +- **Companion Artifact Structure**: parallel paths for requirements, design, verification, source, tests +- **References** _(if applicable)_: external standards or specifications - only in `introduction.md` + +# System Verification Design (MANDATORY) + +Create `{system-name}.md` (`#` heading) and `{system-name}/` folder: + +- **Verification Approach**: test types (unit, integration, end-to-end), framework, project structure +- **Test Environment**: OS, runtime, external services, files, or configuration required +- **Acceptance Criteria**: what constitutes a passing system test (IEC 62304 §5.7.2) +- **Test Scenarios**: named scenarios for each system requirement + +# Subsystem Verification Design (MANDATORY) + +Place `{subsystem-name}.md` in the **parent** folder; create `{subsystem-name}/` for children: + +- **Verification Approach**: integration test approach and mocking at subsystem boundary +- **Test Environment**: any environment setup beyond the standard test runner +- **Acceptance Criteria**: what constitutes a passing subsystem test (IEC 62304 §5.5.2) +- **Test Scenarios**: named scenarios including boundary conditions, error paths, and normal operation + +# Unit Verification Design (MANDATORY) + +Place `{unit-name}.md` in the **parent** folder: + +- **Verification Approach**: what is mocked/stubbed and why; injected vs. real dependencies +- **Test Environment**: any environment setup beyond the standard test runner +- **Acceptance Criteria**: what constitutes passing unit tests (IEC 62304 §5.5.2) +- **Test Scenarios**: named scenarios including boundary values, error paths, and normal operation + +# OTS Verification Evidence (when OTS items exist) + +Create `docs/verification/ots.md` (`#` heading) covering the overall OTS verification strategy. + +For each OTS item, create `docs/verification/ots/{ots-name}.md` (`##` heading) covering: +verification approach (self-validation, integration tests, vendor evidence). + +# Shared Package Verification Evidence (when Shared Packages exist) + +Create `docs/verification/shared.md` (`#` heading) covering the overall Shared Package verification strategy. + +For each Shared Package, create `docs/verification/shared/{package-name}.md` (`##` heading) covering: +verification approach. + +# Writing Guidelines + +- Name scenarios clearly ("Valid input returns parsed result", not "Test 1") +- Use verbal cross-references - not markdown hyperlinks (break in PDF) +- Use Mermaid diagrams to supplement (not replace) text + +# Quality Checks + +- [ ] `introduction.md` includes Companion Artifact Structure +- [ ] Each file's heading depth matches its folder depth +- [ ] All folders use kebab-case mirroring source structure +- [ ] Each system/subsystem/unit file includes all mandatory sections (Verification Approach, + Test Environment, Acceptance Criteria, Test Scenarios) +- [ ] Non-applicable mandatory sections contain "N/A - {justification}" +- [ ] Requirements-to-test coverage is tracked via the ReqStream trace matrix, not in these documents +- [ ] `docs/verification/ots.md` and `docs/verification/ots/{ots-name}.md` exist when OTS items are present +- [ ] `docs/verification/shared.md` and `docs/verification/shared/{package-name}.md` exist when Shared Packages are present +- [ ] Documents are integrated into ReviewMark review-sets diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3dbebb9..1ef2d02 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -356,9 +356,18 @@ jobs: dotnet versionmark --capture --job-id "build-docs" \ --output "artifacts/versionmark-build-docs.json" -- \ dotnet git node npm pandoc weasyprint sarifmark sonarmark reqstream \ - buildmark versionmark reviewmark fileassert + 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. @@ -366,6 +375,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 @@ -385,20 +398,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" @@ -406,7 +419,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 @@ -416,14 +429,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/SpdxModel Build Notes.pdf" + docs/build_notes/generated/build_notes.html + "docs/generated/SpdxModel Build Notes.pdf" - name: Assert Build Notes Documents with FileAssert run: > @@ -431,6 +444,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. @@ -438,6 +455,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 @@ -454,7 +475,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 "SpdxModel CodeQL Analysis" --report-depth 1 @@ -462,7 +483,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 @@ -474,14 +495,14 @@ jobs: --project-key demaconsulting_SpdxModel --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 @@ -491,14 +512,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/SpdxModel Code Quality.pdf" + docs/code_quality/generated/quality.html + "docs/generated/SpdxModel Code Quality Report.pdf" - name: Assert Code Quality Documents with FileAssert run: > @@ -513,6 +534,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 @@ -524,22 +549,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 @@ -549,14 +574,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/SpdxModel Review Plan.pdf" + docs/code_review_plan/generated/plan.html + "docs/generated/SpdxModel Code Review Plan.pdf" - name: Generate Review Report HTML with Pandoc shell: bash @@ -566,14 +591,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/SpdxModel Review Report.pdf" + docs/code_review_report/generated/report.html + "docs/generated/SpdxModel Code Review Report.pdf" - name: Assert Code Review Documents with FileAssert run: > @@ -586,6 +611,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: > @@ -594,14 +623,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/SpdxModel Software Design.pdf" + docs/design/generated/design.html + "docs/generated/SpdxModel Software Design Document.pdf" - name: Assert Design Documents with FileAssert run: > @@ -609,11 +638,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/SpdxModel Verification Design Document.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: > @@ -622,14 +686,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/SpdxModel User Guide.pdf" + docs/user_guide/generated/user_guide.html + "docs/generated/SpdxModel User Guide.pdf" - name: Assert User Guide Documents with FileAssert run: > @@ -638,8 +702,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. @@ -659,6 +723,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 @@ -670,9 +738,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 @@ -683,14 +751,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/SpdxModel Requirements.pdf" + docs/requirements_doc/generated/requirements.html + "docs/generated/SpdxModel Requirements Document.pdf" - name: Generate Trace Matrix HTML with Pandoc shell: bash @@ -700,14 +768,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/SpdxModel Trace Matrix.pdf" + docs/requirements_report/generated/trace_matrix.html + "docs/generated/SpdxModel Trace Matrix.pdf" - name: Assert Requirements Documents with FileAssert run: > @@ -723,6 +791,4 @@ jobs: uses: actions/upload-artifact@v7 with: name: documents - path: |- - docs/*.pdf - docs/build_notes.md + path: docs/generated/* diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index c16c443..b65f6d8 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -52,3 +52,4 @@ ignores: - "**/3rd-party/**" - "**/AGENT_REPORT_*.md" - "**/.agent-logs/**" + - "artifacts/**" diff --git a/.reviewmark.yaml b/.reviewmark.yaml index 2330a74..28a22d4 100644 --- a/.reviewmark.yaml +++ b/.reviewmark.yaml @@ -11,6 +11,7 @@ needs-review: - "**/*.cs" # All C# source and test files - "docs/reqstream/**/*.yaml" # Requirements files - "docs/design/**/*.md" # Design documentation files + - "docs/verification/**/*.md" # Verification documentation files - "docs/user_guide/**/*.md" # User guide documentation - "!**/obj/**" # Exclude build output - "!**/bin/**" # Exclude build output @@ -43,6 +44,8 @@ reviews: - "docs/reqstream/spdx-model/spdx-model.yaml" - "docs/design/introduction.md" - "docs/design/spdx-model/spdx-model.md" + - "docs/verification/introduction.md" + - "docs/verification/spdx-model/spdx-model.md" - "test/DemaConsulting.SpdxModel.Tests/SpdxModelTests.cs" - id: SpdxModel-Design @@ -52,6 +55,8 @@ reviews: - "docs/reqstream/spdx-model/platform-requirements.yaml" - "docs/design/introduction.md" - "docs/design/spdx-model/**/*.md" + - "docs/verification/introduction.md" + - "docs/verification/spdx-model/**/*.md" - id: SpdxModel-AllRequirements title: Review that All SpdxModel Requirements are Complete @@ -65,6 +70,7 @@ reviews: paths: - "docs/reqstream/spdx-model/io/io.yaml" - "docs/design/spdx-model/io/io.md" + - "docs/verification/spdx-model/io/io.md" - "test/DemaConsulting.SpdxModel.Tests/IO/SpdxModelIOTests.cs" - id: SpdxModel-IO-Spdx2JsonDeserializer @@ -73,6 +79,8 @@ reviews: - "docs/reqstream/spdx-model/io/spdx-2-json-deserializer.yaml" - "docs/design/spdx-model/io/spdx-2-json-deserializer.md" - "docs/design/spdx-model/io/spdx-constants.md" + - "docs/verification/spdx-model/io/spdx-2-json-deserializer.md" + - "docs/verification/spdx-model/io/spdx-constants.md" - "src/DemaConsulting.SpdxModel/IO/Spdx2JsonDeserializer.cs" - "src/DemaConsulting.SpdxModel/IO/SpdxConstants.cs" - "test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize*.cs" @@ -83,6 +91,8 @@ reviews: - "docs/reqstream/spdx-model/io/spdx-2-json-serializer.yaml" - "docs/design/spdx-model/io/spdx-2-json-serializer.md" - "docs/design/spdx-model/io/spdx-constants.md" + - "docs/verification/spdx-model/io/spdx-2-json-serializer.md" + - "docs/verification/spdx-model/io/spdx-constants.md" - "src/DemaConsulting.SpdxModel/IO/Spdx2JsonSerializer.cs" - "src/DemaConsulting.SpdxModel/IO/SpdxConstants.cs" - "test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerialize*.cs" @@ -92,6 +102,7 @@ reviews: paths: - "docs/reqstream/spdx-model/transform/transform.yaml" - "docs/design/spdx-model/transform/transform.md" + - "docs/verification/spdx-model/transform/transform.md" - "test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxModelTransformTests.cs" - id: SpdxModel-Transform-SpdxRelationships @@ -99,6 +110,7 @@ reviews: paths: - "docs/reqstream/spdx-model/transform/spdx-relationships.yaml" - "docs/design/spdx-model/transform/spdx-relationships.md" + - "docs/verification/spdx-model/transform/spdx-relationships.md" - "src/DemaConsulting.SpdxModel/Transform/SpdxRelationships.cs" - "test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxRelationshipsTests.cs" @@ -107,6 +119,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-annotation.yaml" - "docs/design/spdx-model/spdx-annotation.md" + - "docs/verification/spdx-model/spdx-annotation.md" - "src/DemaConsulting.SpdxModel/SpdxAnnotation.cs" - "src/DemaConsulting.SpdxModel/SpdxAnnotationType.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxAnnotationTests.cs" @@ -116,6 +129,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-checksum.yaml" - "docs/design/spdx-model/spdx-checksum.md" + - "docs/verification/spdx-model/spdx-checksum.md" - "src/DemaConsulting.SpdxModel/SpdxChecksum.cs" - "src/DemaConsulting.SpdxModel/SpdxChecksumAlgorithm.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxChecksumTests.cs" @@ -125,6 +139,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-creation-information.yaml" - "docs/design/spdx-model/spdx-creation-information.md" + - "docs/verification/spdx-model/spdx-creation-information.md" - "src/DemaConsulting.SpdxModel/SpdxCreationInformation.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxCreationInformationTests.cs" @@ -133,6 +148,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-document.yaml" - "docs/design/spdx-model/spdx-document.md" + - "docs/verification/spdx-model/spdx-document.md" - "src/DemaConsulting.SpdxModel/SpdxDocument.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxDocumentTests.cs" @@ -141,13 +157,16 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-element.yaml" - "docs/design/spdx-model/spdx-element.md" + - "docs/verification/spdx-model/spdx-element.md" - "src/DemaConsulting.SpdxModel/SpdxElement.cs" + - "test/DemaConsulting.SpdxModel.Tests/SpdxElementTests.cs" - id: SpdxModel-SpdxExternalDocumentReference title: Review that SpdxModel SpdxExternalDocumentReference Implementation is Correct paths: - "docs/reqstream/spdx-model/spdx-external-document-reference.yaml" - "docs/design/spdx-model/spdx-external-document-reference.md" + - "docs/verification/spdx-model/spdx-external-document-reference.md" - "src/DemaConsulting.SpdxModel/SpdxExternalDocumentReference.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxExternalDocumentReferenceTests.cs" @@ -156,6 +175,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-external-reference.yaml" - "docs/design/spdx-model/spdx-external-reference.md" + - "docs/verification/spdx-model/spdx-external-reference.md" - "src/DemaConsulting.SpdxModel/SpdxExternalReference.cs" - "src/DemaConsulting.SpdxModel/SpdxReferenceCategory.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxExternalReferenceTests.cs" @@ -165,6 +185,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-extracted-licensing-info.yaml" - "docs/design/spdx-model/spdx-extracted-licensing-info.md" + - "docs/verification/spdx-model/spdx-extracted-licensing-info.md" - "src/DemaConsulting.SpdxModel/SpdxExtractedLicensingInfo.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxExtractedLicensingInfoTests.cs" @@ -173,6 +194,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-file.yaml" - "docs/design/spdx-model/spdx-file.md" + - "docs/verification/spdx-model/spdx-file.md" - "src/DemaConsulting.SpdxModel/SpdxFile.cs" - "src/DemaConsulting.SpdxModel/SpdxFileType.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxFileTests.cs" @@ -182,6 +204,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-helpers.yaml" - "docs/design/spdx-model/spdx-helpers.md" + - "docs/verification/spdx-model/spdx-helpers.md" - "src/DemaConsulting.SpdxModel/SpdxHelpers.cs" - id: SpdxModel-SpdxLicenseElement @@ -189,6 +212,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-license-element.yaml" - "docs/design/spdx-model/spdx-license-element.md" + - "docs/verification/spdx-model/spdx-license-element.md" - "src/DemaConsulting.SpdxModel/SpdxLicenseElement.cs" - id: SpdxModel-SpdxPackage @@ -196,6 +220,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-package.yaml" - "docs/design/spdx-model/spdx-package.md" + - "docs/verification/spdx-model/spdx-package.md" - "src/DemaConsulting.SpdxModel/SpdxPackage.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxPackageTests.cs" @@ -204,6 +229,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-package-verification-code.yaml" - "docs/design/spdx-model/spdx-package-verification-code.md" + - "docs/verification/spdx-model/spdx-package-verification-code.md" - "src/DemaConsulting.SpdxModel/SpdxPackageVerificationCode.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxPackageVerificationCodeTests.cs" @@ -212,6 +238,7 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-relationship.yaml" - "docs/design/spdx-model/spdx-relationship.md" + - "docs/verification/spdx-model/spdx-relationship.md" - "src/DemaConsulting.SpdxModel/SpdxRelationship.cs" - "src/DemaConsulting.SpdxModel/SpdxRelationshipType.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxRelationshipTests.cs" @@ -221,5 +248,6 @@ reviews: paths: - "docs/reqstream/spdx-model/spdx-snippet.yaml" - "docs/design/spdx-model/spdx-snippet.md" + - "docs/verification/spdx-model/spdx-snippet.md" - "src/DemaConsulting.SpdxModel/SpdxSnippet.cs" - "test/DemaConsulting.SpdxModel.Tests/SpdxSnippetTests.cs" diff --git a/AGENTS.md b/AGENTS.md index 7249295..9107b89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,15 @@ +# Project Overview + +- **project-name**: `SpdxModel` +- **organization**: DEMA Consulting +- **project-tagline**: SPDX document model for .NET +- **description**: SpdxModel is a modern C# library for working with SPDX (Software Package Data + Exchange) documents. It provides a comprehensive in-memory model for reading, manipulating, and + writing SPDX Software Bill of Materials (SBOM) files in JSON format, with full support for SPDX + 2.2 and 2.3 specifications. +- **languages**: `C#` +- **technologies**: `.NET`, `.NET Standard 2.0`, `NuGet` + # Project Structure ```text @@ -10,13 +22,27 @@ │ ├── requirements_doc/ │ ├── requirements_report/ │ ├── reqstream/ -│ └── user_guide/ +│ ├── user_guide/ +│ └── verification/ ├── src/ │ └── DemaConsulting.SpdxModel/ └── test/ └── DemaConsulting.SpdxModel.Tests/ ``` +# Language and Spelling (ALL Agents) + +Always use **US English** spelling in all output (code, comments, documentation, +commit messages, and reports). + +# Reference Template + +This repository follows a reference template for structure and file conventions. + +- **Template URL**: `https://github.com/demaconsulting/Agents/raw/refs/heads/template` +- **Repository map**: `{template-url}/repository-map.md` +- **Template files**: `{template-url}/{file-path}` for files described in the map + # Codebase Navigation (ALL Agents) When working with source code, design, or requirements artifacts, read @@ -45,16 +71,15 @@ 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` | +- **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 +94,11 @@ 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 TemplateDotNetLibrary template patterns and best practices +- **Structural audit**: (repository layout vs. template) → Call the template-sync agent # 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 +117,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 +125,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 +134,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 bfb0f58..a617ade 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -126,7 +126,7 @@ Note the spaces after `///` for proper indentation in summary blocks. ### Test Framework -We use MSTest v4 for unit and integration tests. +We use xUnit v3 for unit and integration tests. ### Test Naming Convention @@ -141,10 +141,10 @@ Examples: ### Writing Tests - Write tests that are clear and focused -- Use modern MSTest v4 assertions: - - `Assert.HasCount(expectedCount, collection)` - - `Assert.IsEmpty(collection)` - - `Assert.DoesNotContain(item, collection)` +- Use xUnit v3 assertions: + - `Assert.Equal(expected, actual)` + - `Assert.Contains(expected, collection)` + - `Assert.Throws(() => action())` - Always clean up resources (use `try/finally` for console redirection) - Link tests to requirements in `requirements.yaml` when applicable diff --git a/README.md b/README.md index b86e4a2..fd169c2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ comprehensive in-memory model for reading, manipulating, and writing SPDX Softwa - 🔄 **JSON Serialization** - Read and write SPDX documents in JSON format - 🎯 **Type-Safe** - Strongly-typed C# API with nullable reference types - 🔍 **Transform Support** - Built-in utilities for manipulating SPDX relationships +- 🔁 **Deep Copy Support** - Deep copy any SPDX element or entire documents +- ⚖️ **Comparison Utilities** - Compare and check equality of SPDX elements +- ✅ **Validation Support** - Validate SPDX documents and elements against the specification - ⚡ **Multi-Target** - Supports .NET Standard 2.0, .NET 8, 9, and 10 - 🖥️ **Multi-Platform** - Builds and runs on Windows, Linux, and macOS - 🧪 **Well-Tested** - Comprehensive test suite with high code coverage @@ -70,6 +73,7 @@ var document = new SpdxDocument Id = "SPDXRef-DOCUMENT", Name = "My Software", Version = "SPDX-2.3", + DataLicense = "CC0-1.0", DocumentNamespace = "https://example.com/my-software", CreationInformation = new SpdxCreationInformation { @@ -127,6 +131,12 @@ var rootPackages = document.GetRootPackages(); - **`SpdxSnippet`** - Represents a code snippet - **`SpdxRelationship`** - Represents relationships between elements - **`SpdxCreationInformation`** - Document creation metadata +- **`SpdxAnnotation`** - Represents document annotations +- **`SpdxChecksum`** - Represents element checksums +- **`SpdxExternalDocumentReference`** - Represents external document references +- **`SpdxExternalReference`** - Represents external references on packages +- **`SpdxExtractedLicensingInfo`** - Represents extracted licensing information +- **`SpdxPackageVerificationCode`** - Represents package verification codes ### Serialization diff --git a/docs/build_notes/definition.yaml b/docs/build_notes/definition.yaml index 207a375..99131c3 100644 --- a/docs/build_notes/definition.yaml +++ b/docs/build_notes/definition.yaml @@ -1,12 +1,10 @@ --- -resource-path: - - docs/build_notes - - docs/template +resource-path: [docs/build_notes, docs/template] 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 # Generated by BuildMark (git history, release notes) + - docs/build_notes/generated/versions.md # Generated by VersionMark (tool versions) template: template.html table-of-contents: true number-sections: true diff --git a/docs/build_notes/introduction.md b/docs/build_notes/introduction.md index c438381..f3f9050 100644 --- a/docs/build_notes/introduction.md +++ b/docs/build_notes/introduction.md @@ -1,33 +1,23 @@ # Introduction -This document contains the build notes for the SpdxModel project. +This document contains the build notes for SpdxModel. ## Purpose -This report serves as a comprehensive record of changes and bug fixes for this -release of SpdxModel. It provides transparency about what has changed since the -previous version and helps users understand the improvements and fixes included -in this build. +This document provides a record of the changes, new features, and bug fixes included +in this release of SpdxModel. It also records the versions of all tools used in +the build pipeline, providing traceability between the software artifacts and the +environment that produced them. ## Scope This build notes report covers: -- Version information and commit details -- Changes and new features implemented +- Version information and commit details for this release +- Changes and new features implemented since the previous version - Bugs fixed in this release +- Versions of all tools used in the build and compliance pipeline -## Generation Source +## References -This report is automatically generated by the BuildMark tool, analyzing the -Git repository history and issue tracking information. It provides evidence of -changes made to the SpdxModel library for SPDX SBOM manipulation. - -## Audience - -This document is intended for: - -- Software developers working on SpdxModel -- Users evaluating what has changed in this release -- Project stakeholders tracking progress -- Contributors understanding recent changes +- [SpdxModel releases](https://github.com/demaconsulting/SpdxModel/releases) diff --git a/docs/build_notes/title.txt b/docs/build_notes/title.txt index 906cef4..4024195 100644 --- a/docs/build_notes/title.txt +++ b/docs/build_notes/title.txt @@ -1,16 +1,14 @@ --- -title: SpdxModel -subtitle: Build Notes -author: DEMA Consulting -description: Build notes for the SpdxModel library for SPDX SBOM manipulation +title: "SpdxModel Build Notes" +subtitle: "SPDX document model for .NET" +author: "DEMA Consulting" +description: "Build Notes for SpdxModel" lang: en-US keywords: - - SpdxModel - Build Notes - - Release Notes + - SpdxModel - SPDX - SBOM - C# - .NET - - Documentation --- diff --git a/docs/code_quality/definition.yaml b/docs/code_quality/definition.yaml index 68c58f2..f1dde0b 100644 --- a/docs/code_quality/definition.yaml +++ b/docs/code_quality/definition.yaml @@ -1,12 +1,10 @@ --- -resource-path: - - docs/code_quality - - docs/template +resource-path: [docs/code_quality, docs/template] input-files: - docs/code_quality/title.txt - docs/code_quality/introduction.md - - docs/code_quality/codeql-quality.md - - docs/code_quality/sonar-quality.md + - docs/code_quality/generated/codeql-quality.md + - docs/code_quality/generated/sonar-quality.md template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_quality/introduction.md b/docs/code_quality/introduction.md index a28a185..d05fe7d 100644 --- a/docs/code_quality/introduction.md +++ b/docs/code_quality/introduction.md @@ -1,35 +1,17 @@ # Introduction -This document contains the code quality analysis report for the SpdxModel project. +This document records the static analysis results for SpdxModel. ## Purpose -This report serves as evidence that the SpdxModel codebase maintains good quality -standards. It provides a comprehensive analysis of code quality metrics, including -quality gate status, code issues, security hotspots, technical debt, and code coverage. +To provide evidence that SpdxModel has been analyzed for code quality issues and that any findings have been +reviewed and resolved or accepted. ## Scope -This code quality report covers: +Covers static analysis of all source code in `src/` for SpdxModel using CodeQL (SARIF) and SonarCloud. +Test code is excluded from static analysis requirements. -- Quality gate status and conditions -- Code issues categorized by type and severity -- Security hotspots requiring review -- Technical debt assessment -- Code coverage and duplication metrics +## References -## Analysis Source - -This report contains quality analysis results captured at the time this version of SpdxModel -was built. It serves as evidence that the code maintains good quality standards and provides -transparency about the project's code health. The analysis includes results from various -quality tools run during the build process. - -## Audience - -This document is intended for: - -- Software developers working on SpdxModel -- Quality assurance teams reviewing code quality -- Project stakeholders evaluating project health -- Contributors understanding quality standards +- [SpdxModel releases](https://github.com/demaconsulting/SpdxModel/releases) diff --git a/docs/code_quality/title.txt b/docs/code_quality/title.txt index 28673de..e7611b3 100644 --- a/docs/code_quality/title.txt +++ b/docs/code_quality/title.txt @@ -1,15 +1,15 @@ --- title: SpdxModel Code Quality -subtitle: Code Quality Report +subtitle: SPDX document model for .NET author: DEMA Consulting -description: Code Quality Report for the SpdxModel C# library for serializing and deserializing SPDX SBOMs +description: Code Quality Report for SpdxModel lang: en-US keywords: - SpdxModel - Code Quality - - SonarCloud + - Static Analysis - CodeQL - - Analysis + - SonarCloud - C# - .NET - SPDX diff --git a/docs/code_review_plan/definition.yaml b/docs/code_review_plan/definition.yaml index 3a24f0b..5b612b8 100644 --- a/docs/code_review_plan/definition.yaml +++ b/docs/code_review_plan/definition.yaml @@ -1,11 +1,9 @@ --- -resource-path: - - docs/code_review_plan - - docs/template +resource-path: [docs/code_review_plan, docs/template] 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 # Generated by ReviewMark (formal review plan) template: template.html table-of-contents: true number-sections: true diff --git a/docs/code_review_plan/introduction.md b/docs/code_review_plan/introduction.md index 2cfb085..829bbf5 100644 --- a/docs/code_review_plan/introduction.md +++ b/docs/code_review_plan/introduction.md @@ -1,33 +1,16 @@ # Introduction -This document contains the review plan for the SpdxModel project. +This document defines the formal code review plan for SpdxModel. ## Purpose -This review plan provides a comprehensive overview of all files requiring formal review -in the SpdxModel project. It identifies which review-sets cover which -files and serves as evidence that every file requiring review is covered by at least -one named review-set. +To define review-sets and the evidence required to demonstrate that each software item +has been reviewed according to the project's compliance requirements. ## Scope -This review plan covers: +This document covers all review-sets defined in `.reviewmark.yaml` for SpdxModel. -- C# source code files requiring formal review -- YAML configuration and requirements files requiring formal review -- Mapping of reviewed files to named review-sets +## References -## Generation Source - -This plan is automatically generated by the ReviewMark tool, analyzing the -`.reviewmark.yaml` configuration and the review evidence store. It serves as evidence -that every file requiring review is covered by a current, valid review. - -## Audience - -This document is intended for: - -- Software developers working on SpdxModel -- Quality assurance teams validating review coverage -- Project stakeholders reviewing compliance status -- Auditors verifying that all required files have been reviewed +- [SpdxModel releases](https://github.com/demaconsulting/SpdxModel/releases) diff --git a/docs/code_review_plan/title.txt b/docs/code_review_plan/title.txt index 227b7ca..b9c6e94 100644 --- a/docs/code_review_plan/title.txt +++ b/docs/code_review_plan/title.txt @@ -1,13 +1,13 @@ --- -title: SpdxModel Review Plan -subtitle: File Review Plan for the SpdxModel Library -author: DEMA Consulting -description: File Review Plan for the SpdxModel Library +title: "SpdxModel Code Review Plan" +subtitle: "SPDX document model for .NET" +author: "DEMA Consulting" +description: "Code Review Plan for SpdxModel" lang: en-US keywords: + - Code Review - SpdxModel - - Review Plan - - File Reviews + - C# - .NET - - Library + - SPDX --- diff --git a/docs/code_review_report/definition.yaml b/docs/code_review_report/definition.yaml index 6498e6c..bc8c94d 100644 --- a/docs/code_review_report/definition.yaml +++ b/docs/code_review_report/definition.yaml @@ -1,11 +1,9 @@ --- -resource-path: - - docs/code_review_report - - docs/template +resource-path: [docs/code_review_report, docs/template] 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 # Generated by ReviewMark (completed review records) 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 e834541..4a432e3 100644 --- a/docs/code_review_report/introduction.md +++ b/docs/code_review_report/introduction.md @@ -1,33 +1,16 @@ # Introduction -This document contains the review report for the SpdxModel project. +This document records the completed formal code reviews for SpdxModel. ## Purpose -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 SpdxModel remain valid for the current state of the -reviewed files. +To provide compliance evidence that all required review-sets have been reviewed +and approved according to the project's review plan. ## Scope -This review report covers: +This document covers all completed reviews for review-sets defined in `.reviewmark.yaml`. -- Current review-set status (current, stale, or missing) -- File fingerprints and review evidence matching -- Review coverage verification +## References -## Generation Source - -This report is automatically generated by the ReviewMark tool, comparing the current -file fingerprints against the review evidence store. It serves as evidence that all -review-sets are current and no reviewed file has changed since its review was conducted. - -## Audience - -This document is intended for: - -- Software developers working on SpdxModel -- Quality assurance teams validating review currency -- Project stakeholders reviewing compliance status -- Auditors verifying that all reviews remain valid for the current release +- [SpdxModel releases](https://github.com/demaconsulting/SpdxModel/releases) diff --git a/docs/code_review_report/title.txt b/docs/code_review_report/title.txt index 029a5d6..4ab7a5a 100644 --- a/docs/code_review_report/title.txt +++ b/docs/code_review_report/title.txt @@ -1,13 +1,13 @@ --- -title: SpdxModel Review Report -subtitle: File Review Report for the SpdxModel Library -author: DEMA Consulting -description: File Review Report for the SpdxModel Library +title: "SpdxModel Code Review Report" +subtitle: "SPDX document model for .NET" +author: "DEMA Consulting" +description: "Code Review Report for SpdxModel" lang: en-US keywords: + - Code Review - SpdxModel - - Review Report - - File Reviews + - SPDX - .NET - - Library + - C# --- diff --git a/docs/design/definition.yaml b/docs/design/definition.yaml index da2bd1d..17f069a 100644 --- a/docs/design/definition.yaml +++ b/docs/design/definition.yaml @@ -1,11 +1,9 @@ --- -resource-path: - - docs/design - - docs/template +resource-path: [docs/design, docs/template] input-files: - docs/design/title.txt - docs/design/introduction.md - - docs/design/spdx-model/spdx-model.md + - docs/design/spdx-model.md - docs/design/spdx-model/spdx-document.md - docs/design/spdx-model/spdx-element.md - docs/design/spdx-model/spdx-package.md diff --git a/docs/design/introduction.md b/docs/design/introduction.md index 0d0ce7a..e62ab4a 100644 --- a/docs/design/introduction.md +++ b/docs/design/introduction.md @@ -1,25 +1,28 @@ -# DemaConsulting.SpdxModel Design Documentation +# Introduction + +SpdxModel is a .NET library for working with SPDX (Software Package Data Exchange) documents. +It provides a comprehensive in-memory model for reading, manipulating, and writing SPDX Software +Bill of Materials (SBOM) files in JSON format. The library is organized into a root data model +system with IO and Transform subsystems. ## Purpose -This document provides the design overview for the DemaConsulting.SpdxModel library, a .NET library -for reading, writing, and manipulating SPDX (Software Package Data Exchange) documents. It serves as -the entry point for the design documentation, providing architectural context for formal code review, -compliance auditing, and maintenance support. +This document defines the design for each software item in SpdxModel — full architectural and +detailed design for local items (systems, subsystems, and units). A reviewer should be able to +understand how each item satisfies its requirements without reading source code. ## Scope -This design documentation covers the DemaConsulting.SpdxModel library, including: +Local items: -- The SPDX data model (documents, packages, files, snippets, relationships, annotations, checksums, etc.) -- JSON serialization and deserialization (SPDX 2.2 and SPDX 2.3) -- Relationship manipulation utilities +- **SpdxModel**: system, subsystem, and unit design. -Excluded from scope: +Out of scope: -- Consumer application code using this library -- CI/CD pipeline configuration +- Test projects +- Build pipeline - NuGet package distribution infrastructure +- The internal design of OTS software items ## Software Structure @@ -48,92 +51,91 @@ DemaConsulting.SpdxModel (System) └── SpdxSnippet (Unit) ``` -OTS Software Items: - -- MSTest — unit test framework -- ReqStream — requirements traceability enforcement -- BuildMark — build notes documentation generation -- VersionMark — tool version documentation -- SarifMark — CodeQL SARIF report generation -- SonarMark — SonarCloud quality report generation - ## Folder Layout -```text -docs/design/ -├── introduction.md -└── spdx-model/ - ├── spdx-model.md - ├── io/ - │ ├── io.md - │ ├── spdx-2-json-deserializer.md - │ ├── spdx-2-json-serializer.md - │ └── spdx-constants.md - ├── transform/ - │ ├── transform.md - │ └── spdx-relationships.md - ├── spdx-annotation.md - ├── spdx-checksum.md - ├── spdx-creation-information.md - ├── spdx-document.md - ├── spdx-element.md - ├── spdx-external-document-reference.md - ├── spdx-external-reference.md - ├── spdx-extracted-licensing-info.md - ├── spdx-file.md - ├── spdx-helpers.md - ├── spdx-license-element.md - ├── spdx-package-verification-code.md - ├── spdx-package.md - ├── spdx-relationship.md - └── spdx-snippet.md -``` - ```text src/DemaConsulting.SpdxModel/ ├── IO/ │ ├── Spdx2JsonDeserializer.cs — SPDX 2.x JSON deserialization │ ├── Spdx2JsonSerializer.cs — SPDX 2.x JSON serialization -│ └── SpdxConstants.cs — SPDX constants +│ └── SpdxConstants.cs — SPDX JSON field name constants ├── Transform/ │ └── SpdxRelationships.cs — Relationship manipulation utilities ├── SpdxAnnotation.cs — Annotation data model -├── SpdxAnnotationType.cs — Annotation type enum +├── SpdxAnnotationType.cs — Annotation type enumeration ├── SpdxChecksum.cs — Checksum data model -├── SpdxChecksumAlgorithm.cs — Checksum algorithm enum +├── SpdxChecksumAlgorithm.cs — Checksum algorithm enumeration ├── SpdxCreationInformation.cs — Creation information data model -├── SpdxDocument.cs — Document data model -├── SpdxElement.cs — Base element class +├── SpdxDocument.cs — Root document data model +├── SpdxElement.cs — Abstract base element class ├── SpdxExternalDocumentReference.cs — External document reference model ├── SpdxExternalReference.cs — External reference data model -├── SpdxExtractedLicensingInfo.cs — Extracted licensing info model +├── SpdxExtractedLicensingInfo.cs — Extracted licensing information model ├── SpdxFile.cs — File data model -├── SpdxFileType.cs — File type enum -├── SpdxHelpers.cs — Helper utilities -├── SpdxLicenseElement.cs — License element base class +├── SpdxFileType.cs — File type enumeration +├── SpdxHelpers.cs — Shared utility functions +├── SpdxLicenseElement.cs — Abstract license element base class ├── SpdxPackage.cs — Package data model ├── SpdxPackageVerificationCode.cs — Package verification code model -├── SpdxReferenceCategory.cs — Reference category enum +├── SpdxReferenceCategory.cs — Reference category enumeration ├── SpdxRelationship.cs — Relationship data model -├── SpdxRelationshipType.cs — Relationship type enum +├── SpdxRelationshipType.cs — Relationship type enumeration └── SpdxSnippet.cs — Snippet data model test/DemaConsulting.SpdxModel.Tests/ ├── IO/ │ ├── Examples/ — Test example JSON files -│ └── (Spdx2JsonDeserialize*.cs and Spdx2JsonSerialize*.cs test files) +│ ├── Spdx2JsonDeserialize*.cs — Deserializer unit tests +│ ├── Spdx2JsonSerialize*.cs — Serializer unit tests +│ ├── SpdxJsonHelpers.cs — IO test utility helpers +│ └── SpdxModelIOTests.cs — IO subsystem integration tests ├── Transforms/ -│ └── SpdxRelationshipsTests.cs — Relationship utility tests -├── SpdxAnnotationTests.cs -├── SpdxChecksumTests.cs -├── SpdxCreationInformationTests.cs -├── SpdxDocumentTests.cs -├── SpdxExternalDocumentReferenceTests.cs -├── SpdxExternalReferenceTests.cs -├── SpdxExtractedLicensingInfoTests.cs -├── SpdxFileTests.cs -├── SpdxPackageTests.cs -├── SpdxPackageVerificationCodeTests.cs -├── SpdxRelationshipTests.cs -└── SpdxSnippetTests.cs +│ ├── SpdxModelTransformTests.cs — Transform subsystem integration tests +│ └── SpdxRelationshipsTests.cs — Relationship utilities tests +├── SpdxModelTests.cs — System-level integration tests +├── SpdxAnnotationTests.cs — SpdxAnnotation unit tests +├── SpdxChecksumTests.cs — SpdxChecksum unit tests +├── SpdxCreationInformationTests.cs — SpdxCreationInformation unit tests +├── SpdxDocumentTests.cs — SpdxDocument unit tests +├── SpdxElementTests.cs — SpdxElement unit tests +├── SpdxExternalDocumentReferenceTests.cs — SpdxExternalDocumentReference unit tests +├── SpdxExternalReferenceTests.cs — SpdxExternalReference unit tests +├── SpdxExtractedLicensingInfoTests.cs — SpdxExtractedLicensingInfo unit tests +├── SpdxFileTests.cs — SpdxFile unit tests +├── SpdxHelpersTests.cs — SpdxHelpers unit tests +├── SpdxPackageTests.cs — SpdxPackage unit tests +├── SpdxPackageVerificationCodeTests.cs — SpdxPackageVerificationCode unit tests +├── SpdxRelationshipTests.cs — SpdxRelationship unit tests +├── SpdxSnippetTests.cs — SpdxSnippet unit tests +└── TestHelpers.cs — Test utility helpers (SpdxTestHelpers) ``` + +## Companion Artifact Structure + +Each local software item has corresponding artifacts in parallel directory trees: + +- Requirements: `docs/reqstream/spdx-model/spdx-model.yaml`, + `docs/reqstream/spdx-model[/{subsystem-name}...]/{item}.yaml` +- Design: `docs/design/spdx-model.md`, + `docs/design/spdx-model[/{subsystem-name}...]/{item}.md` +- Verification: `docs/verification/spdx-model.md`, + `docs/verification/spdx-model[/{subsystem-name}...]/{item}.md` +- Source: `src/DemaConsulting.SpdxModel[/{SubsystemName}...]/{Item}.cs` +- Tests: `test/DemaConsulting.SpdxModel.Tests[/{SubsystemName}...]/{Item}Tests.cs` + +Review-sets: defined in `.reviewmark.yaml` + +## References + +- [REF-1] SpdxModel releases, + +## Structural Deviation + +The companion artifact layout described in this document places subsystem design and verification +files at the subsystem level (e.g., `docs/design/spdx-model/io/` for the IO subsystem). +In practice, subsystem design files (`transform.md`, `io.md`) and verification files are located +inside their respective subsystem subfolders rather than at the parent `spdx-model/` level. +This layout was chosen for consistency with the IO subsystem file organization conventions +adopted early in the project and is accepted as a project-wide structural deviation. +Existing file references in review-sets and traceability tooling reflect the actual folder +layout and do not require updating. diff --git a/docs/design/spdx-model.md b/docs/design/spdx-model.md new file mode 100644 index 0000000..64bc7fc --- /dev/null +++ b/docs/design/spdx-model.md @@ -0,0 +1,153 @@ +# SpdxModel + +DemaConsulting.SpdxModel is a .NET library providing a complete implementation of the SPDX +(Software Package Data Exchange) data model. The library exposes an in-memory object model +representing all SPDX document elements, with serialization and transformation capabilities. + +## Architecture + +```mermaid +flowchart TD + subgraph IO + Spdx2JsonDeserializer + Spdx2JsonSerializer + SpdxConstants + end + subgraph Transform + SpdxRelationships + end + SpdxDocument + SpdxElement + SpdxLicenseElement + SpdxPackage + SpdxFile + SpdxSnippet + SpdxRelationship + SpdxAnnotation + SpdxChecksum + SpdxCreationInformation + SpdxExternalDocumentReference + SpdxExternalReference + SpdxExtractedLicensingInfo + SpdxPackageVerificationCode + SpdxHelpers + + SpdxDocument --> SpdxElement + SpdxLicenseElement --> SpdxElement + SpdxPackage --> SpdxLicenseElement + SpdxFile --> SpdxLicenseElement + SpdxSnippet --> SpdxLicenseElement + SpdxRelationship --> SpdxElement + SpdxAnnotation --> SpdxElement + + Spdx2JsonDeserializer --> SpdxDocument + Spdx2JsonDeserializer --> SpdxConstants + Spdx2JsonSerializer --> SpdxDocument + Spdx2JsonSerializer --> SpdxConstants + SpdxRelationships --> SpdxDocument + SpdxRelationships --> SpdxRelationship + SpdxDocument --> SpdxPackage + SpdxDocument --> SpdxFile + SpdxDocument --> SpdxSnippet + SpdxDocument --> SpdxRelationship + SpdxDocument --> SpdxAnnotation + SpdxDocument --> SpdxExternalDocumentReference + SpdxDocument --> SpdxCreationInformation + SpdxDocument --> SpdxExtractedLicensingInfo + SpdxFile --> SpdxChecksum + SpdxPackage --> SpdxChecksum + SpdxPackage --> SpdxExternalReference + SpdxPackage --> SpdxPackageVerificationCode + SpdxExternalDocumentReference --> SpdxChecksum + Spdx2JsonDeserializer --> SpdxHelpers + Spdx2JsonSerializer --> SpdxHelpers + SpdxDocument --> SpdxPackage + SpdxDocument --> SpdxFile + SpdxDocument --> SpdxSnippet + SpdxDocument --> SpdxRelationship + SpdxDocument --> SpdxAnnotation + SpdxDocument --> SpdxExternalDocumentReference + SpdxDocument --> SpdxCreationInformation + SpdxDocument --> SpdxExtractedLicensingInfo + SpdxFile --> SpdxChecksum + SpdxPackage --> SpdxChecksum + SpdxPackage --> SpdxExternalReference + SpdxPackage --> SpdxPackageVerificationCode + SpdxExternalDocumentReference --> SpdxChecksum + Spdx2JsonDeserializer --> SpdxHelpers + Spdx2JsonSerializer --> SpdxHelpers +``` + +## External Interfaces + +**SPDX JSON Input**: JSON file conforming to the SPDX 2.2 or 2.3 JSON schema. + +- *Type*: File (JSON) +- *Role*: Consumer +- *Contract*: `Spdx2JsonDeserializer.Deserialize(string)` accepts raw JSON text and returns a + populated `SpdxDocument`. +- *Constraints*: Input must be valid JSON; SPDX field validation is performed after + deserialization via `SpdxDocument.Validate()`. + +**SPDX JSON Output**: JSON file conforming to the SPDX 2.3 JSON schema. + +- *Type*: File (JSON) +- *Role*: Provider +- *Contract*: `Spdx2JsonSerializer.Serialize(SpdxDocument)` returns a complete SPDX 2.3 JSON + string. +- *Constraints*: Optional fields are omitted when empty or null; output always conforms to SPDX + 2.3 schema. + +**In-Process .NET Public API**: Object model and transformation API consumed by .NET callers. + +- *Type*: In-process .NET public API +- *Role*: Provider +- *Contract*: Exposes `SpdxDocument` and all data model classes, `Spdx2JsonDeserializer`, + `Spdx2JsonSerializer`, and `SpdxRelationships` as public types. +- *Constraints*: Targets `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`. + +**Error Handling**: + +- `Spdx2JsonDeserializer.Deserialize` throws `System.Text.Json.JsonException` when the input + is fatally malformed JSON that cannot be parsed. Missing or unknown SPDX fields do not throw; + they produce default or empty values in the resulting `SpdxDocument`. +- `SpdxDocument.Validate(List)` never throws. It appends human-readable issue strings + to the supplied list; an empty list after the call indicates a valid document. + +## Dependencies + +- **System.Text.Json**: used by the IO subsystem for JSON DOM parsing and serialization; + available in-box on modern .NET targets and via NuGet for .NET Standard 2.0. + +## Risk Control Measures + +N/A - not a safety-classified software item. + +## Data Flow + +```mermaid +flowchart LR + A[JSON File] --> B[Spdx2JsonDeserializer] + B --> C[SpdxDocument] + C --> D[Transform utilities] + D --> C + C --> E[Spdx2JsonSerializer] + E --> F[JSON File] +``` + +1. Caller provides a JSON string to `Spdx2JsonDeserializer.Deserialize`. +2. The deserializer uses `System.Text.Json.Nodes` to parse the JSON DOM. +3. Per-element helpers populate a new `SpdxDocument` instance. +4. The caller inspects or modifies the `SpdxDocument` in memory, optionally using + `SpdxRelationships` utilities. +5. `Spdx2JsonSerializer.Serialize` traverses the `SpdxDocument` and produces a JSON string. + +## Design Constraints + +- Targets `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0` simultaneously. +- Minimal runtime dependencies: relies on BCL/framework APIs where possible; compatibility NuGet + packages used on older targets. +- Nullable reference types enabled: all public API members declare nullability explicitly. +- Data model classes use public mutable properties to allow flexible construction; deep-copy + methods provide safe cloning. +- No static mutable state in data model classes; thread safety is the caller's responsibility. diff --git a/docs/design/spdx-model/io/io.md b/docs/design/spdx-model/io/io.md index 28dd3af..ca45b77 100644 --- a/docs/design/spdx-model/io/io.md +++ b/docs/design/spdx-model/io/io.md @@ -1,55 +1,62 @@ -# IO Subsystem Design - -## Purpose +### IO The IO subsystem provides JSON serialization and deserialization for SPDX 2.x documents, converting between the in-memory `SpdxDocument` object model and SPDX JSON files conforming to the SPDX 2.2 and 2.3 specifications. -## Units - -| Unit | File | Responsibility | -| ---- | ---- | -------------- | -| `Spdx2JsonDeserializer` | `IO/Spdx2JsonDeserializer.cs` | Reads SPDX 2.x JSON into the object model | -| `Spdx2JsonSerializer` | `IO/Spdx2JsonSerializer.cs` | Writes the object model to SPDX 2.x JSON | -| `SpdxConstants` | `IO/SpdxConstants.cs` | String constants for SPDX JSON field names | +#### Overview -## Design +The IO subsystem is responsible for all JSON I/O for SPDX 2.x documents. It contains three +units: `Spdx2JsonDeserializer`, which reads JSON text into a `SpdxDocument`; `Spdx2JsonSerializer`, +which writes a `SpdxDocument` back to JSON text; and `SpdxConstants`, which holds the JSON field +name strings used by both the deserializer and serializer. -### Spdx2JsonDeserializer +#### Interfaces -`Spdx2JsonDeserializer` reads a JSON stream or string and populates a `SpdxDocument`. It uses -`System.Text.Json.Nodes`, parsing input with `JsonNode.Parse` and traversing `JsonObject` and -`JsonArray` nodes to reconstruct each element. Both SPDX 2.2 and 2.3 JSON schemas are -supported; version differences are handled transparently during parsing. +**Spdx2JsonDeserializer.Deserialize**: Reads an SPDX 2.x JSON string into an `SpdxDocument`. -Key design decisions: +- *Type*: In-process .NET public API +- *Role*: Provider +- *Contract*: Accepts a raw JSON string; returns a fully populated `SpdxDocument`. +- *Constraints*: Input must be valid JSON; unsupported or unknown fields are silently ignored. -- DOM-based parsing via `JsonNode` (rather than streaming) to allow forward references between - document elements -- Graceful handling of optional SPDX fields (missing fields result in default values) +**Spdx2JsonSerializer.Serialize**: Writes an `SpdxDocument` to an SPDX 2.3 JSON string. -### Spdx2JsonSerializer +- *Type*: In-process .NET public API +- *Role*: Provider +- *Contract*: Accepts an `SpdxDocument`; returns a complete SPDX 2.3 JSON string. +- *Constraints*: Optional fields are omitted when null or empty. -`Spdx2JsonSerializer` takes an `SpdxDocument`, builds a JSON DOM using `JsonObject` and -`JsonArray`, and emits the final JSON with `ToJsonString(...)`. It iterates over each element -collection in document order, creating the appropriate JSON structure for each SPDX element -type. +**Error Handling**: -Key design decisions: +- `Spdx2JsonDeserializer.Deserialize` throws `System.Text.Json.JsonException` when the input is + fatally malformed JSON that cannot be parsed by `JsonNode.Parse`. Missing or unknown SPDX fields + do not throw; the corresponding properties in the returned `SpdxDocument` receive their default + values (empty strings for required fields, null for optional fields). +- `Spdx2JsonSerializer.Serialize` does not throw under normal operation. It silently omits null + or empty optional fields from the output JSON. -- Output follows SPDX 2.3 JSON schema by default -- Optional fields are omitted when empty or null to keep output clean +#### Design -### SpdxConstants +Deserialization uses `System.Text.Json.Nodes` DOM-based parsing so that all elements are +available before cross-references are resolved: -`SpdxConstants` is a static class holding string constants for every JSON property name used in -the SPDX 2.x JSON format. Using named constants prevents typos and centralizes the mapping -between the object model and the serialized form. +1. Caller invokes `Spdx2JsonDeserializer.Deserialize(jsonString)`. +2. `JsonNode.Parse` produces a `JsonObject` root. +3. `DeserializeDocument` traverses the root object, calling per-element helpers + (`DeserializePackage`, `DeserializeFile`, etc.) for each child array. +4. `SpdxConstants` supplies the JSON property name strings to avoid hard-coded literals. +5. A populated `SpdxDocument` is returned to the caller. -## Dependencies +Serialization follows the reverse path: -The IO subsystem depends on: +1. Caller invokes `Spdx2JsonSerializer.Serialize(document)`. +2. `SerializeDocument` builds a `JsonObject` root. +3. Per-element helpers (`SerializePackage`, `SerializeFile`, etc.) append child objects. +4. `SpdxConstants` supplies property names. +5. `ToJsonString(...)` produces the final JSON string. -- `System.Text.Json` (BCL / NuGet) -- All data model units in the root namespace (`SpdxDocument`, `SpdxPackage`, etc.) +Element-level field preservation (for example, verifying that all package fields survive +a round trip) is the responsibility of the unit-level IO tests, not the subsystem-level +integration tests. The subsystem-level tests verify that a complete document survives a +round trip and passes validation. diff --git a/docs/design/spdx-model/io/spdx-2-json-deserializer.md b/docs/design/spdx-model/io/spdx-2-json-deserializer.md index bf6e856..4b0a432 100644 --- a/docs/design/spdx-model/io/spdx-2-json-deserializer.md +++ b/docs/design/spdx-model/io/spdx-2-json-deserializer.md @@ -1,35 +1,76 @@ -# Spdx2JsonDeserializer Unit Design +### Spdx2JsonDeserializer -## Purpose +#### Purpose `Spdx2JsonDeserializer` reads SPDX 2.x JSON documents and populates the in-memory `SpdxDocument` object model. It supports both the SPDX 2.2 and SPDX 2.3 JSON schemas, handling version differences transparently during parsing. -## Design +#### Data Model -`Spdx2JsonDeserializer` is a public static class with no instance state. All public entry points -accept either a JSON string or a `JsonNode` and return strongly typed model objects. +N/A - `Spdx2JsonDeserializer` is a public static class with no instance state. -Key design decisions: +#### Key Methods -- DOM-based parsing via `System.Text.Json.Nodes` (`JsonNode`/`JsonArray`) to allow forward - references between document elements before the full document is assembled. -- Graceful handling of optional SPDX fields: missing properties result in default values rather - than exceptions. -- Per-element `Deserialize*` methods (`DeserializePackage`, `DeserializeFile`, etc.) are public - to support targeted unit testing and partial deserialization. +**Deserialize**: Entry point — parses a raw JSON string into an `SpdxDocument`. -Key methods: +- *Parameters*: `string json` — raw SPDX 2.x JSON text. +- *Returns*: `SpdxDocument` — fully populated in-memory document. +- *Preconditions*: `json` must be valid JSON text. +- *Postconditions*: The returned `SpdxDocument` reflects all elements present in the JSON input; + unrecognized fields are silently ignored. -| Method | Description | -| ------ | ----------- | -| `Deserialize(string)` | Entry point — parses a raw JSON string into an `SpdxDocument` | -| `DeserializeDocument(JsonNode)` | Converts a parsed `JsonNode` tree into an `SpdxDocument` | -| `Deserialize*(JsonNode?)` | Per-element helpers for each SPDX element type | +**DeserializeDocument**: Converts a parsed `JsonNode` tree into an `SpdxDocument`. -## Dependencies +- *Parameters*: `JsonNode node` — root JSON node from `JsonNode.Parse`. +- *Returns*: `SpdxDocument` — populated document. +- *Preconditions*: `node` must be a `JsonObject` representing the SPDX document root. +- *Postconditions*: All child element arrays are deserialized by the corresponding per-element + helpers. -- `System.Text.Json` (BCL) — JSON DOM parsing via `JsonNode` -- `SpdxDocument` and all data model units in the root namespace -- `SpdxConstants` — string constants for JSON property names +**Deserialize\* helpers**: Per-element deserialization methods (`DeserializePackage`, +`DeserializeFile`, `DeserializeSnippet`, `DeserializeRelationship`, `DeserializeAnnotation`, +`DeserializeChecksum`, `DeserializeExternalDocumentReference`, `DeserializeExternalReference`, +`DeserializeExtractedLicensingInfo`, `DeserializeCreationInformation`). + +- *Parameters*: `JsonNode? node` — the element's JSON node, which may be null. +- *Returns*: The corresponding model type (e.g., `SpdxPackage`), or a default instance when + `node` is null. +- *Preconditions*: none. +- *Postconditions*: Missing optional fields result in default values; no exception is thrown for + absent properties. + +**DeserializeVerificationCode**: Deserializes an optional `SpdxPackageVerificationCode`. + +- *Parameters*: `JsonNode? node` — the package verification code JSON node, which may be null. +- *Returns*: `SpdxPackageVerificationCode?` — a populated instance when `node` is non-null; + `null` when `node` is null (indicating the package verification code was absent in the JSON). +- *Preconditions*: none. +- *Postconditions*: Returns `null` when `node` is `null`; does not return a default instance. + +**Find (private helper)**: Locates a named descendant node within a JSON tree, descending +through arrays automatically. + +- *Algorithm*: Accepts a root `JsonNode?`, an index into the `names` path array, and the full + `names` array. When the current node is a `JsonArray`, each element is searched recursively + and the first non-null result is returned (enabling path resolution through SPDX `ranges` + arrays). When the current node is an object, the next name in the path is looked up and + recursion continues. Used by `DeserializeSnippet` to extract `startPointer` and `endPointer` + values from the SPDX 2.x `ranges` array structure without knowing the array index in advance. + +#### Error Handling + +Missing or null JSON properties produce default values rather than exceptions. Input must be +valid JSON; a `JsonException` from `System.Text.Json` will propagate to the caller if the input +is malformed. + +#### Dependencies + +- **System.Text.Json** — JSON DOM parsing via `JsonNode`, `JsonObject`, and `JsonArray`. +- **SpdxDocument** and all data model units in the root namespace. +- **SpdxConstants** — string constants for JSON property names. + +#### Callers + +- External consumers of the library who call `Spdx2JsonDeserializer.Deserialize` to load SPDX + documents. diff --git a/docs/design/spdx-model/io/spdx-2-json-serializer.md b/docs/design/spdx-model/io/spdx-2-json-serializer.md index e40ca4f..445f2fc 100644 --- a/docs/design/spdx-model/io/spdx-2-json-serializer.md +++ b/docs/design/spdx-model/io/spdx-2-json-serializer.md @@ -1,35 +1,76 @@ -# Spdx2JsonSerializer Unit Design +### Spdx2JsonSerializer -## Purpose +#### Purpose `Spdx2JsonSerializer` converts an in-memory `SpdxDocument` object model to an SPDX 2.3 JSON string. It is the counterpart to `Spdx2JsonDeserializer` and completes the round-trip serialization support for the IO subsystem. -## Design +#### Data Model -`Spdx2JsonSerializer` is a public static class with no instance state. All public methods -accept strongly typed model objects and return `JsonObject`/`JsonArray` nodes or a final JSON -string. +N/A - `Spdx2JsonSerializer` is a public static class with no instance state. -Key design decisions: +#### Key Methods -- Output conforms to SPDX 2.3 JSON schema. -- Optional fields are omitted entirely (not written as `null`) when empty or null to keep - output concise and compatible with strict schema validators. -- Per-element `Serialize*` methods (`SerializePackage`, `SerializeFile`, etc.) are public to - support targeted unit testing and partial serialization. +**Serialize**: Entry point — returns a complete SPDX 2.3 JSON string. -Key methods: +- *Parameters*: `SpdxDocument document` — the in-memory document to serialize. +- *Returns*: `string` — SPDX 2.3 JSON text. +- *Preconditions*: none. +- *Postconditions*: The returned string is valid JSON conforming to the SPDX 2.3 schema; optional + fields absent from the model are omitted from the output. -| Method | Description | -| ------ | ----------- | -| `Serialize(SpdxDocument)` | Entry point — returns a complete SPDX JSON string | -| `SerializeDocument(SpdxDocument)` | Converts an `SpdxDocument` to a `JsonObject` | -| `Serialize*(…)` | Per-element helpers for each SPDX element type | +**SerializeDocument**: Converts an `SpdxDocument` to a `JsonObject`. -## Dependencies +- *Parameters*: `SpdxDocument document` — document to serialize. +- *Returns*: `JsonObject` — root JSON object with all element arrays populated. +- *Preconditions*: none. +- *Postconditions*: All element arrays are serialized by the corresponding per-element helpers. -- `System.Text.Json` (BCL) — JSON node construction via `JsonObject`/`JsonArray` -- `SpdxDocument` and all data model units in the root namespace -- `SpdxConstants` — string constants for JSON property names +**Serialize\* helpers**: Per-element serialization methods (`SerializePackage`, `SerializeFile`, +`SerializeSnippet`, `SerializeRelationship`, `SerializeAnnotation`, `SerializeChecksum`, +`SerializeExternalDocumentReference`, `SerializeExternalReference`, +`SerializeExtractedLicensingInfo`, `SerializeCreationInformation`, +`SerializeVerificationCode`). + +- *Parameters*: The corresponding model object. +- *Returns*: A `JsonObject` or `JsonArray` representing the element. +- *Preconditions*: none. +- *Postconditions*: Optional fields that are null or empty are omitted from the output object. + Exception: the top-level `files`, `packages`, `snippets`, and `relationships` arrays are + required by the SPDX 2.x schema and are always emitted even when empty — they are not + optional at the document level. + +**Serialize\*s array helpers**: Per-element-array serialization methods (`SerializePackages`, +`SerializeFiles`, `SerializeSnippets`, `SerializeRelationships`, `SerializeAnnotations`, +`SerializeChecksums`, `SerializeExternalDocumentReferences`, `SerializeExternalReferences`, +`SerializeExtractedLicensingInfos`). + +- *Parameters*: A typed array of the corresponding model objects (e.g., `SpdxPackage[]`). +- *Returns*: A `JsonArray` containing one serialized `JsonObject` per element. +- *Pattern*: Each method creates an empty `JsonArray`, iterates the input array calling + the corresponding singular helper, and returns the populated array. + +#### Error Handling + +No exceptions are thrown for valid model objects. Null or empty optional fields are silently +omitted rather than written as null JSON values. + +Notable conditional serialization behaviors: + +- `SerializeAnnotation`: The `SPDXID` field is conditionally omitted when the annotation's + `Id` is null or empty (annotations on sub-elements often do not carry their own SPDX ID). +- `SerializeSnippet`: A line-range entry is only added to the `ranges` array when both + `SnippetLineStart` and `SnippetLineEnd` are non-zero; otherwise only the byte-range entry + is written. + +#### Dependencies + +- **System.Text.Json** — JSON node construction via `JsonObject` and `JsonArray`. +- **SpdxDocument** and all data model units in the root namespace. +- **SpdxConstants** — string constants for JSON property names. + +#### Callers + +- External consumers of the library who call `Spdx2JsonSerializer.Serialize` to produce SPDX + JSON output. diff --git a/docs/design/spdx-model/io/spdx-constants.md b/docs/design/spdx-model/io/spdx-constants.md index 1c48326..1515351 100644 --- a/docs/design/spdx-model/io/spdx-constants.md +++ b/docs/design/spdx-model/io/spdx-constants.md @@ -1,24 +1,33 @@ -# SpdxConstants Unit Design +### SpdxConstants -## Purpose +#### Purpose `SpdxConstants` is a static class that centralizes all JSON property-name strings used when serializing and deserializing SPDX 2.x JSON documents. It eliminates hard-coded string literals -scattered throughout the IO subsystem and provides a single place to update field names if the +throughout the IO subsystem and provides a single place to update field names if the SPDX specification changes. -## Design +#### Data Model -`SpdxConstants` is a non-instantiable `internal` static class containing only `internal const string` fields. -Each constant corresponds to one JSON property name in the SPDX 2.x JSON schema (e.g., -`FieldSpdxId`, `FieldName`, `FieldVersionInfo`). +N/A - `SpdxConstants` contains only `internal const string` fields and no instance state. +Representative constants include `FieldSpdxId` (`"SPDXID"`), `FieldName` (`"name"`), +`FieldVersionInfo` (`"versionInfo"`), `FieldPackages` (`"packages"`), +`FieldRelationships` (`"relationships"`), `FieldAnnotationType` (`"annotationType"`) and over +sixty other property-name constants covering all SPDX 2.x JSON fields. -Key design decisions: +#### Key Methods -- All constants are `const string` to allow use as switch-case labels and compile-time - embedding. -- No logic or state — purely a name registry. +N/A - `SpdxConstants` contains no methods; it is a pure name registry of `const string` values. -## Dependencies +#### Error Handling -- None (no external dependencies; consumed by `Spdx2JsonDeserializer` and `Spdx2JsonSerializer`) +N/A - no logic is executed; all values are compile-time constants. + +#### Dependencies + +N/A - no external dependencies; consumed by `Spdx2JsonDeserializer` and `Spdx2JsonSerializer`. + +#### Callers + +- **Spdx2JsonDeserializer** — uses constants as JSON property name keys when reading elements. +- **Spdx2JsonSerializer** — uses constants as JSON property name keys when writing elements. diff --git a/docs/design/spdx-model/spdx-annotation.md b/docs/design/spdx-model/spdx-annotation.md index 9587ad3..41ea388 100644 --- a/docs/design/spdx-model/spdx-annotation.md +++ b/docs/design/spdx-model/spdx-annotation.md @@ -1,32 +1,127 @@ -# SpdxAnnotation Unit Design +## SpdxAnnotation -## Purpose +### Purpose `SpdxAnnotation` represents an SPDX annotation — a comment or review note attached to any SPDX element by a person, organization, or tool. Annotations support compliance workflows where reviewers document findings about software components. -## Design +### Data Model -`SpdxAnnotation` is a sealed class that extends `SpdxElement` (inheriting the `Id` field). +**Annotator**: `string` — Person, organization, or tool that made the annotation, in the format +`Person: name`, `Organization: name`, or `Tool: name`. -Data members: +**Date**: `string` — ISO 8601 UTC timestamp of the annotation (e.g., `2023-01-01T00:00:00Z`). -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Annotator` | `string` | Person, organization, or tool that made the annotation | -| `Date` | `string` | ISO 8601 UTC timestamp of the annotation | -| `Type` | `SpdxAnnotationType` | Enumerated annotation type (Review, Other) | -| `Comment` | `string` | Free-text annotation content | +**Type**: `SpdxAnnotationType` — Enumerated annotation type; either `Review` or `Other`. -Key methods: +**Comment**: `string` — Free-text annotation content describing the finding or note. -- `DeepCopy()` — returns a new `SpdxAnnotation` with all fields copied -- `Enhance(SpdxAnnotation)` — fills in missing fields from another instance -- `Validate(string, List)` — appends validation issues to the supplied list -- `Same` — static `IEqualityComparer` comparing annotator, date, type, and comment +### Key Methods -## Dependencies +**DeepCopy**: Returns a new `SpdxAnnotation` with all fields copied. -- `SpdxElement` (base class) -- `SpdxAnnotationType` (enum) +- *Parameters*: none. +- *Returns*: `SpdxAnnotation` — independent copy of this instance. +- *Preconditions*: none. +- *Postconditions*: The returned instance has the same field values and shares no mutable + references with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxAnnotation other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Any empty or default-valued fields in this instance are replaced with the + corresponding non-empty values from `other`. + +**Enhance (static array overload)**: Merges two annotation arrays, returning an updated array. + +- *Parameters*: `SpdxAnnotation[] array` — base array to merge into; `SpdxAnnotation[] others` — + additional annotations to incorporate. +- *Returns*: `SpdxAnnotation[]` — updated array containing all annotations from both inputs. +- *Algorithm*: Iterates `others`; for each item, searches `array` using `Same`. If a match is + found, the existing item is enhanced with the other's field values. If no match is found, a + deep copy of the item is appended. +- *Preconditions*: none. +- *Postconditions*: The returned array contains at least all elements of `array` and at least + one representative of each element in `others`. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `string parent` — identifier of the parent element (e.g. package or file SPDX-ID) + used as a prefix in issue messages; `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Any missing required fields are recorded as strings in `issues`. + +**Same**: `static IEqualityComparer` — compares annotations by annotator, date, +type, and comment. Used for deduplication when merging annotation arrays. + +### Error Handling + +Validation errors (missing required fields) are collected into a `List` passed to +`Validate` rather than thrown as exceptions. Callers decide whether to surface or suppress the +issues. No exceptions are thrown by `DeepCopy` or `Enhance`. + +### Dependencies + +- **SpdxElement** — base class providing the `Id` property. +- **SpdxAnnotationType** — enumeration for annotation type values. +- **SpdxHelpers** — `IsValidSpdxDateTime` used in `Validate`; `EnhanceString` used in `Enhance`. + +### SpdxAnnotationType + +`SpdxAnnotationType` is an enumeration of the SPDX annotation type tokens, with round-trip text +conversion provided by `SpdxAnnotationTypeExtensions`. + +#### SpdxAnnotationType Purpose + +Enumerate the valid SPDX annotation type strings and provide lossless conversion between the enum +representation used in the in-memory model and the text representation used in SPDX JSON documents. + +#### SpdxAnnotationType Data Model + +| Enum Value | Integer | SPDX Text Form | +|------------|---------|-------------------------------------------------| +| `Missing` | -1 | `""` (sentinel; indicates no type has been set) | +| `Review` | 0 | `REVIEW` | +| `Other` | 1 | `OTHER` | + +#### SpdxAnnotationType Key Methods + +**FromText**: Converts an SPDX annotation type text string to its enum value. + +- *Signature*: `static SpdxAnnotationType FromText(string annotationType)` +- *Parameters*: `string annotationType` — the raw text from the SPDX document (case-insensitive). +- *Returns*: `SpdxAnnotationType.Missing` when `annotationType` is an empty string; otherwise the + matching enum value. +- *Exceptions*: `InvalidOperationException` — thrown when `annotationType` is a non-empty string + that does not match any known annotation type name. + +**ToText**: Converts an enum value to its SPDX text form. + +- *Signature*: `static string ToText(this SpdxAnnotationType annotationType)` +- *Parameters*: `SpdxAnnotationType annotationType` — the enum value to serialize. +- *Returns*: The canonical SPDX text (e.g., `"REVIEW"`, `"OTHER"`). +- *Exceptions*: `InvalidOperationException` — thrown when the value is `Missing` or is a numeric + value that does not correspond to any named enum member. + +#### SpdxAnnotationType Error Handling + +- **`FromText`**: throws `InvalidOperationException` with a message identifying the unsupported + value when given a non-empty string that is not a recognized annotation type token. +- **`ToText`**: throws `InvalidOperationException` when the enum value is `Missing` or does not + correspond to a named enum member. + +#### Dependencies / Callers + +- **Spdx2JsonDeserializer** — calls `FromText` when deserializing annotation type fields from JSON. +- **Spdx2JsonSerializer** — calls `ToText` when serializing annotation type fields to JSON. + +### Callers + +- **SpdxDocument** — holds the document-level `Annotations` array. +- **SpdxLicenseElement** — holds element-level `Annotations` arrays. +- **Spdx2JsonDeserializer** — constructs `SpdxAnnotation` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxAnnotation` instances to JSON. diff --git a/docs/design/spdx-model/spdx-checksum.md b/docs/design/spdx-model/spdx-checksum.md index 46b78ae..ca11369 100644 --- a/docs/design/spdx-model/spdx-checksum.md +++ b/docs/design/spdx-model/spdx-checksum.md @@ -1,29 +1,129 @@ -# SpdxChecksum Unit Design +## SpdxChecksum -## Purpose +### Purpose `SpdxChecksum` represents an SPDX checksum — an algorithm-value pair used to verify the integrity of files and packages. Supporting multiple algorithms provides flexibility across different security policies and tooling ecosystems. -## Design +### Data Model -`SpdxChecksum` is a sealed class with no base class (not an `SpdxElement`). +**Algorithm**: `SpdxChecksumAlgorithm` — Identifies the hash algorithm (e.g., `SHA1`, `SHA256`, +`SHA512`, `MD5`). Defaults to `Missing` when not populated. -Data members: +**Value**: `string` — Lower-case hexadecimal digest value produced by the algorithm. -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Algorithm` | `SpdxChecksumAlgorithm` | Identifies the hash algorithm (SHA1, SHA256, MD5, etc.) | -| `Value` | `string` | Lower-case hexadecimal digest value | +### SpdxChecksumAlgorithm Enumeration -Key methods: +`SpdxChecksumAlgorithm` is an enumeration with a sentinel value and 17 named algorithm values: -- `DeepCopy()` — returns a new `SpdxChecksum` with all fields copied -- `Enhance(SpdxChecksum)` — fills in missing fields from another instance -- `Validate(string, List)` — appends validation issues to the supplied list -- `Same` — static `IEqualityComparer` comparing algorithm and value +| Enum Value | SPDX Text Form | +|----------------|-------------------------| +| `Missing` | `""` (empty string) | +| `Sha1` | `SHA1` | +| `Sha224` | `SHA224` | +| `Sha256` | `SHA256` | +| `Sha384` | `SHA384` | +| `Sha512` | `SHA512` | +| `Md2` | `MD2` | +| `Md4` | `MD4` | +| `Md5` | `MD5` | +| `Md6` | `MD6` | +| `Sha3256` | `SHA3-256` | +| `Sha3384` | `SHA3-384` | +| `Sha3512` | `SHA3-512` | +| `Blake2B256` | `BLAKE2b-256` | +| `Blake2B384` | `BLAKE2b-384` | +| `Blake2B512` | `BLAKE2b-512` | +| `Blake3` | `BLAKE3` | +| `Adler32` | `ADLER32` | -## Dependencies +`SpdxChecksumAlgorithmExtensions` provides two static helper methods: -- `SpdxChecksumAlgorithm` (enum) +**FromText**: Converts an SPDX algorithm text string to its enum value. + +- *Signature*: `static SpdxChecksumAlgorithm FromText(string checksumAlgorithm)` +- *Parameters*: `string checksumAlgorithm` — the SPDX text form of the algorithm (case-insensitive). +- *Returns*: `SpdxChecksumAlgorithm` — the corresponding enum value; returns `Missing` for an + empty string. +- *Exceptions*: `InvalidOperationException` — thrown when the input is a non-empty string that + does not match any known algorithm name (comparison is case-insensitive). +- *Case-insensitivity*: The input is converted to upper-case before comparison, so `"sha1"`, + `"SHA1"`, and `"Sha1"` are all accepted. + +**ToText**: Converts an enum value to its SPDX text form. + +- *Signature*: `static string ToText(this SpdxChecksumAlgorithm checksumAlgorithm)` +- *Parameters*: `SpdxChecksumAlgorithm checksumAlgorithm` — the algorithm enum value. +- *Returns*: `string` — the corresponding SPDX text representation. +- *Exceptions*: `InvalidOperationException` — thrown when the value is `Missing` or is a numeric + value that does not correspond to any named enum member. + +### Key Methods + +**DeepCopy**: Returns a new `SpdxChecksum` with all fields copied. + +- *Parameters*: none. +- *Returns*: `SpdxChecksum` — independent copy of this instance. +- *Preconditions*: none. +- *Postconditions*: The returned instance has the same field values and shares no mutable + references with the original. + +**Enhance (instance)**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxChecksum other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Any empty or default-valued fields in this instance are replaced with + non-empty values from `other`. + +**Enhance (static array merge)**: Merges two checksum arrays by matching on algorithm and value. + +- *Signature*: `static SpdxChecksum[] Enhance(SpdxChecksum[] array, SpdxChecksum[] others)` +- *Parameters*: `SpdxChecksum[] array` — base array; `SpdxChecksum[] others` — additions. +- *Returns*: `SpdxChecksum[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Entries in `others` that match an existing entry (by `Same.Equals`) are used + to enhance the existing entry. Entries in `others` that do not match any existing entry are + deep-copied and appended. The order of existing entries is preserved. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `string parent` — identifier for error messages; `List issues` — list + to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Missing or malformed fields are recorded in `issues`. Specifically: + - If `Algorithm` is `Missing`, the message `"{parent} Invalid Checksum Algorithm Field - Missing"` is appended. + - If `Algorithm` is a numeric value not defined in the enumeration, the message + `"{parent} Invalid Checksum Algorithm Field - Unknown"` is appended. + - If `Value` is empty, the message `"{parent} Invalid Checksum Value Field - Empty"` is appended. + +**Same**: `static IEqualityComparer` — compares checksums by algorithm and value. +Used for deduplication when merging checksum arrays. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. No exceptions are +thrown by `DeepCopy` or `Enhance`. + +- **`FromText`**: throws `InvalidOperationException` when the input string is non-empty and + unrecognized. +- **`ToText`**: throws `InvalidOperationException` when the algorithm value is `Missing` or is + an out-of-range numeric value not corresponding to any named enum member. +- **`Validate` unknown-algorithm branch**: when `Algorithm` holds a numeric value that is not + a named member of `SpdxChecksumAlgorithm`, `Validate` appends + `"{parent} Invalid Checksum Algorithm Field - Unknown"` to the issues list. + +### Dependencies + +- **SpdxChecksumAlgorithm** — enumeration of supported hash algorithms. +- **SpdxHelpers** — `EnhanceString` utility used in the instance `Enhance` method. + +### Callers + +- **SpdxPackage** — holds the package-level `Checksums` array. +- **SpdxFile** — holds the file-level `Checksums` array. +- **SpdxExternalDocumentReference** — holds a single `Checksum` for document integrity. +- **Spdx2JsonDeserializer** — constructs `SpdxChecksum` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxChecksum` instances to JSON. diff --git a/docs/design/spdx-model/spdx-creation-information.md b/docs/design/spdx-model/spdx-creation-information.md index 78f16ce..5212906 100644 --- a/docs/design/spdx-model/spdx-creation-information.md +++ b/docs/design/spdx-model/spdx-creation-information.md @@ -1,31 +1,71 @@ -# SpdxCreationInformation Unit Design +## SpdxCreationInformation -## Purpose +### Purpose `SpdxCreationInformation` captures the metadata about who created an SPDX document and when. -One instance is required per SPDX document. It enables provenance tracing and forward/backward +One instance is required per `SpdxDocument`. It enables provenance tracing and forward/backward compatibility for processing tools. -## Design +### Data Model -`SpdxCreationInformation` is a sealed class with no base class. +**Creators**: `string[]` — Identifies the persons, organizations, or tools that created the +document, each entry in the format `Person: name`, `Organization: name`, or `Tool: name`. -Data members: +**Created**: `string` — ISO 8601 UTC timestamp of document creation; may be empty for +partially-constructed documents (empty is permitted and skips format validation). -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Creators` | `string[]` | Identifies the persons, organizations, or tools that created the document | -| `Created` | `string` | ISO 8601 UTC timestamp of document creation; may be empty for partially-constructed documents | -| `Comment` | `string?` | Optional creator comment | -| `LicenseListVersion` | `string?` | Optional SPDX license list version used | +**Comment**: `string?` — Optional free-text comment from the creators. -Key methods: +**LicenseListVersion**: `string?` — Optional version string of the SPDX license list used +when constructing the document. -- `DeepCopy()` — returns a new `SpdxCreationInformation` with all fields copied -- `Enhance(SpdxCreationInformation)` — fills in missing fields from another instance -- `Validate(List)` — appends validation issues; validates `Created` format via regex when non-empty - (empty `Created` is permitted and skips format validation) +### Key Methods -## Dependencies +**DeepCopy**: Returns a new `SpdxCreationInformation` with all fields copied. -- `System.Text.RegularExpressions` — used internally to validate the `LicenseListVersion` field format +- *Parameters*: none. +- *Returns*: `SpdxCreationInformation` — independent copy of this instance. +- *Preconditions*: none. +- *Postconditions*: The returned instance has equal field values and shares no mutable references + with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxCreationInformation other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: `Creators` is updated to the union of both arrays, deduplicated (preserving + order, removing duplicates). Scalar fields (`Created`, `Comment`, `LicenseListVersion`) are + filled from `other` only when currently empty or null in this instance. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: All four validation rules are checked and violations recorded in `issues`: + (1) `Creators` must be non-empty; (2) each creator entry must start with `Person:`, + `Organization:`, or `Tool:`; (3) `Created` must be a valid SPDX date-time string when + non-empty (an empty `Created` is permitted); (4) `LicenseListVersion`, when present, must + match the pattern `\d+\.\d+`. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. Four rules are +checked: (1) `Creators` non-empty, (2) each creator prefixed with `Person:`, `Organization:`, +or `Tool:`, (3) `Created` valid SPDX date-time (when non-empty), (4) `LicenseListVersion` +matching `\d+\.\d+` (when present). `LicenseListVersion` format is validated via +`LicenseListVersionRegex`. No exceptions are thrown by `DeepCopy` or `Enhance`. + +### Dependencies + +- **System.Text.RegularExpressions** — used internally for `LicenseListVersion` format + validation via `LicenseListVersionRegex` (pattern `^\d+\.\d+$`). +- **SpdxHelpers** — `EnhanceString` used in `Enhance` to fill empty string fields; `IsValidSpdxDateTime` + used in `Validate` to check the `Created` field format. + +### Callers + +- **SpdxDocument** — holds exactly one `SpdxCreationInformation` instance as `CreationInformation`. +- **Spdx2JsonDeserializer** — constructs `SpdxCreationInformation` during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxCreationInformation` to JSON. diff --git a/docs/design/spdx-model/spdx-document.md b/docs/design/spdx-model/spdx-document.md index b120766..c36c2f6 100644 --- a/docs/design/spdx-model/spdx-document.md +++ b/docs/design/spdx-model/spdx-document.md @@ -1,46 +1,120 @@ -# SpdxDocument Unit Design +## SpdxDocument -## Purpose +### Purpose `SpdxDocument` is the root container of the SPDX object model. It aggregates all SPDX elements (packages, files, snippets, relationships, annotations, and extracted licensing information) -and exposes document-level operations such as validation, deep copy, and root-package retrieval. - -## Design - -`SpdxDocument` is a sealed class that extends `SpdxElement` (inheriting the `Id` field). - -Data members (key fields): - -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Name` | `string` | Document name | -| `Version` | `string` | SPDX specification version (e.g., `SPDX-2.3`) | -| `DataLicense` | `string` | License for the SPDX metadata itself | -| `DocumentNamespace` | `string` | Unique URI namespace for this document | -| `CreationInformation` | `SpdxCreationInformation` | Creation metadata | -| `ExternalDocumentReferences` | `SpdxExternalDocumentReference[]` | References to external SPDX documents | -| `ExtractedLicensingInfo` | `SpdxExtractedLicensingInfo[]` | Non-standard license texts | -| `Packages` | `SpdxPackage[]` | All packages in the document | -| `Files` | `SpdxFile[]` | All files in the document | -| `Snippets` | `SpdxSnippet[]` | All snippets in the document | -| `Relationships` | `SpdxRelationship[]` | All relationships in the document | -| `Annotations` | `SpdxAnnotation[]` | All annotations in the document | -| `Describes` | `string[]` | IDs of elements described by this document | - -Key methods: - -- `DeepCopy()` — returns a fully independent deep copy of the entire document graph -- `Validate(List, bool ntia)` — validates all contained elements; optional NTIA SBOM minimum elements check -- `GetRootPackages()` — returns packages directly described by the document via `DESCRIBES` relationships -- `GetAllElements()` — enumerates all contained `SpdxElement` instances -- `GetElement(string id)` / `GetElement(string id)` — retrieves an element by SPDX ID -- `Same` — static `IEqualityComparer` comparing by document name - -## Dependencies - -- `SpdxElement` (base class) -- All other data model units: `SpdxPackage`, `SpdxFile`, `SpdxSnippet`, `SpdxRelationship`, - `SpdxAnnotation`, `SpdxCreationInformation`, `SpdxExternalDocumentReference`, - `SpdxExtractedLicensingInfo` -- `System.Text.RegularExpressions` — version field format validation +and exposes document-level operations such as validation, deep copy, and element retrieval. + +### Data Model + +**Name**: `string` — Human-readable document name. + +**Version**: `string` — SPDX specification version string (e.g., `SPDX-2.3`). + +**DataLicense**: `string` — License for the SPDX metadata itself (always `CC0-1.0` per the +SPDX specification). + +**DocumentNamespace**: `string` — Unique URI namespace for this document; used to qualify +element IDs when referencing across documents. + +**Comment**: `string?` — Optional free-text comment about the document. + +**CreationInformation**: `SpdxCreationInformation` — Metadata about document authorship and +creation time. + +**ExternalDocumentReferences**: `SpdxExternalDocumentReference[]` — References to external SPDX +documents that this document's elements may relate to. + +**ExtractedLicensingInfo**: `SpdxExtractedLicensingInfo[]` — Non-standard license texts +extracted from software in this document. + +**Packages**: `SpdxPackage[]` — All software packages described in the document. + +**Files**: `SpdxFile[]` — All files described in the document. + +**Snippets**: `SpdxSnippet[]` — All snippets described in the document. + +**Relationships**: `SpdxRelationship[]` — All directed relationships between elements in the +document. + +**Annotations**: `SpdxAnnotation[]` — Document-level annotations. + +**Describes**: `string[]` — SPDX IDs of elements directly described by this document (used +when `DESCRIBES` relationships are not present). + +### Key Methods + +**DeepCopy**: Returns a fully independent deep copy of the entire document graph. + +- *Parameters*: none. +- *Returns*: `SpdxDocument` — new instance with all arrays and nested objects deep-copied. +- *Preconditions*: none. +- *Postconditions*: The returned instance is structurally identical and shares no mutable + references with the original. + +**Validate**: Validates all contained elements and the document itself. + +- *Parameters*: `List issues` — list to append issues to; `bool ntia` — when `true`, + also checks NTIA SBOM minimum elements requirements. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: All validation issues found in the document and its elements are appended + to `issues`. + +**GetRootPackages**: Returns packages directly described by the document. + +- *Parameters*: none. +- *Returns*: `SpdxPackage[]` — packages whose SPDX ID appears in the `Describes` array, is the + target of a `DESCRIBES` relationship from the document element, or is the source of a + `DESCRIBED_BY` relationship pointing at the document element. All three mechanisms are checked + and the results are unioned. +- *Preconditions*: none. +- *Postconditions*: none. + +**GetAllElements**: Enumerates all `SpdxElement` instances contained in the document. + +- *Parameters*: none. +- *Returns*: `IEnumerable` — all packages, files, snippets, and annotations + (including the document itself and per-element annotations). `SpdxRelationship` elements are + excluded because relationships are not independently addressable elements and their inclusion + would cause them to appear as duplicates in element-ID lookups. +- *Preconditions*: none. +- *Postconditions*: none. + +**GetElement / GetElement\**: Retrieves an element by SPDX ID. + +- *Parameters*: `string id` — the `SPDXRef-…` identifier. +- *Returns*: `SpdxElement?` or `T?` — the matching element, or `null` if not found. +- *Preconditions*: none. +- *Postconditions*: none. + +**Same**: `static IEqualityComparer` — compares documents by `Name` and root-package +identity. Two documents are considered the same when their `Name` values match AND their sets of +root packages (as returned by `GetRootPackages`) contain the same packages in any order under +`SpdxPackage.Same`. Used for deduplication scenarios. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. The `Version` +field format is checked by a regular expression. No exceptions are thrown by `DeepCopy`, +`GetRootPackages`, `GetAllElements`, or `GetElement`. + +### Dependencies + +- **SpdxElement** — base class. +- **SpdxPackage**, **SpdxFile**, **SpdxSnippet** — package, file, and snippet elements. +- **SpdxRelationship** — relationship elements. +- **SpdxAnnotation** — annotation elements. +- **SpdxCreationInformation** — creation metadata. +- **SpdxExternalDocumentReference** — external document references. +- **SpdxExtractedLicensingInfo** — extracted licensing information. +- **System.Text.RegularExpressions** — `Version` field format validation. + +### Callers + +- **Spdx2JsonDeserializer** — produces `SpdxDocument` instances from JSON input. +- **Spdx2JsonSerializer** — consumes `SpdxDocument` instances to produce JSON output. +- **SpdxRelationships** — adds relationships to `SpdxDocument` instances. +- External consumers of this library — use `SpdxDocument` as the root of the in-memory SPDX + model. diff --git a/docs/design/spdx-model/spdx-element.md b/docs/design/spdx-model/spdx-element.md index e552e48..911d076 100644 --- a/docs/design/spdx-model/spdx-element.md +++ b/docs/design/spdx-model/spdx-element.md @@ -1,30 +1,46 @@ -# SpdxElement Unit Design +## SpdxElement -## Purpose +### Purpose `SpdxElement` is the abstract base class for all identifiable SPDX elements. It defines the common `Id` property (`SPDXRef-…`) and the shared `EnhanceElement` helper, ensuring consistent identity handling across all element types. -## Design +### Data Model -`SpdxElement` is a public abstract class. It is directly inherited by `SpdxDocument`, `SpdxRelationship`, -and `SpdxAnnotation`. `SpdxLicenseElement` is an abstract class that also inherits from `SpdxElement`, and -is in turn inherited by `SpdxPackage`, `SpdxFile`, and `SpdxSnippet`. +**Id**: `string` — SPDX element identifier in `SPDXRef-` format. Must be unique within +a document. -Data members: +**NoAssertion**: `const string` — The sentinel value `"NOASSERTION"` used by optional fields to +indicate that the value was intentionally omitted or is not known. -| Member | Type | Description | -| ------ | ---- | ----------- | -| `Id` | `string` | SPDX element identifier in `SPDXRef-` format | -| `NoAssertion` | `const string` | The sentinel value `"NOASSERTION"` used by optional fields | -| `SpdxRefRegex` | `protected static Regex` | Validates `SPDXRef-…` format | +**SpdxRefRegex**: `protected static readonly Regex` — Pre-compiled regular expression that validates +the `SPDXRef-…` format; used by subclass `Validate` methods. Matches the full pattern +`^SPDXRef-[a-zA-Z0-9.-]+$`. The 100 ms timeout is a ReDoS protection measure against +pathological input strings from untrusted SPDX sources. -Key methods: +### Key Methods -- `EnhanceElement(SpdxElement)` — protected helper that populates `Id` from another element if currently empty +**EnhanceElement**: Protected helper that populates `Id` from another element if currently empty. -## Dependencies +- *Parameters*: `SpdxElement other` — source element. +- *Returns*: `void` +- *Preconditions*: `other` must not be null. +- *Postconditions*: If `Id` is empty or null, it is set to `other.Id` via `SpdxHelpers.EnhanceString`. -- `System.Text.RegularExpressions` — `SpdxRefRegex` for ID validation -- `SpdxHelpers` — `EnhanceString` utility used in `EnhanceElement` +### Error Handling + +N/A - `SpdxElement` is abstract and contains no validation logic of its own. Subclasses +implement `Validate` and append issues to a `List`. + +### Dependencies + +- **System.Text.RegularExpressions** — `SpdxRefRegex` for ID format validation. +- **SpdxHelpers** — `EnhanceString` utility used in `EnhanceElement`. + +### Callers + +- **SpdxDocument** — extends `SpdxElement`. +- **SpdxRelationship** — extends `SpdxElement`. +- **SpdxAnnotation** — extends `SpdxElement`. +- **SpdxLicenseElement** — extends `SpdxElement` (abstract intermediate class). diff --git a/docs/design/spdx-model/spdx-external-document-reference.md b/docs/design/spdx-model/spdx-external-document-reference.md index 7d78f18..cc4aaec 100644 --- a/docs/design/spdx-model/spdx-external-document-reference.md +++ b/docs/design/spdx-model/spdx-external-document-reference.md @@ -1,31 +1,68 @@ -# SpdxExternalDocumentReference Unit Design +## SpdxExternalDocumentReference -## Purpose +### Purpose `SpdxExternalDocumentReference` represents a reference from one SPDX document to another, enabling modular SBOM construction and cross-document element referencing. Each reference -includes a checksum to verify the referenced document's integrity. +includes a checksum to verify the integrity of the referenced document. -## Design +### Data Model -`SpdxExternalDocumentReference` is a sealed class with no base class (not an `SpdxElement`). +**ExternalDocumentId**: `string` — Local identifier for the referenced document within this +document (e.g., `DocumentRef-tools`). Used as a prefix when referencing elements across documents. -Data members: +**Checksum**: `SpdxChecksum` — Cryptographic checksum of the referenced document for integrity +verification. -| Property | Type | Description | -| -------- | ---- | ----------- | -| `ExternalDocumentId` | `string` | Local identifier for the referenced document (e.g., `DocumentRef-tools`) | -| `Document` | `string` | URI of the referenced SPDX document | -| `Checksum` | `SpdxChecksum` | Checksum of the referenced document for integrity verification | +**Document**: `string` — URI of the referenced SPDX document. -Key methods: +### Key Methods -- `DeepCopy()` — returns a new instance with all fields deep-copied -- `Enhance(SpdxExternalDocumentReference)` — fills in missing fields from another instance -- `Enhance(array, array)` — static method merging two arrays by matching on `ExternalDocumentId` -- `Validate(List)` — appends validation issues to the supplied list -- `Same` — static `IEqualityComparer` comparing by `Document` +**DeepCopy**: Returns a new instance with all fields deep-copied. -## Dependencies +- *Parameters*: none. +- *Returns*: `SpdxExternalDocumentReference` — independent copy. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. -- `SpdxChecksum` — integrity checksum for the referenced document +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxExternalDocumentReference other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty fields in this instance are populated from `other`. + +**Enhance (static array merge)**: Merges two external document reference arrays by matching on +`Document` URI. + +- *Parameters*: `SpdxExternalDocumentReference[] array`, `SpdxExternalDocumentReference[] others`. +- *Returns*: `SpdxExternalDocumentReference[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Entries present in both arrays (matched by `Document` URI via `Same.Equals`) + are enhanced; new entries from `additions` are deep-copied and appended. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Missing or malformed fields are recorded in `issues`. + +**Same**: `static IEqualityComparer` — compares by `Document` URI. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. The nested +`Checksum` is also validated. No exceptions are thrown by `DeepCopy`, `Enhance`, or the +static merge method. + +### Dependencies + +- **SpdxChecksum** — integrity checksum for the referenced document. +- **SpdxHelpers** — `EnhanceString` used in `Enhance`. + +### Callers + +- **SpdxDocument** — holds the `ExternalDocumentReferences` array. +- **Spdx2JsonDeserializer** — constructs `SpdxExternalDocumentReference` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxExternalDocumentReference` instances to JSON. diff --git a/docs/design/spdx-model/spdx-external-reference.md b/docs/design/spdx-model/spdx-external-reference.md index 8335b64..8cba75f 100644 --- a/docs/design/spdx-model/spdx-external-reference.md +++ b/docs/design/spdx-model/spdx-external-reference.md @@ -1,32 +1,111 @@ -# SpdxExternalReference Unit Design +## SpdxExternalReference -## Purpose +### Purpose `SpdxExternalReference` represents a link from an SPDX package to an external resource, such as a package registry URL, vulnerability database entry, or documentation site. External references enrich SBOMs with contextual information from authoritative sources. -## Design +### Data Model -`SpdxExternalReference` is a sealed class with no base class. +**Category**: `SpdxReferenceCategory` — Broad category of the reference (e.g., `SECURITY`, +`PACKAGE-MANAGER`, `OTHER`). -Data members: +**Type**: `string` — Specific reference type within the category (e.g., `cpe23Type`, `purl`, +`advisory`). -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Category` | `SpdxReferenceCategory` | Broad category (e.g., SECURITY, PACKAGE-MANAGER) | -| `Type` | `string` | Specific reference type within the category (e.g., `cpe23Type`, `purl`) | -| `Locator` | `string` | URI or identifier for the external resource | -| `Comment` | `string?` | Optional explanatory comment | +**Locator**: `string` — URI or other identifier for the external resource. -Key methods: +**Comment**: `string?` — Optional explanatory comment. -- `DeepCopy()` — returns a new instance with all fields copied -- `Enhance(SpdxExternalReference)` — fills in missing fields from another instance -- `Enhance(array, array)` — static method merging two arrays by matching on category, type, and locator -- `Validate(string, List)` — validates the reference; `string` parameter is the owning package name -- `Same` — static `IEqualityComparer` comparing by category, type, and locator +### Key Methods -## Dependencies +**DeepCopy**: Returns a new instance with all fields copied. -- `SpdxReferenceCategory` (enum) +- *Parameters*: none. +- *Returns*: `SpdxExternalReference` — independent copy. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable state with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxExternalReference other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty fields in this instance are populated from `other`. + +**Enhance (static array merge)**: Merges two external reference arrays by matching on category, +type, and locator. + +- *Parameters*: `SpdxExternalReference[] array`, `SpdxExternalReference[] others`. +- *Returns*: `SpdxExternalReference[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Matching entries are enhanced; new entries are appended. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `string package` — owning package name for error messages; + `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Missing or invalid fields are recorded in `issues`. + +**Same**: `static IEqualityComparer` — compares by category, type, and +locator. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. No exceptions are +thrown by `DeepCopy`, `Enhance`, or the static merge method. + +### Dependencies + +- **SpdxReferenceCategory** — enumeration of supported reference categories. +- **SpdxHelpers** — `EnhanceString` used in `Enhance`. + +### SpdxReferenceCategory + +`SpdxReferenceCategory` is an enumeration of the broad reference categories defined by the SPDX +specification. + +#### Enum Values + +| Value | Integer | Description | +|------------------|---------|-------------------------------------------------| +| `Missing` | -1 | Sentinel indicating no category has been set. | +| `Security` | 0 | References to security-related information. | +| `PackageManager` | 1 | References to package management systems. | +| `PersistentId` | 2 | References to software-heritage persistent IDs. | +| `Other` | 3 | References that do not fit other categories. | + +#### FromText + +Converts a category string from an SPDX document to the corresponding `SpdxReferenceCategory` enum +value. + +- *Parameters*: `string category` — the raw text from the SPDX document (case-insensitive). +- *Returns*: `SpdxReferenceCategory.Missing` when `category` is an empty string; otherwise the + matching enum value. +- *Preconditions*: none. +- *Postconditions*: none. +- *Exceptions*: `InvalidOperationException` — thrown when `category` is not a recognized SPDX + reference category string. +- *Note*: `PACKAGE_MANAGER` (with underscore) is accepted as a backward-compatibility alias for + `PACKAGE-MANAGER` (with hyphen). No equivalent underscore alias exists for any other category. + +#### ToText + +Converts a `SpdxReferenceCategory` enum value to its canonical SPDX text representation. + +- *Parameters*: `SpdxReferenceCategory category` — the enum value to serialize. +- *Returns*: The canonical SPDX text (e.g., `"SECURITY"`, `"PACKAGE-MANAGER"`). +- *Preconditions*: `category` must not be `SpdxReferenceCategory.Missing`. +- *Postconditions*: none. +- *Exceptions*: `InvalidOperationException` — thrown when `category` is + `SpdxReferenceCategory.Missing` or an unsupported enum value. + +### Callers + +- **SpdxPackage** — holds the `ExternalReferences` array. +- **Spdx2JsonDeserializer** — constructs `SpdxExternalReference` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxExternalReference` instances to JSON. diff --git a/docs/design/spdx-model/spdx-extracted-licensing-info.md b/docs/design/spdx-model/spdx-extracted-licensing-info.md index 673d6e9..a80f564 100644 --- a/docs/design/spdx-model/spdx-extracted-licensing-info.md +++ b/docs/design/spdx-model/spdx-extracted-licensing-info.md @@ -1,33 +1,69 @@ -# SpdxExtractedLicensingInfo Unit Design +## SpdxExtractedLicensingInfo -## Purpose +### Purpose `SpdxExtractedLicensingInfo` records the full text and metadata of a non-standard license found within a software package. It is used when the license does not appear on the SPDX License List and must be captured verbatim for compliance purposes. -## Design +### Data Model -`SpdxExtractedLicensingInfo` is a sealed class with no base class. +**LicenseId**: `string` — Local identifier in `LicenseRef-…` format, unique within the document. -Data members: +**ExtractedText**: `string` — Full verbatim text of the license as found in the software. -| Property | Type | Description | -| -------- | ---- | ----------- | -| `LicenseId` | `string` | Local identifier in `LicenseRef-…` format | -| `ExtractedText` | `string` | Full verbatim text of the license | -| `Name` | `string?` | Optional human-readable license name | -| `CrossReferences` | `string[]` | Optional URIs to the license text elsewhere | -| `Comment` | `string?` | Optional explanatory comment | +**Name**: `string?` — Optional human-readable license name. -Key methods: +**CrossReferences**: `string[]` — Optional URIs pointing to the license text at canonical +external locations. -- `DeepCopy()` — returns a new instance with all fields deep-copied -- `Enhance(SpdxExtractedLicensingInfo)` — fills in missing fields from another instance -- `Enhance(array, array)` — static method merging two arrays by matching on `LicenseId` -- `Validate(List)` — appends validation issues to the supplied list -- `Same` — static `IEqualityComparer` comparing by `ExtractedText` +**Comment**: `string?` — Optional explanatory comment. -## Dependencies +### Key Methods -- No external dependencies beyond base .NET BCL types +**DeepCopy**: Returns a new instance with all fields deep-copied. + +- *Parameters*: none. +- *Returns*: `SpdxExtractedLicensingInfo` — independent copy. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxExtractedLicensingInfo other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty or null fields are populated from `other`; CrossReferences are merged by + concatenation and deduplication. + +**Enhance (static array merge)**: Merges two extracted licensing info arrays by matching on +`ExtractedText`. + +- *Parameters*: `SpdxExtractedLicensingInfo[] base`, `SpdxExtractedLicensingInfo[] additions`. +- *Returns*: `SpdxExtractedLicensingInfo[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Matching entries are enhanced; new entries are appended. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Missing required fields are recorded in `issues`. + +**Same**: `static IEqualityComparer` — compares by `ExtractedText`. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. No exceptions are +thrown by `DeepCopy`, `Enhance`, or the static merge method. + +### Dependencies + +- **SpdxHelpers** — `EnhanceString` used in `Enhance`. + +### Callers + +- **SpdxDocument** — holds the `ExtractedLicensingInfo` array. +- **Spdx2JsonDeserializer** — constructs `SpdxExtractedLicensingInfo` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxExtractedLicensingInfo` instances to JSON. diff --git a/docs/design/spdx-model/spdx-file.md b/docs/design/spdx-model/spdx-file.md index f639c37..7bf5bca 100644 --- a/docs/design/spdx-model/spdx-file.md +++ b/docs/design/spdx-model/spdx-file.md @@ -1,37 +1,134 @@ -# SpdxFile Unit Design +## SpdxFile -## Purpose +### Purpose `SpdxFile` represents an individual file within an SPDX document, enabling fine-grained tracking of source files, binaries, and other artifacts together with their licensing, checksums, and contributor information. -## Design +### Data Model -`SpdxFile` is a sealed class that extends `SpdxLicenseElement` (which extends `SpdxElement`), -inheriting `Id`, `ConcludedLicense`, `CopyrightText`, and related license fields. +**FileName**: `string` — Relative path of the file (e.g., `./src/main.c`). Used as the match +key when merging file arrays. -Data members (beyond inherited fields): +**FileTypes**: `SpdxFileType[]` — File type classifications (e.g., `SOURCE`, `BINARY`, +`DOCUMENTATION`). -| Property | Type | Description | -| -------- | ---- | ----------- | -| `FileName` | `string` | Relative path of the file (e.g., `./src/main.c`) | -| `FileTypes` | `SpdxFileType[]` | File type classifications (SOURCE, BINARY, etc.) | -| `Checksums` | `SpdxChecksum[]` | Integrity checksums for the file | -| `LicenseInfoInFiles` | `string[]` | License expressions found in the file | -| `Comment` | `string?` | Optional comment | -| `Notice` | `string?` | Optional copyright notice text | -| `Contributors` | `string[]` | Contributors to this file | +**Checksums**: `SpdxChecksum[]` — Integrity checksums for this file using one or more algorithms. -Key methods: +**LicenseInfoInFiles**: `string[]` — License expressions found within the file. -- `DeepCopy()` — returns a fully deep-copied instance -- `Enhance(SpdxFile)` — fills in missing fields from another instance -- `Enhance(array, array)` — static method merging two file arrays, matching on `FileName` -- `Validate(List)` — appends validation issues to the supplied list -- `Same` — static `IEqualityComparer` comparing by `FileName` +**Comment**: `string?` — Optional free-text comment. -## Dependencies +**Notice**: `string?` — Optional copyright notice text found in or about the file. -- `SpdxLicenseElement` (base class) -- `SpdxChecksum`, `SpdxFileType` (enum) +**Contributors**: `string[]` — Contributors to this file. + +*Inherited from `SpdxLicenseElement`*: `Id`, `ConcludedLicense`, `LicenseComments`, +`CopyrightText`, `AttributionText`, `Annotations`. + +### Key Methods + +**DeepCopy**: Returns a fully deep-copied instance. + +- *Parameters*: none. +- *Returns*: `SpdxFile` — independent copy including all arrays. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxFile other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Fields in this instance are updated when the current value has lower fitness + than the source value, following the hierarchy: concrete value > NOASSERTION > empty string > + null. `FileTypes`, `LicenseInfoInFiles`, `Contributors`, and `AttributionText` (inherited) are + merged by concatenation and deduplication. `Checksums` are merged using identity-match and + enhance via `SpdxChecksum.Enhance`. + +**Enhance (static array merge)**: Merges two file arrays, matching on `FileName`. + +- *Parameters*: `SpdxFile[] array`, `SpdxFile[] others`. +- *Returns*: `SpdxFile[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Matching entries are enhanced; new entries are appended. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: The following rules are checked and any violations are appended to `issues`: + 1. `FileName` must start with `"./"` (SPDX §4.1 — relative paths required). + 2. `Id` must match the `SPDXRef-[id-string]` format (SPDX §4.2). + 3. At least one SHA1 checksum must be present in `Checksums` (SPDX §4.4). + Nested checksums and annotations are also validated. + +**Same**: `static IEqualityComparer` — compares by `FileName`, with the condition that +two files with differing SHA1 checksums are considered distinct even if their `FileName` values +match. + +#### SHA1 Tiebreaker (SPDX specification rationale) + +The SPDX specification allows the same file path to be tracked at multiple revisions. If both +entries carry a SHA1 checksum and the values differ, they represent distinct file versions. +If either entry lacks a SHA1 checksum, identity falls back to `FileName` alone. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. Nested checksums +are also validated. No exceptions are thrown by `DeepCopy`, `Enhance`, or the static merge method. + +### Dependencies + +- **SpdxLicenseElement** — abstract base class providing license and copyright fields. +- **SpdxChecksum** — checksum instances in the `Checksums` array. +- **SpdxFileType** — enumeration for file type classification. + +### SpdxFileType and SpdxFileTypeExtensions + +`SpdxFileType` is an enumeration of the file type categories defined by the SPDX specification. + +#### Enum Values + +| Value | Description | +|-----------------|-------------------------------------------------------| +| `Source` | Human-readable source code. | +| `Binary` | Compiled object, target image, or binary executable. | +| `Archive` | Archive file (e.g., zip, tar). | +| `Application` | Application file. | +| `Audio` | Audio file. | +| `Image` | Image file. | +| `Text` | Human-readable text file. | +| `Video` | Video file. | +| `Documentation` | Documentation file. | +| `Spdx` | SPDX document. | +| `Other` | Other type not matching standard categories. | + +#### FromText + +Converts a file type string from an SPDX document to the corresponding `SpdxFileType` enum value. + +- *Parameters*: `string fileType` — the raw text from the SPDX document (case-insensitive). +- *Returns*: The matching `SpdxFileType` enum value. +- *Preconditions*: none. +- *Postconditions*: none. +- *Exceptions*: `InvalidOperationException` — thrown with a message identifying the unsupported + value when `fileType` does not match any known SPDX file type string. + +#### ToText + +Converts a `SpdxFileType` enum value to its canonical SPDX text representation. + +- *Parameters*: `SpdxFileType fileType` — the enum value to serialize. +- *Returns*: The canonical SPDX text (e.g., `"SOURCE"`, `"BINARY"`). +- *Preconditions*: `fileType` must be a supported enum value. +- *Postconditions*: none. +- *Exceptions*: `InvalidOperationException` — thrown when `fileType` is an unsupported enum value. + +### Callers + +- **SpdxDocument** — holds the `Files` array. +- **Spdx2JsonDeserializer** — constructs `SpdxFile` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxFile` instances to JSON. diff --git a/docs/design/spdx-model/spdx-helpers.md b/docs/design/spdx-model/spdx-helpers.md index 2a06700..01a1b98 100644 --- a/docs/design/spdx-model/spdx-helpers.md +++ b/docs/design/spdx-model/spdx-helpers.md @@ -1,32 +1,68 @@ -# SpdxHelpers Unit Design +## SpdxHelpers -## Purpose +### Purpose `SpdxHelpers` is an internal static utility class providing shared helper methods used across -the data model. It centralizes common operations such as string enhancement (selecting the -best available value by fitness ranking) and SPDX date-time validation. +the data model. It centralizes common operations such as string enhancement (selecting the best +available value by fitness ranking) and SPDX date-time validation. -## Design +### Data Model -`SpdxHelpers` is a `partial` internal static class. Date-time validation uses -`[GeneratedRegex]` on .NET 7 and later (source-generated, AOT-safe), with a cached `Regex` -instance as a fallback for earlier targets such as `netstandard2.0`. +N/A - `SpdxHelpers` is a static utility class with no instance state. -Key methods: +### Key Methods -| Method | Description | -| ------ | ----------- | -| `IsValidSpdxDateTime(string?)` | Returns `true` if the value matches ISO 8601 UTC format | -| `EnhanceString(params string?[])` | Returns the highest-fitness value: concrete > `NOASSERTION` > empty > `null` | +**IsValidSpdxDateTime**: Returns `true` if the supplied value matches the ISO 8601 UTC timestamp +format required by SPDX. -Key design decisions: +- *Parameters*: `string? value` — the timestamp string to validate. +- *Returns*: Returns `true` if `value` matches the ISO 8601 UTC format, or if `value` is null or + empty (both treated as not-set and therefore valid); `false` otherwise. +- *Preconditions*: none. +- *Postconditions*: none. -- `internal` visibility — not part of the public API; only used within the assembly. -- `partial` class enables the `[GeneratedRegex]` attribute on .NET 7+; pre-.NET 7 targets use - a cached `Regex` instance instead. -- `EnhanceString` uses a fitness ranking so that a meaningful value is always preferred over - `NOASSERTION` or absent values, regardless of argument order. +**EnhanceString**: Returns the highest-fitness string from the supplied candidates. -## Dependencies +- *Parameters*: `params string?[] values` — ordered list of candidate values. +- *Returns*: `string?` — the best candidate: concrete (non-empty, non-NOASSERTION) > `NOASSERTION` + > empty string > `null`. +- *Preconditions*: none. +- *Postconditions*: The returned value is the most informative of the candidates regardless of + argument order. -- `System.Text.RegularExpressions` — date-time validation regex +### Error Handling + +N/A - both methods are pure functions with no side effects. `IsValidSpdxDateTime` returns `false` +for invalid input rather than throwing. + +### Dependencies + +- **SpdxElement** — `SpdxElement.NoAssertion` constant used in `EnhanceString` fitness ranking to + identify `NOASSERTION` values. +- **System.Text.RegularExpressions** — date-time validation regex. On .NET 7 and later, a + source-generated `[GeneratedRegex]` is used for AOT safety; earlier targets use a cached + `Regex` instance. + +### Callers + +**EnhanceString callers** (all data model `Enhance` methods): + +- **SpdxElement** — `EnhanceString` used in `EnhanceElement`. +- **SpdxAnnotation** — `EnhanceString` used in `Enhance`. +- **SpdxChecksum** — `EnhanceString` used in `Enhance`. +- **SpdxCreationInformation** — `EnhanceString` used in `Enhance`. +- **SpdxExternalDocumentReference** — `EnhanceString` used in `Enhance`. +- **SpdxExternalReference** — `EnhanceString` used in `Enhance`. +- **SpdxExtractedLicensingInfo** — `EnhanceString` used in `Enhance`. +- **SpdxFile** — `EnhanceString` used in `Enhance`. +- **SpdxLicenseElement** — `EnhanceString` used in `Enhance`. +- **SpdxPackage** — `EnhanceString` used in `Enhance`. +- **SpdxPackageVerificationCode** — `EnhanceString` used in `Enhance`. +- **SpdxRelationship** — `EnhanceString` used in `Enhance`. +- **SpdxSnippet** — `EnhanceString` used in `Enhance`. + +**IsValidSpdxDateTime callers**: + +- **SpdxCreationInformation** — `IsValidSpdxDateTime` used in `Validate`. +- **SpdxAnnotation** — `IsValidSpdxDateTime` used in `Validate`. +- **SpdxPackage** — `IsValidSpdxDateTime` used in `Validate`. diff --git a/docs/design/spdx-model/spdx-license-element.md b/docs/design/spdx-model/spdx-license-element.md index 017b1a8..35e6d84 100644 --- a/docs/design/spdx-model/spdx-license-element.md +++ b/docs/design/spdx-model/spdx-license-element.md @@ -1,31 +1,68 @@ -# SpdxLicenseElement Unit Design +## SpdxLicenseElement -## Purpose +### Purpose `SpdxLicenseElement` is an abstract intermediate base class that adds license-related fields to `SpdxElement`. It is the common ancestor of `SpdxPackage`, `SpdxFile`, and `SpdxSnippet`, -avoiding duplication of the concluded-license, copyright, and attribution fields. +centralizing the concluded-license, copyright, and attribution fields to avoid duplication across +all three element types. -## Design +### Data Model -`SpdxLicenseElement` is a public abstract class that extends `SpdxElement`. +**ConcludedLicense**: `string` — License expression concluded by the SPDX document preparer +for this element. -Data members (beyond `SpdxElement.Id`): +**LicenseComments**: `string?` — Optional explanation of the concluded license choice. -| Property | Type | Description | -| -------- | ---- | ----------- | -| `ConcludedLicense` | `string` | License expression concluded by the SPDX document preparer | -| `LicenseComments` | `string?` | Explanation of the concluded license choice | -| `CopyrightText` | `string` | Copyright declarations text | -| `AttributionText` | `string[]` | Attribution notices required for use | -| `Annotations` | `SpdxAnnotation[]` | Additional information about this element | +**CopyrightText**: `string` — Copyright declarations text for this element. -Key design decisions: +**AttributionText**: `string[]` — Attribution notices required when redistributing or using +this element. -- Abstract (non-instantiable) — no direct consumers; always subclassed. -- Provides `EnhanceLicenseElement(SpdxLicenseElement)` protected helper analogous to - `SpdxElement.EnhanceElement` for consistent field merging. +**Annotations**: `SpdxAnnotation[]` — Element-level annotations (comments, reviews, or other +notes attached to this element). -## Dependencies +*Inherited from `SpdxElement`*: `Id`. -- `SpdxElement` (base class) +### Key Methods + +**EnhanceLicenseElement**: Protected helper that populates license-related fields from another +instance when the existing value has lower fitness than the source value. + +- *Parameters*: `SpdxLicenseElement other` — source element. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: + a) **String fields** (`ConcludedLicense`, `LicenseComments`, `CopyrightText`): fitness-based + selection — concrete value > NOASSERTION > empty string > null. + b) **AttributionText**: merged by concatenation and deduplication (union of both arrays). + c) **Annotations**: merged by identity-match (enhance existing entries by comparer) and + append (add new entries not already present). + d) **Base-class delegation**: also calls `EnhanceElement(other)`, which populates the + inherited `Id` field if absent. + +#### Algorithm + +The fitness ranking used for string fields is: null=0, empty string=1, NOASSERTION=2, concrete +value=3. The field with the higher fitness rank is retained. When both fields have equal fitness, +the current value is kept. + +For `AttributionText` and `Annotations`, both arrays are merged: existing entries are enhanced +in-place where a match is found (by annotation identity comparer), and unmatched entries from +`other` are appended as deep copies. + +### Error Handling + +N/A - `SpdxLicenseElement` is abstract. Subclasses implement their own `Validate` methods. + +### Dependencies + +- **SpdxElement** — abstract base class providing the `Id` property. +- **SpdxAnnotation** — element-level annotations. +- **SpdxHelpers** — shared utility functions for fitness-ranked string selection. + +### Callers + +- **SpdxPackage** — extends `SpdxLicenseElement`. +- **SpdxFile** — extends `SpdxLicenseElement`. +- **SpdxSnippet** — extends `SpdxLicenseElement`. diff --git a/docs/design/spdx-model/spdx-model.md b/docs/design/spdx-model/spdx-model.md index fbd7f7a..a9c7ecd 100644 --- a/docs/design/spdx-model/spdx-model.md +++ b/docs/design/spdx-model/spdx-model.md @@ -1,89 +1,139 @@ -# DemaConsulting.SpdxModel System Design - -## System Architecture +## SpdxModel DemaConsulting.SpdxModel is a .NET library providing a complete implementation of the SPDX (Software Package Data Exchange) data model. The library exposes an in-memory object model -representing all SPDX document elements, plus serialization and transformation capabilities. - -### Subsystems - -| Subsystem | Folder | Responsibility | -| --------- | ------ | -------------- | -| IO | `IO/` | JSON serialization and deserialization for SPDX 2.2 and 2.3 formats | -| Transform | `Transform/` | Utilities for manipulating SPDX documents in memory | - -### Data Model Units - -| Unit | File | Responsibility | -| ---- | ---- | -------------- | -| `SpdxElement` | `SpdxElement.cs` | Abstract base for all identifiable SPDX elements | -| `SpdxLicenseElement` | `SpdxLicenseElement.cs` | Abstract base for elements carrying license and copyright fields | -| `SpdxDocument` | `SpdxDocument.cs` | Root container of a complete SPDX document | -| `SpdxPackage` | `SpdxPackage.cs` | Represents a software package in the SBOM | -| `SpdxFile` | `SpdxFile.cs` | Represents an individual file in the SBOM | -| `SpdxSnippet` | `SpdxSnippet.cs` | Represents a code snippet within a file | -| `SpdxRelationship` | `SpdxRelationship.cs` | Represents a directional relationship between elements | -| `SpdxAnnotation` | `SpdxAnnotation.cs` | Represents a review or assessment annotation | -| `SpdxChecksum` | `SpdxChecksum.cs` | Represents a cryptographic checksum | -| `SpdxCreationInformation` | `SpdxCreationInformation.cs` | Metadata about document authorship and creation time | -| `SpdxExternalDocumentReference` | `SpdxExternalDocumentReference.cs` | Reference to an external SPDX document | -| `SpdxExternalReference` | `SpdxExternalReference.cs` | Reference to an external resource (registry, VDB, etc.) | -| `SpdxExtractedLicensingInfo` | `SpdxExtractedLicensingInfo.cs` | Non-standard license text extracted from software | -| `SpdxPackageVerificationCode` | `SpdxPackageVerificationCode.cs` | Cryptographic integrity code for a package | -| `SpdxHelpers` | `SpdxHelpers.cs` | Shared utility functions (date-time validation, string fitness) | - -## External Interfaces and Dependencies - -### External Dependencies - -- **System.Text.Json** — used by the IO subsystem for JSON reading and writing; available in-box on - modern .NET targets and via NuGet for .NET Standard 2.0 -- **.NET Standard 2.0 / .NET 8 / .NET 9 / .NET 10** — target frameworks - -### Public API Surface - -The library exposes: - -- `SpdxDocument` — root object representing a complete SPDX document -- Data model classes for all SPDX elements -- `Spdx2JsonDeserializer` — reads SPDX JSON into the object model -- `Spdx2JsonSerializer` — writes the object model to SPDX JSON -- `SpdxRelationships` — static utilities for relationship manipulation - -## Data Flow - -```text -JSON File - │ - ▼ -Spdx2JsonDeserializer ──► SpdxDocument (in-memory model) - │ - (manipulate via - Transform utilities) - │ - ▼ - Spdx2JsonSerializer ──► JSON File +representing all SPDX document elements, with serialization and transformation capabilities. + +### Architecture + +```mermaid +flowchart TD + subgraph IO + Spdx2JsonDeserializer + Spdx2JsonSerializer + SpdxConstants + end + subgraph Transform + SpdxRelationships + end + SpdxDocument + SpdxElement + SpdxLicenseElement + SpdxPackage + SpdxFile + SpdxSnippet + SpdxRelationship + SpdxAnnotation + SpdxChecksum + SpdxCreationInformation + SpdxExternalDocumentReference + SpdxExternalReference + SpdxExtractedLicensingInfo + SpdxPackageVerificationCode + SpdxHelpers + + SpdxDocument --> SpdxElement + SpdxLicenseElement --> SpdxElement + SpdxPackage --> SpdxLicenseElement + SpdxFile --> SpdxLicenseElement + SpdxSnippet --> SpdxLicenseElement + SpdxRelationship --> SpdxElement + SpdxAnnotation --> SpdxElement + + Spdx2JsonDeserializer --> SpdxDocument + Spdx2JsonDeserializer --> SpdxConstants + Spdx2JsonSerializer --> SpdxDocument + Spdx2JsonSerializer --> SpdxConstants + SpdxRelationships --> SpdxDocument + SpdxRelationships --> SpdxRelationship + SpdxDocument --> SpdxPackage + SpdxDocument --> SpdxFile + SpdxDocument --> SpdxSnippet + SpdxDocument --> SpdxRelationship + SpdxDocument --> SpdxAnnotation + SpdxDocument --> SpdxExternalDocumentReference + SpdxDocument --> SpdxCreationInformation + SpdxDocument --> SpdxExtractedLicensingInfo + SpdxFile --> SpdxChecksum + SpdxPackage --> SpdxChecksum + SpdxPackage --> SpdxExternalReference + SpdxPackage --> SpdxPackageVerificationCode + SpdxExternalDocumentReference --> SpdxChecksum + Spdx2JsonDeserializer --> SpdxHelpers + Spdx2JsonSerializer --> SpdxHelpers ``` -## System-Wide Design Constraints and Decisions +### External Interfaces + +**SPDX JSON Input**: JSON file conforming to the SPDX 2.2 or 2.3 JSON schema. + +- *Type*: File (JSON) +- *Role*: Consumer +- *Contract*: `Spdx2JsonDeserializer.Deserialize(string)` accepts raw JSON text and returns a + populated `SpdxDocument`. +- *Constraints*: Input must be valid JSON; SPDX field validation is performed after + deserialization via `SpdxDocument.Validate()`. + +**SPDX JSON Output**: JSON file conforming to the SPDX 2.3 JSON schema. + +- *Type*: File (JSON) +- *Role*: Provider +- *Contract*: `Spdx2JsonSerializer.Serialize(SpdxDocument)` returns a complete SPDX 2.3 JSON + string. +- *Constraints*: Optional fields are omitted when empty or null; output always conforms to SPDX + 2.3 schema. + +**In-Process .NET Public API**: Object model and transformation API consumed by .NET callers. -- **Immutability by convention**: data model classes use public mutable properties to allow - flexible construction while deep-copy methods provide safe cloning -- **Nullable reference types enabled**: all public API members declare nullability explicitly -- **Minimal runtime dependencies**: keeps the library lightweight and avoids dependency conflicts - for consumers by relying only on BCL/framework-provided APIs where available, with - compatibility NuGet packages used on older targets such as `netstandard2.0` -- **Target multi-framework**: the library targets `netstandard2.0`, `net8.0`, `net9.0`, - and `net10.0` simultaneously +- *Type*: In-process .NET public API +- *Role*: Provider +- *Contract*: Exposes `SpdxDocument` and all data model classes, `Spdx2JsonDeserializer`, + `Spdx2JsonSerializer`, and `SpdxRelationships` as public types. +- *Constraints*: Targets `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`. -## Integration Patterns +#### Error Handling -Consumers typically: +- `Spdx2JsonDeserializer.Deserialize` throws `System.Text.Json.JsonException` when the input is + fatally malformed JSON (i.e., the input cannot be parsed as a JSON document). Missing or unknown + SPDX fields do not cause an exception; they are silently ignored or left at their default values. +- `SpdxDocument.Validate(List)` never throws; it appends human-readable issue strings to + the supplied list and returns normally, allowing callers to inspect all issues at once. -1. Deserialize an SPDX document from a JSON file using `Spdx2JsonDeserializer` -2. Inspect or modify the `SpdxDocument` object model in memory -3. Serialize back to JSON using `Spdx2JsonSerializer` +### Dependencies + +- **System.Text.Json**: used by the IO subsystem for JSON DOM parsing and serialization; + available in-box on modern .NET targets and via NuGet for .NET Standard 2.0. + +### Risk Control Measures + +N/A - not a safety-classified software item. + +### Data Flow + +```mermaid +flowchart LR + A[JSON File] --> B[Spdx2JsonDeserializer] + B --> C[SpdxDocument] + C --> D[Transform utilities] + D --> C + C --> E[Spdx2JsonSerializer] + E --> F[JSON File] +``` -For programmatic SBOM construction, consumers create `SpdxDocument` instances directly and -populate the data model before serializing. +1. Caller provides a JSON string to `Spdx2JsonDeserializer.Deserialize`. +2. The deserializer uses `System.Text.Json.Nodes` to parse the JSON DOM. +3. Per-element helpers populate a new `SpdxDocument` instance. +4. The caller inspects or modifies the `SpdxDocument` in memory, optionally using + `SpdxRelationships` utilities. +5. `Spdx2JsonSerializer.Serialize` traverses the `SpdxDocument` and produces a JSON string. + +### Design Constraints + +- Targets `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0` simultaneously; the library builds + and runs on Windows, Linux, and macOS. +- Minimal runtime dependencies: relies on BCL/framework APIs where possible; compatibility NuGet + packages used on older targets. +- Nullable reference types enabled: all public API members declare nullability explicitly. +- Data model classes use public mutable properties to allow flexible construction; deep-copy + methods provide safe cloning. +- No static mutable state in data model classes; thread safety is the caller's responsibility. diff --git a/docs/design/spdx-model/spdx-package-verification-code.md b/docs/design/spdx-model/spdx-package-verification-code.md index d2d4d8f..c8af38d 100644 --- a/docs/design/spdx-model/spdx-package-verification-code.md +++ b/docs/design/spdx-model/spdx-package-verification-code.md @@ -1,29 +1,56 @@ -# SpdxPackageVerificationCode Unit Design +## SpdxPackageVerificationCode -## Purpose +### Purpose `SpdxPackageVerificationCode` represents an SPDX package verification code — a SHA1 digest computed over the contents of a package (optionally excluding specified files). It provides cryptographic assurance that package contents have not been modified. -## Design +### Data Model -`SpdxPackageVerificationCode` is a sealed class with no base class. +**Value**: `string` — SHA1 hex digest computed over the sorted file checksums of the package. -Data members: +**ExcludedFiles**: `string[]` — File paths excluded from the verification code computation +(e.g., the `.spdx` file itself). -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Value` | `string` | SHA1 hex digest of the package contents | -| `ExcludedFiles` | `string[]` | Files excluded from the verification code computation | +### Key Methods -Key methods: +**DeepCopy**: Returns a new instance with all fields deep-copied. -- `DeepCopy()` — returns a new instance with all fields deep-copied -- `Enhance(SpdxPackageVerificationCode)` — fills in missing fields from another instance -- `Validate(string, List)` — validates the code value; `string` parameter is the owning package name -- `Same` — static `IEqualityComparer` comparing by `Value` and `ExcludedFiles` +- *Parameters*: none. +- *Returns*: `SpdxPackageVerificationCode` — independent copy. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. -## Dependencies +**Enhance**: Fills in missing fields from another instance. -- No external dependencies beyond base .NET BCL types +- *Parameters*: `SpdxPackageVerificationCode other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty or null fields in this instance are populated from `other`. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `string package` — owning package name for error messages; + `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: An empty or malformed `Value` is recorded in `issues`. + +**Same**: `static IEqualityComparer` — compares by `Value` only. +`ExcludedFiles` is not considered for equality. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. No exceptions are +thrown by `DeepCopy` or `Enhance`. + +### Dependencies + +N/A - no external dependencies beyond base .NET BCL types. + +### Callers + +- **SpdxPackage** — holds an optional `VerificationCode` instance. +- **Spdx2JsonDeserializer** — constructs `SpdxPackageVerificationCode` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxPackageVerificationCode` instances to JSON. diff --git a/docs/design/spdx-model/spdx-package.md b/docs/design/spdx-model/spdx-package.md index 99ffdc8..4ce7b40 100644 --- a/docs/design/spdx-model/spdx-package.md +++ b/docs/design/spdx-model/spdx-package.md @@ -1,43 +1,134 @@ -# SpdxPackage Unit Design +## SpdxPackage -## Purpose +### Purpose `SpdxPackage` represents an SPDX package — the primary building block of a Software Bill of Materials. It captures identity, provenance, licensing, verification, and dependency metadata for a software package. -## Design - -`SpdxPackage` is a sealed class that extends `SpdxLicenseElement`, inheriting `Id`, -`ConcludedLicense`, `CopyrightText`, and attribution fields. - -Data members (key fields beyond inherited): - -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Name` | `string` | Package name | -| `Version` | `string?` | Package version string | -| `FileName` | `string?` | Filename of the package archive | -| `Supplier` / `Originator` | `string?` | Entity distributing / originating the package | -| `DownloadLocation` | `string` | URI from which the package was obtained | -| `FilesAnalyzed` | `bool?` | Whether files in the package have been analyzed | -| `VerificationCode` | `SpdxPackageVerificationCode?` | Cryptographic verification code | -| `Checksums` | `SpdxChecksum[]` | Package-level checksums | -| `LicenseInfoFromFiles` | `string[]` | Licenses found in files of the package | -| `DeclaredLicense` | `string` | License declared by the package authors; may be empty when not specified | -| `ExternalReferences` | `SpdxExternalReference[]` | Links to external resources | -| `PrimaryPackagePurpose` | `string?` | Primary purpose classification | - -Key methods: - -- `DeepCopy()` — returns a fully deep-copied instance -- `Enhance(SpdxPackage)` — fills in missing fields from another instance -- `Enhance(array, array)` — static merging of two package arrays, matching on `Name` + `Version` -- `Validate(List, SpdxDocument?, bool ntia)` — full validation including NTIA minimum elements; - empty `DeclaredLicense` is permitted and does not produce a validation issue -- `Same` — static `IEqualityComparer` comparing by `Name` and `Version` - -## Dependencies - -- `SpdxLicenseElement` (base class) -- `SpdxChecksum`, `SpdxExternalReference`, `SpdxPackageVerificationCode` +### Data Model + +**Name**: `string` — Package name. Used as the match key (together with `Version`) for array +merging. + +**Version**: `string?` — Package version string; null if not specified. + +**FileName**: `string?` — Filename of the package archive or distribution artifact. + +**Supplier**: `string?` — Entity distributing the package (in `Organization: name` or +`Person: name` format). + +**Originator**: `string?` — Entity that originally authored or created the package. + +**DownloadLocation**: `string` — URI from which the package was or can be obtained. + +**FilesAnalyzed**: `bool?` — Whether the files within the package have been analyzed; `null` +means unspecified. + +**HasFiles**: `string[]` — SPDX IDs of `SpdxFile` elements that belong to this package. When +`doc` is passed to `Validate`, each ID is verified to exist in `doc.Files`. + +**VerificationCode**: `SpdxPackageVerificationCode?` — Cryptographic verification code computed +over the package's files; absent when `FilesAnalyzed` is `false`. + +**Checksums**: `SpdxChecksum[]` — Package-level checksums using one or more algorithms. + +**HomePage**: `string?` — URI of the package home page; `null` if not specified. + +**SourceInformation**: `string?` — Human-readable description of how the package was acquired +or modified from the original source; `null` if not specified. + +**LicenseInfoFromFiles**: `string[]` — License expressions found in files of the package. + +**DeclaredLicense**: `string` — License declared by the package authors; may be empty when not +specified by the package. + +**Summary**: `string?` — Short human-readable description of the package; `null` if not +specified. + +**Description**: `string?` — Detailed human-readable description of the package; `null` if not +specified. + +**Comment**: `string?` — Free-text human annotation; `null` if not specified. + +**ExternalReferences**: `SpdxExternalReference[]` — Links to external resources such as package +registries or vulnerability databases. + +**PrimaryPackagePurpose**: `string?` — Primary purpose classification (e.g., `LIBRARY`, +`APPLICATION`). + +**ReleaseDate**: `string?` — Date and time the package was released, in SPDX date-time format +(`YYYY-MM-DDThh:mm:ssZ`); `null` if not specified. + +**BuiltDate**: `string?` — Date and time the package was built, in SPDX date-time format +(`YYYY-MM-DDThh:mm:ssZ`); `null` if not specified. + +**ValidUntilDate**: `string?` — Date and time after which the package should no longer be +considered valid, in SPDX date-time format (`YYYY-MM-DDThh:mm:ssZ`); `null` if not specified. + +*Inherited from `SpdxLicenseElement`*: `Id`, `ConcludedLicense`, `LicenseComments`, +`CopyrightText`, `AttributionText`, `Annotations`. + +### Key Methods + +**DeepCopy**: Returns a fully deep-copied instance. + +- *Parameters*: none. +- *Returns*: `SpdxPackage` — independent copy including all nested objects and arrays. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxPackage other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty or null fields in this instance are populated from `other`. The nullable + `FilesAnalyzed` field is populated from `other` when null. Array fields `LicenseInfoFromFiles`, + `Checksums`, and `ExternalReferences` are merged by deduplication. The `HasFiles` array is + intentionally not merged because it contains document-scoped SPDX element IDs that may not be + valid across documents. + +**Enhance (static array merge)**: Merges two package arrays, matching on `Name` and `Version`. + +- *Parameters*: `SpdxPackage[] base`, `SpdxPackage[] additions`. +- *Returns*: `SpdxPackage[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Matching entries are enhanced; new entries are appended. + +**Validate**: Validates the package, including NTIA minimum element checks when requested. + +- *Parameters*: `List issues` — list to append issues to; `SpdxDocument? document` — + owning document for cross-reference validation; `bool ntia` — when `true`, also checks NTIA + SBOM minimum elements. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: All discovered issues including nested checksum and external reference + issues are appended to `issues`; an empty `DeclaredLicense` does not produce a validation issue. + Non-null `ReleaseDate`, `BuiltDate`, or `ValidUntilDate` values that do not conform to the SPDX + date-time format (`YYYY-MM-DDThh:mm:ssZ`) each cause a validation issue to be recorded. + When `doc` is non-null, entries in `HasFiles` that do not match any file ID in `doc.Files` + cause an issue to be recorded. + +**Same**: `static IEqualityComparer` — compares by `Name` and `Version`. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. Nested checksums, +external references, and the verification code are also validated. No exceptions are thrown by +`DeepCopy`, `Enhance`, or the static merge method. When `doc` is provided and `HasFiles` +contains IDs for files not present in `doc.Files`, a single issue is appended: +`Package '{name}' HasFiles references missing files`. + +### Dependencies + +- **SpdxLicenseElement** — abstract base class. +- **SpdxChecksum** — package-level checksums. +- **SpdxExternalReference** — external resource links. +- **SpdxPackageVerificationCode** — optional package integrity code. + +### Callers + +- **SpdxDocument** — holds the `Packages` array. +- **Spdx2JsonDeserializer** — constructs `SpdxPackage` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxPackage` instances to JSON. diff --git a/docs/design/spdx-model/spdx-relationship.md b/docs/design/spdx-model/spdx-relationship.md index fd31528..e1c8521 100644 --- a/docs/design/spdx-model/spdx-relationship.md +++ b/docs/design/spdx-model/spdx-relationship.md @@ -1,36 +1,129 @@ -# SpdxRelationship Unit Design +## SpdxRelationship -## Purpose +### Purpose `SpdxRelationship` represents a directed relationship between two SPDX elements. Relationships define the dependency graph, containment hierarchy, and other associations between packages, files, and snippets in an SPDX document. -## Design +### Data Model -`SpdxRelationship` is a sealed class that extends `SpdxElement` (inheriting the `Id` field, -which identifies the *source* element of the relationship). +**Id** (inherited): `string` — SPDX ID of the source element of the relationship. -Data members: +**RelatedSpdxElement**: `string` — SPDX ID of the target element. May be `NOASSERTION` or use +a `DocumentRef-` prefix for cross-document references. -| Property | Type | Description | -| -------- | ---- | ----------- | -| `Id` (inherited) | `string` | SPDX ID of the source element | -| `RelatedSpdxElement` | `string` | SPDX ID of the target element | -| `RelationshipType` | `SpdxRelationshipType` | Type of relationship (DESCRIBES, CONTAINS, DEPENDS_ON, etc.) | -| `Comment` | `string?` | Optional explanatory comment | +**RelationshipType**: `SpdxRelationshipType` — Type of relationship (e.g., `DESCRIBES`, +`CONTAINS`, `DEPENDS_ON`, `GENERATED_FROM`). -Key methods: +**Comment**: `string?` — Optional explanatory comment. -- `DeepCopy()` — returns a new instance with all fields copied -- `Enhance(SpdxRelationship)` — fills in missing fields from another instance -- `Enhance(array, array)` — static method merging two relationship arrays by source, target, and type -- `Validate(List, SpdxDocument?)` — validates element ID references exist in the document -- `Same` — static `IEqualityComparer` comparing source, target, and type -- `SameElements` — static `IEqualityComparer` comparing only source and target (ignoring type) +### Key Methods -## Dependencies +**DeepCopy**: Returns a new instance with all fields copied. -- `SpdxElement` (base class) -- `SpdxRelationshipType` (enum) -- `SpdxDocument` — used during validation to resolve element IDs +- *Parameters*: none. +- *Returns*: `SpdxRelationship` — independent copy. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxRelationship other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty or null fields are populated from `other`. + +**Enhance (static array merge)**: Merges two relationship arrays by matching on source ID, target +ID, and type. + +- *Parameters*: `SpdxRelationship[] base`, `SpdxRelationship[] additions`. +- *Returns*: `SpdxRelationship[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Matching entries are enhanced; new entries are appended. + +**Validate**: Validates the relationship fields and, when a document is provided, verifies that +referenced element IDs exist in that document. + +- *Parameters*: `List issues` — list to append issues to; `SpdxDocument? document` — + optional document for element ID resolution. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: An empty source ID, an empty related element ID, or a `Missing` relationship + type each produce a validation issue. When `document` is provided, source and related element + IDs that do not resolve to an element in that document also produce issues, unless the related + element is `NOASSERTION` or uses a `DocumentRef-` cross-document prefix. + +**Same**: `static IEqualityComparer` — compares by source ID, target ID, and +relationship type. + +**SameElements**: `static IEqualityComparer` — compares by source ID and +target ID only, ignoring relationship type. Used by `SpdxRelationships.Add` when `replace` is +`true`. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. No exceptions are +thrown by `DeepCopy`, `Enhance`, or the static merge method. + +### Dependencies + +- **SpdxElement** — base class providing the `Id` property. +- **SpdxRelationshipType** — enumeration of relationship types. +- **SpdxDocument** — used during `Validate` to resolve element IDs. + +### Callers + +- **SpdxDocument** — holds the `Relationships` array. +- **SpdxRelationships** — adds and deduplicates relationships in a document. +- **Spdx2JsonDeserializer** — constructs `SpdxRelationship` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxRelationship` instances to JSON. + +### SpdxRelationshipType + +#### Overview + +`SpdxRelationshipType` is an enumeration of SPDX-defined relationship type tokens and the +`SpdxRelationshipTypeExtensions` static class provides round-trip text conversion between enum +values and their canonical SPDX string representations. + +#### Enum Values + +The enumeration defines 45 relationship type values plus the sentinel value `Missing` (= -1). +Key values include: + +| Enum Value | SPDX String | +|-----------------|-------------------------------| +| `Missing` | (sentinel — not serializable) | +| `Describes` | `DESCRIBES` | +| `Contains` | `CONTAINS` | +| `DependsOn` | `DEPENDS_ON` | +| `GeneratedFrom` | `GENERATED_FROM` | +| … | … (45 values total) | + +#### Conversion Methods + +**FromText**: Converts a string to `SpdxRelationshipType`. + +- *Parameters*: `string relationshipType` — SPDX relationship type string (case-insensitive); + `null` is treated as empty string. +- *Returns*: `SpdxRelationshipType` — corresponding enum value. +- *Postconditions*: An empty or null string returns `Missing`. +- *Throws*: `InvalidOperationException` when the string is not recognized. + +**ToText**: Converts a `SpdxRelationshipType` to its canonical SPDX string. + +- *Parameters*: `SpdxRelationshipType relationshipType` — enum value to convert. +- *Returns*: `string` — SPDX text representation. +- *Throws*: `InvalidOperationException` when `relationshipType` is `Missing` or an unrecognized enum value. + +#### Enum Error Handling + +- `FromText` throws `InvalidOperationException` for unrecognized (non-empty) strings. +- `ToText` throws `InvalidOperationException` for `Missing` or out-of-range enum values. +- Neither method performs I/O or has side effects. + +#### Dependencies and Callers + +- Consumed by **Spdx2JsonDeserializer** (via `FromText`) during JSON parsing. +- Consumed by **Spdx2JsonSerializer** (via `ToText`) during JSON serialization. diff --git a/docs/design/spdx-model/spdx-snippet.md b/docs/design/spdx-model/spdx-snippet.md index b2483d2..c69c78b 100644 --- a/docs/design/spdx-model/spdx-snippet.md +++ b/docs/design/spdx-model/spdx-snippet.md @@ -1,37 +1,80 @@ -# SpdxSnippet Unit Design +## SpdxSnippet -## Purpose +### Purpose `SpdxSnippet` represents a portion of a file in an SPDX document. Snippets are used when a -specific range of bytes (or lines) within a file has different licensing or provenance from the -rest of the file, enabling granular compliance tracking for reused code segments. +specific byte or line range within a file has different licensing or provenance from the rest of +the file, enabling granular compliance tracking for reused code segments. -## Design +### Data Model -`SpdxSnippet` is a sealed class that extends `SpdxLicenseElement`, inheriting `Id`, -`ConcludedLicense`, `CopyrightText`, and attribution fields. +**SnippetFromFile**: `string` — SPDX ID of the `SpdxFile` containing this snippet. -Data members (beyond inherited fields): +**SnippetByteStart**: `int` — Inclusive start byte offset of the snippet within the file. -| Property | Type | Description | -| -------- | ---- | ----------- | -| `SnippetFromFile` | `string` | SPDX ID of the file containing this snippet | -| `SnippetByteStart` | `int` | Inclusive start byte offset of the snippet | -| `SnippetByteEnd` | `int` | Inclusive end byte offset of the snippet | -| `SnippetLineStart` | `int` | Optional start line number | -| `SnippetLineEnd` | `int` | Optional end line number | -| `LicenseInfoInSnippet` | `string[]` | License expressions found in this snippet | -| `Comment` | `string?` | Optional comment | -| `Name` | `string?` | Optional human-readable snippet name | +**SnippetByteEnd**: `int` — Inclusive end byte offset of the snippet within the file. -Key methods: +**SnippetLineStart**: `int` — Optional start line number; `0` if unspecified. -- `DeepCopy()` — returns a fully deep-copied instance -- `Enhance(SpdxSnippet)` — fills in missing fields from another instance -- `Enhance(array, array)` — static method merging snippet arrays, matching on file ID and byte range -- `Validate(List)` — appends validation issues to the supplied list -- `Same` — static `IEqualityComparer` comparing by file, byte start, and byte end +**SnippetLineEnd**: `int` — Optional end line number; `0` if unspecified. -## Dependencies +**LicenseInfoInSnippet**: `string[]` — License expressions found in this snippet. -- `SpdxLicenseElement` (base class) +**Comment**: `string?` — Optional free-text comment. + +**Name**: `string?` — Optional human-readable snippet name. + +*Inherited from `SpdxLicenseElement`*: `Id`, `ConcludedLicense`, `LicenseComments`, +`CopyrightText`, `AttributionText`, `Annotations`. + +### Key Methods + +**DeepCopy**: Returns a fully deep-copied instance. + +- *Parameters*: none. +- *Returns*: `SpdxSnippet` — independent copy. +- *Preconditions*: none. +- *Postconditions*: The returned instance shares no mutable references with the original. + +**Enhance**: Fills in missing fields from another instance. + +- *Parameters*: `SpdxSnippet other` — source of additional field values. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Empty or null string fields are populated from `other`; integer fields with value + `0` are replaced by the corresponding non-zero value from `other`; `LicenseInfoInSnippet` is merged + by deduplication from both instances. + +**Enhance (static array merge)**: Merges two snippet arrays by matching on file SPDX ID and byte +range. + +- *Parameters*: `SpdxSnippet[] array`, `SpdxSnippet[] others`. +- *Returns*: `SpdxSnippet[]` — merged array. +- *Preconditions*: none. +- *Postconditions*: Matching entries are enhanced; new entries are appended. + +**Validate**: Appends validation issues to the supplied list. + +- *Parameters*: `List issues` — list to append issues to. +- *Returns*: `void` +- *Preconditions*: none. +- *Postconditions*: Missing required fields, invalid byte ranges, malformed IDs, and invalid + annotations are recorded in `issues`. + +**Same**: `static IEqualityComparer` — compares by `SnippetFromFile`, byte start, +and byte end. + +### Error Handling + +Validation errors are collected into the `List` passed to `Validate`. No exceptions are +thrown by `DeepCopy`, `Enhance`, or the static merge method. + +### Dependencies + +- **SpdxLicenseElement** — abstract base class. + +### Callers + +- **SpdxDocument** — holds the `Snippets` array. +- **Spdx2JsonDeserializer** — constructs `SpdxSnippet` instances during deserialization. +- **Spdx2JsonSerializer** — serializes `SpdxSnippet` instances to JSON. diff --git a/docs/design/spdx-model/transform/spdx-relationships.md b/docs/design/spdx-model/transform/spdx-relationships.md index a8f5456..3f43852 100644 --- a/docs/design/spdx-model/transform/spdx-relationships.md +++ b/docs/design/spdx-model/transform/spdx-relationships.md @@ -1,30 +1,60 @@ -# SpdxRelationships Unit Design +### SpdxRelationships -## Purpose +#### Purpose `SpdxRelationships` provides utility methods for adding SPDX relationships to an `SpdxDocument` without duplication. It simplifies the common pattern of programmatically constructing SPDX relationship graphs by handling deduplication automatically. -## Design +#### Data Model -`SpdxRelationships` is a public static utility class with no instance state. +N/A - `SpdxRelationships` is a public static utility class with no instance state. -Key methods: +#### Key Methods -| Method | Description | -| ------ | ----------- | -| `Add(SpdxDocument, IEnumerable, bool)` | Adds relationships with deduplication; optional replace | -| `Add(SpdxDocument, SpdxRelationship)` | Adds a single relationship if not already present | +**Add (batch)**: Adds multiple relationships to a document with optional replacement. -Key design decisions: +- *Parameters*: `SpdxDocument document` — target document; `IEnumerable relationships` — + relationships to add; `bool replace = false` — when `true`, existing relationships with matching + source and target elements are removed before adding. +- *Returns*: `void` +- *Preconditions*: Each relationship's source element ID must exist in the document. Each + relationship's target element ID must either exist in the document, be `NOASSERTION`, or use + the `DocumentRef-` prefix. +- *Postconditions*: All supplied relationships are present in the document; if `replace` is `true`, + previously existing relationships with the same source and target are removed. +- *Note*: When `replace` is `true`, removal uses `SpdxRelationship.SameElements` (type-agnostic, + matches by source and target ID only) so that a replace-and-add can change the relationship type + between the same pair of elements. Deduplication during the add path uses `SpdxRelationship.Same` + (type-inclusive) so that relationships of different types between the same elements co-exist. -- Deduplication is performed using the `SpdxRelationship.Same` equality comparer so that the - same logical relationship (same elements and type) is never written twice. -- The optional `replace` flag on the batch overload allows callers to update existing - relationships rather than skip duplicates. +**Add (single)**: Adds a single relationship to the document if not already present. -## Dependencies +- *Parameters*: `SpdxDocument document` — target document; `SpdxRelationship relationship` — + relationship to add. +- *Returns*: `void` +- *Preconditions*: The relationship source ID must match an element in the document. The target + ID must either match an element in the document, be `NOASSERTION`, or use the `DocumentRef-` + external-reference prefix. +- *Postconditions*: If the same logical relationship (same source, target, and type) already + exists, it is enhanced with any new field values. Otherwise a deep copy is appended. -- `SpdxDocument` — the target document whose `Relationships` array is modified -- `SpdxRelationship` — the relationship type and its `Same` equality comparer +#### Error Handling + +An `ArgumentException` (parameter name: `relationship`) is thrown under two conditions: + +1. The relationship's source element ID (`SpdxRelationship.Id`) is not found in the document. +2. The relationship's target element ID (`SpdxRelationship.RelatedSpdxElement`) is not found + in the document and is neither `NOASSERTION` nor prefixed with `DocumentRef-`. + +These checks prevent malformed documents from being constructed. In the batch overload all +relationships are validated before any mutation so that a failure leaves the document unchanged. + +#### Dependencies + +- **SpdxDocument** — the target document whose `Relationships` array is modified. +- **SpdxRelationship** — the relationship type and its `Same` and `SameElements` equality comparers. + +#### Callers + +- External consumers of the library who programmatically build or modify SPDX relationship graphs. diff --git a/docs/design/spdx-model/transform/transform.md b/docs/design/spdx-model/transform/transform.md index 81e11dd..71347a9 100644 --- a/docs/design/spdx-model/transform/transform.md +++ b/docs/design/spdx-model/transform/transform.md @@ -1,36 +1,55 @@ -# Transform Subsystem Design - -## Purpose +### Transform The Transform subsystem provides utilities for manipulating SPDX documents in memory, enabling consumers to programmatically build and modify SPDX relationship graphs. -## Units +#### Overview -| Unit | File | Responsibility | -| ---- | ---- | -------------- | -| `SpdxRelationships` | `Transform/SpdxRelationships.cs` | Utilities for adding and managing SPDX relationships | +The Transform subsystem contains a single unit, `SpdxRelationships`, which provides static +helper methods for adding relationships to an `SpdxDocument`. It handles deduplication +automatically so that callers do not need to check for existing relationships before adding new +ones. -## Design +#### Interfaces -### SpdxRelationships +**SpdxRelationships.Add (batch)**: Adds multiple relationships to a document with deduplication. -`SpdxRelationships` is a static utility class that provides helper methods for adding relationships -to an `SpdxDocument`. It ensures relationships are added without duplication and in a consistent -manner, reducing boilerplate for consumers constructing SPDX documents programmatically. +- *Type*: In-process .NET public API +- *Role*: Provider +- *Contract*: Accepts an `SpdxDocument`, an `IEnumerable`, and an optional + `replace` flag. Adds each relationship if not already present; when `replace` is `true`, + existing relationships with matching source and target elements are removed first. +- *Constraints*: The source element ID must always exist in the document; the target element ID + must either exist in the document, be `NOASSERTION`, or use the `DocumentRef-` external-reference + prefix. An `ArgumentException` is thrown when either constraint is violated. +- *Atomicity*: When `ArgumentException` is thrown, the document is left in its original state — + no relationships are added or removed. -Key methods: +**SpdxRelationships.Add (single)**: Adds a single relationship to a document if not already present. -- `Add(...)` — adds a single relationship to the document if it does not already exist -- `Add(...)` — adds multiple relationships, deduplicating against existing entries, with an optional `replace` parameter +- *Type*: In-process .NET public API +- *Role*: Provider +- *Contract*: Accepts an `SpdxDocument` and an `SpdxRelationship`. If the same relationship + (same source, target, and type) already exists it is enhanced; otherwise a deep copy is appended. +- *Constraints*: The source element ID must exist in the document. The target element ID must + either exist in the document, be `NOASSERTION`, or use the `DocumentRef-` prefix. +- *Atomicity*: When `ArgumentException` is thrown, the document is left in its original state — + no relationship is added or removed. -Key design decisions: +#### Design -- Static class with no instance state to simplify usage -- Deduplication logic prevents malformed documents with duplicate relationship entries +`SpdxRelationships` is a static class with no instance state: -## Dependencies +The batch `Add` overload executes in three phases to preserve atomicity: -The Transform subsystem depends on: +1. **Pre-validate**: Materialize the incoming enumerable into an array and call the internal + `ValidateRelationship` helper on every relationship. If any validation fails an + `ArgumentException` is thrown immediately and the document is left unchanged. +2. **Replace** (when `replace` is `true`): Remove all existing relationships whose source and + target element IDs match any incoming relationship, using `SpdxRelationship.SameElements` + (type-agnostic comparison). +3. **Add**: Call the internal `AddValidated` helper for each incoming relationship. `AddValidated` + searches for an existing match using `SpdxRelationship.Same` (type-inclusive); if found it + calls `Enhance`, otherwise it appends a `DeepCopy`. -- `SpdxDocument` and `SpdxRelationship` data model units +The single `Add` overload delegates directly: it calls `ValidateRelationship` then `AddValidated`. diff --git a/docs/design/title.txt b/docs/design/title.txt index ce459d6..6073a50 100644 --- a/docs/design/title.txt +++ b/docs/design/title.txt @@ -1,15 +1,14 @@ --- -title: SpdxModel Software Design -subtitle: Software Design Document for the SpdxModel Library -author: DEMA Consulting -description: Software design document for the SpdxModel C# library for reading, writing, and manipulating SPDX SBOM documents +title: "SpdxModel Software Design Document" +subtitle: "SPDX document model for .NET" +author: "DEMA Consulting" +description: "Software Design Document for SpdxModel" lang: en-US keywords: - - SpdxModel - Design - Software Design Document - C# - .NET - SPDX - SBOM ---- +--- \ No newline at end of file diff --git a/docs/reqstream/ots/buildmark.yaml b/docs/reqstream/ots/buildmark.yaml index d97a927..d44cbfe 100644 --- a/docs/reqstream/ots/buildmark.yaml +++ b/docs/reqstream/ots/buildmark.yaml @@ -1,7 +1,5 @@ --- -# BuildMark OTS Software Requirements -# -# Requirements for the BuildMark build documentation tool functionality. +# BuildMark OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/fileassert.yaml b/docs/reqstream/ots/fileassert.yaml index 6bf0b37..251bf29 100644 --- a/docs/reqstream/ots/fileassert.yaml +++ b/docs/reqstream/ots/fileassert.yaml @@ -1,7 +1,5 @@ --- -# FileAssert OTS Software Requirements -# -# Requirements for the FileAssert document assertion tool functionality. +# FileAssert OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/pandoc.yaml b/docs/reqstream/ots/pandoc.yaml index f00d443..712e121 100644 --- a/docs/reqstream/ots/pandoc.yaml +++ b/docs/reqstream/ots/pandoc.yaml @@ -1,7 +1,5 @@ --- -# Pandoc OTS Software Requirements -# -# Requirements for the Pandoc document conversion tool functionality. +# Pandoc OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/reqstream.yaml b/docs/reqstream/ots/reqstream.yaml index a2bcd3c..366778d 100644 --- a/docs/reqstream/ots/reqstream.yaml +++ b/docs/reqstream/ots/reqstream.yaml @@ -1,7 +1,5 @@ --- -# ReqStream OTS Software Requirements -# -# Requirements for the ReqStream requirements traceability tool functionality. +# ReqStream OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/reviewmark.yaml b/docs/reqstream/ots/reviewmark.yaml index b179786..941a5ed 100644 --- a/docs/reqstream/ots/reviewmark.yaml +++ b/docs/reqstream/ots/reviewmark.yaml @@ -1,7 +1,5 @@ --- -# ReviewMark OTS Software Requirements -# -# Requirements for the ReviewMark file review tool functionality. +# ReviewMark OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/sarifmark.yaml b/docs/reqstream/ots/sarifmark.yaml index 81f7ecb..354bb09 100644 --- a/docs/reqstream/ots/sarifmark.yaml +++ b/docs/reqstream/ots/sarifmark.yaml @@ -1,7 +1,5 @@ --- -# SarifMark OTS Software Requirements -# -# Requirements for the SarifMark SARIF report processing tool functionality. +# SarifMark OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/sonarmark.yaml b/docs/reqstream/ots/sonarmark.yaml index 4df432a..6a2e711 100644 --- a/docs/reqstream/ots/sonarmark.yaml +++ b/docs/reqstream/ots/sonarmark.yaml @@ -1,7 +1,5 @@ --- -# SonarMark OTS Software Requirements -# -# Requirements for the SonarMark quality reporting tool functionality. +# SonarMark OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/versionmark.yaml b/docs/reqstream/ots/versionmark.yaml index bca0c76..754492e 100644 --- a/docs/reqstream/ots/versionmark.yaml +++ b/docs/reqstream/ots/versionmark.yaml @@ -1,7 +1,5 @@ --- -# VersionMark OTS Software Requirements -# -# Requirements for the VersionMark version tracking tool functionality. +# VersionMark OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/weasyprint.yaml b/docs/reqstream/ots/weasyprint.yaml index f9aae37..f17fd3c 100644 --- a/docs/reqstream/ots/weasyprint.yaml +++ b/docs/reqstream/ots/weasyprint.yaml @@ -1,7 +1,5 @@ --- -# WeasyPrint OTS Software Requirements -# -# Requirements for the WeasyPrint PDF generation tool functionality. +# WeasyPrint OTS requirements sections: - title: OTS Software Requirements diff --git a/docs/reqstream/ots/mstest.yaml b/docs/reqstream/ots/xunit.yaml similarity index 53% rename from docs/reqstream/ots/mstest.yaml rename to docs/reqstream/ots/xunit.yaml index d3ed5a2..ec30389 100644 --- a/docs/reqstream/ots/mstest.yaml +++ b/docs/reqstream/ots/xunit.yaml @@ -1,22 +1,20 @@ --- -# MSTest OTS Software Requirements -# -# Requirements for the MSTest testing framework functionality. +# xUnit v3 OTS requirements sections: - title: OTS Software Requirements sections: - - title: MSTest Requirements + - title: xUnit v3 Requirements requirements: - - id: SpdxModel-OTS-MSTest - title: MSTest shall execute unit tests and report results. + - id: SpdxModel-OTS-xUnit + title: xUnit v3 shall execute unit tests and report results. justification: | - MSTest (MSTest.TestFramework and MSTest.TestAdapter) is the unit-testing framework used + xUnit v3 (xunit.v3 and xunit.runner.visualstudio) is the unit-testing framework used by the project. It discovers and runs all test methods and writes TRX result files that feed into coverage reporting and requirements traceability. Passing tests confirm the framework is functioning correctly. tags: [ots] tests: - - Spdx2JsonDeserializer_Deserialize_ValidSpdx22JsonReturnsExpectedDocument - - Spdx2JsonDeserializer_Deserialize_ValidSpdx23JsonReturnsExpectedDocument - - Spdx2JsonSerializer_SerializeDocument_CorrectResults + - Spdx2JsonDeserializer_Deserialize_ValidSpdx22Json_ReturnsExpectedDocument + - Spdx2JsonDeserializer_Deserialize_ValidSpdx23Json_ReturnsExpectedDocument + - Spdx2JsonSerializer_SerializeDocument_ValidInput_CorrectResults diff --git a/docs/reqstream/spdx-model/io/io.yaml b/docs/reqstream/spdx-model/io/io.yaml index b668ca0..ae192c9 100644 --- a/docs/reqstream/spdx-model/io/io.yaml +++ b/docs/reqstream/spdx-model/io/io.yaml @@ -1,29 +1,41 @@ --- -# SpdxModel IO Subsystem Requirements -# -# This is the subsystem-level requirements file for the IO subsystem. -# Unit requirements are in the sibling unit files: -# spdx-2-json-deserializer.yaml -# spdx-2-json-serializer.yaml +# IO subsystem requirements sections: - - title: IO Subsystem Requirements - requirements: - - id: SpdxModel-IO-Serialization - title: The library shall support JSON serialization and deserialization of SPDX 2.x documents. - tags: - - io - - serialization - justification: | - JSON is the primary interchange format for SPDX documents. Supporting serialization and - deserialization in JSON ensures interoperability with other SPDX tools and systems that - produce and consume SPDX SBOMs. - children: - - SpdxModel-Serialization-Deserialize22Json - - SpdxModel-Serialization-Deserialize23Json - - SpdxModel-Serialization-DeserializeElements - - SpdxModel-Serialization-SerializeJson - - SpdxModel-Serialization-SerializeElements - tests: - - SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidDocument - - SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidDocument + - title: SpdxModel Requirements + sections: + - title: IO Requirements + requirements: + - id: SpdxModel-IO-Deserialization + title: The library shall support JSON deserialization of SPDX 2.x documents. + tags: + - io + - serialization + justification: | + JSON deserialization is the primary mechanism for reading SPDX documents produced by + other tools. Supporting both SPDX 2.2 and 2.3 formats ensures compatibility with the + broad ecosystem of SPDX tooling. + children: + - SpdxModel-IO-Spdx2JsonDeserializer-Deserialize22Json + - SpdxModel-IO-Spdx2JsonDeserializer-Deserialize23Json + - SpdxModel-IO-Spdx2JsonDeserializer-DeserializeElements + - SpdxModel-IO-Constants + tests: + - SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidDocument + - SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidDocument + + - id: SpdxModel-IO-Serialization + title: The library shall support JSON serialization of SPDX 2.x documents. + tags: + - io + - serialization + justification: | + JSON serialization enables applications to produce SPDX documents for distribution, + toolchain integration, and compliance reporting. Producing well-formed SPDX JSON ensures + interoperability with other tools in the SPDX ecosystem. + children: + - SpdxModel-IO-SerializeJson + - SpdxModel-IO-SerializeElements + tests: + - SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidDocument + - SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidDocument diff --git a/docs/reqstream/spdx-model/io/spdx-2-json-deserializer.yaml b/docs/reqstream/spdx-model/io/spdx-2-json-deserializer.yaml index c4f7e2f..f8f33a3 100644 --- a/docs/reqstream/spdx-model/io/spdx-2-json-deserializer.yaml +++ b/docs/reqstream/spdx-model/io/spdx-2-json-deserializer.yaml @@ -1,63 +1,64 @@ --- -# SpdxModel Spdx2JsonDeserializer Unit Requirements -# -# This file defines the requirements for the Spdx2JsonDeserializer unit -# in the IO subsystem of the SpdxModel library. +# Spdx2JsonDeserializer unit requirements sections: - - title: Spdx2JsonDeserializer Requirements - requirements: - - id: SpdxModel-Serialization-Deserialize22Json - title: The library shall support deserializing SPDX 2.2 JSON documents. - tags: - - serialization - justification: | - Deserializing SPDX 2.2 JSON documents is essential for backward compatibility and - interoperability with systems using the SPDX 2.2 specification. This ensures that the - library can read and process existing SPDX 2.2 documents from various sources. - tests: - - Spdx2JsonDeserializer_Deserialize_ValidSpdx22JsonReturnsExpectedDocument + - title: SpdxModel Requirements + sections: + - title: IO Requirements + sections: + - title: Spdx2JsonDeserializer Requirements + requirements: + - id: SpdxModel-IO-Spdx2JsonDeserializer-Deserialize22Json + title: The library shall support deserializing SPDX 2.2 JSON documents. + tags: + - serialization + justification: | + Deserializing SPDX 2.2 JSON documents is essential for backward compatibility and + interoperability with systems using the SPDX 2.2 specification. This ensures that the + library can read and process existing SPDX 2.2 documents from various sources. + tests: + - Spdx2JsonDeserializer_Deserialize_ValidSpdx22Json_ReturnsExpectedDocument - - id: SpdxModel-Serialization-Deserialize23Json - title: The library shall support deserializing SPDX 2.3 JSON documents. - tags: - - serialization - justification: | - Support for SPDX 2.3 JSON documents ensures the library remains current with the latest - SPDX specification. This allows users to leverage new features and improvements introduced - in SPDX 2.3 while maintaining compatibility with modern SBOM tools. - tests: - - Spdx2JsonDeserializer_Deserialize_ValidSpdx23JsonReturnsExpectedDocument + - id: SpdxModel-IO-Spdx2JsonDeserializer-Deserialize23Json + title: The library shall support deserializing SPDX 2.3 JSON documents. + tags: + - serialization + justification: | + Support for SPDX 2.3 JSON documents ensures the library remains current with the latest + SPDX specification. This allows users to leverage new features and improvements introduced + in SPDX 2.3 while maintaining compatibility with modern SBOM tools. + tests: + - Spdx2JsonDeserializer_Deserialize_ValidSpdx23Json_ReturnsExpectedDocument - - id: SpdxModel-Serialization-DeserializeElements - title: The library shall correctly deserialize all SPDX 2.x element types from JSON. - tags: - - serialization - justification: | - Each SPDX element type (annotation, checksum, package, file, snippet, relationship, etc.) - has distinct JSON representation and field mappings. Verifying each element type is - deserialized correctly ensures the library faithfully reconstructs every artifact present - in a source SPDX JSON document. - tests: - - Spdx2JsonDeserializer_DeserializeDocument_CorrectResults - - Spdx2JsonDeserializer_DeserializeAnnotation_CorrectResults - - Spdx2JsonDeserializer_DeserializeAnnotations_CorrectResults - - Spdx2JsonDeserializer_DeserializeChecksum_CorrectResults - - Spdx2JsonDeserializer_DeserializeChecksums_CorrectResults - - Spdx2JsonDeserializer_DeserializeCreationInformation_CorrectResults - - Spdx2JsonDeserializer_DeserializeExternalDocumentReference_CorrectResults - - Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_CorrectResults - - Spdx2JsonDeserializer_DeserializeExternalReference_CorrectResults - - Spdx2JsonDeserializer_DeserializeExternalReferences_CorrectResults - - Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_CorrectResults - - Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_CorrectResults - - Spdx2JsonDeserializer_DeserializeFile_CorrectResults - - Spdx2JsonDeserializer_DeserializeFiles_CorrectResults - - Spdx2JsonDeserializer_DeserializePackage_CorrectResults - - Spdx2JsonDeserializer_DeserializePackages_CorrectResults - - Spdx2JsonDeserializer_DeserializePackageVerificationCode_CorrectResults - - Spdx2JsonDeserializer_DeserializeRelationship_CorrectResults - - Spdx2JsonDeserializer_DeserializeRelationships_CorrectResults - - Spdx2JsonDeserializer_DeserializeSnippet_CorrectResults - - Spdx2JsonDeserializer_DeserializeSnippet_WithoutLineRanges_DefaultsToZero - - Spdx2JsonDeserializer_DeserializeSnippets_CorrectResults + - id: SpdxModel-IO-Spdx2JsonDeserializer-DeserializeElements + title: The library shall correctly deserialize all SPDX 2.x element types from JSON. + tags: + - serialization + justification: | + Each SPDX element type (annotation, checksum, package, file, snippet, relationship, etc.) + has distinct JSON representation and field mappings. Verifying each element type is + deserialized correctly ensures the library faithfully reconstructs every artifact present + in a source SPDX JSON document. + tests: + - Spdx2JsonDeserializer_DeserializeDocument_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeAnnotation_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeAnnotations_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeChecksum_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeChecksums_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeCreationInformation_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeExternalDocumentReference_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeExternalReference_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeExternalReferences_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeFile_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeFiles_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializePackage_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializePackages_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializePackageVerificationCode_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeRelationship_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeRelationships_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeSnippet_ValidInput_CorrectResults + - Spdx2JsonDeserializer_DeserializeSnippet_WithoutLineRanges_DefaultsToZero + - Spdx2JsonDeserializer_DeserializeSnippets_ValidInput_CorrectResults diff --git a/docs/reqstream/spdx-model/io/spdx-2-json-serializer.yaml b/docs/reqstream/spdx-model/io/spdx-2-json-serializer.yaml index a99ef74..3ca51fc 100644 --- a/docs/reqstream/spdx-model/io/spdx-2-json-serializer.yaml +++ b/docs/reqstream/spdx-model/io/spdx-2-json-serializer.yaml @@ -1,51 +1,56 @@ --- -# SpdxModel Spdx2JsonSerializer Unit Requirements -# -# This file defines the requirements for the Spdx2JsonSerializer unit -# in the IO subsystem of the SpdxModel library. +# Spdx2JsonSerializer unit requirements sections: - - title: Spdx2JsonSerializer Requirements - requirements: - - id: SpdxModel-Serialization-SerializeJson - title: The library shall support serializing SPDX documents to JSON format. - tags: - - serialization - justification: | - Serialization capability is fundamental for creating and exporting SPDX documents in JSON - format. This enables users to generate SBOMs programmatically and share them with other - systems and tools in the SPDX ecosystem. - tests: - - Spdx2JsonSerializer_SerializeDocument_CorrectResults - - Spdx2JsonSerializer_Serialize_CorrectResults + - title: SpdxModel Requirements + sections: + - title: IO Requirements + sections: + - title: Spdx2JsonSerializer Requirements + requirements: + - id: SpdxModel-IO-SerializeJson + title: The library shall support serializing SPDX documents to JSON format. + tags: + - serialization + justification: | + Serialization capability is fundamental for creating and exporting SPDX documents in JSON + format. This enables users to generate SBOMs programmatically and share them with other + systems and tools in the SPDX ecosystem. + tests: + - Spdx2JsonSerializer_SerializeDocument_ValidInput_CorrectResults + - Spdx2JsonSerializer_Serialize_ValidInput_CorrectResults - - id: SpdxModel-Serialization-SerializeElements - title: The library shall correctly serialize all SPDX 2.x element types to JSON. - tags: - - serialization - justification: | - Each SPDX element type (annotation, checksum, package, file, snippet, relationship, etc.) - has distinct JSON representation and field mappings. Verifying each element type is - serialized correctly ensures the library faithfully produces all artifacts when writing - an SPDX JSON document. - tests: - - Spdx2JsonSerializer_SerializeAnnotation_CorrectResults - - Spdx2JsonSerializer_SerializeAnnotations_CorrectResults - - Spdx2JsonSerializer_SerializeChecksum_CorrectResults - - Spdx2JsonSerializer_SerializeChecksums_CorrectResults - - Spdx2JsonSerializer_SerializeCreationInformation_CorrectResults - - Spdx2JsonSerializer_SerializeExternalDocumentReference_CorrectResults - - Spdx2JsonSerializer_SerializeExternalDocumentReferences_CorrectResults - - Spdx2JsonSerializer_SerializeExternalReference_CorrectResults - - Spdx2JsonSerializer_SerializeExternalReferences_CorrectResults - - Spdx2JsonSerializer_SerializeExtractedLicensingInfo_CorrectResults - - Spdx2JsonSerializer_SerializeExtractedLicensingInfos_CorrectResults - - Spdx2JsonSerializer_SerializeFile_CorrectResults - - Spdx2JsonSerializer_SerializeFiles_CorrectResults - - Spdx2JsonSerializer_SerializePackage_CorrectResults - - Spdx2JsonSerializer_SerializePackages_CorrectResults - - Spdx2JsonSerializer_SerializePackageVerificationCode_CorrectResults - - Spdx2JsonSerializer_SerializeRelationship_CorrectResults - - Spdx2JsonSerializer_SerializeRelationships_CorrectResults - - Spdx2JsonSerializer_SerializeSnippet_CorrectResults - - Spdx2JsonSerializer_SerializeSnippets_CorrectResults + - id: SpdxModel-IO-SerializeElements + title: The library shall correctly serialize all SPDX 2.x element types to JSON. + tags: + - serialization + justification: | + Each SPDX element type (annotation, checksum, package, file, snippet, relationship, etc.) + has distinct JSON representation and field mappings. Verifying each element type is + serialized correctly ensures the library faithfully produces all artifacts when writing + an SPDX JSON document. + tests: + - Spdx2JsonSerializer_SerializeAnnotation_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeAnnotations_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeAnnotation_NoId_OmitsSpdxId + - Spdx2JsonSerializer_SerializeChecksum_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeChecksums_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeCreationInformation_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeExternalDocumentReference_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeExternalDocumentReferences_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeExternalReference_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeExternalReferences_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeExtractedLicensingInfo_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeExtractedLicensingInfos_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeFile_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeFiles_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializePackage_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializePackages_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeVerificationCode_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeRelationship_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeRelationships_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeSnippet_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeSnippets_ValidInput_CorrectResults + - Spdx2JsonSerializer_SerializeSnippet_WithAnnotation_IncludesAnnotation + - Spdx2JsonSerializer_SerializeSnippet_NoLineRange_EmitsByteRangeOnly + - Spdx2JsonSerializer_SerializeSnippet_PartialLineRange_EmitsByteRangeOnly diff --git a/docs/reqstream/spdx-model/io/spdx-constants.yaml b/docs/reqstream/spdx-model/io/spdx-constants.yaml new file mode 100644 index 0000000..aa5db38 --- /dev/null +++ b/docs/reqstream/spdx-model/io/spdx-constants.yaml @@ -0,0 +1,24 @@ +--- +# SpdxConstants unit requirements + +sections: + - title: SpdxModel Requirements + sections: + - title: IO Requirements + sections: + - title: SpdxConstants Requirements + requirements: + - id: SpdxModel-IO-Constants + title: >- + The library shall produce and consume JSON output using the correct SPDX field + names as defined by the SPDX 2.x specification. + tags: + - io + justification: | + Centralizing SPDX JSON field name strings in constants avoids hard-coded literals + in the serializer and deserializer, reducing the risk of typos and making future + specification updates easier to manage. This requirement is verified indirectly + through the IO serialization and deserialization tests. + tests: + - SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidDocument + - SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidDocument diff --git a/docs/reqstream/spdx-model/platform-requirements.yaml b/docs/reqstream/spdx-model/platform-requirements.yaml index 1cb5b46..4fe5767 100644 --- a/docs/reqstream/spdx-model/platform-requirements.yaml +++ b/docs/reqstream/spdx-model/platform-requirements.yaml @@ -1,69 +1,91 @@ --- -# SpdxModel Platform Requirements -# -# This file defines the platform support requirements for the SpdxModel library. +# SpdxModel platform requirements sections: - - title: Platform Requirements - requirements: - - id: SpdxModel-Platform-MacOS - title: The library shall build and run on macOS platforms. - tags: - - platform - justification: | - DEMA Consulting libraries must support macOS for developers using Apple platforms. - tests: - # Tests link to "macos" to ensure results come from macOS platform - - "macos@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully" - - "macos@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds" + - title: SpdxModel Requirements + sections: + - title: Platform Support + requirements: + - id: SpdxModel-Platform-MacOS + title: The library shall build and run on macOS platforms. + tags: + - platform + justification: | + DEMA Consulting libraries must support macOS for developers using Apple platforms. + tests: + # Tests link to "macos" to ensure results come from macOS platform + - "macos@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully" + - "macos@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds" - - id: SpdxModel-Platform-Net8 - title: The library shall support .NET 8 runtime. - tags: - - platform - justification: | - .NET 8 is a long-term support (LTS) release and provides a stable foundation for - enterprise applications. Supporting .NET 8 ensures the library can be used in production - environments requiring long-term stability and support. - tests: - - net8.0@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully - - net8.0@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds + - id: SpdxModel-Platform-Windows + title: The library shall build and run on Windows platforms. + tags: + - platform + justification: | + DEMA Consulting libraries must support Windows as the primary development and + deployment platform for enterprise .NET applications. + tests: + - "windows@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully" + - "windows@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds" - - id: SpdxModel-Platform-Net9 - title: The library shall support .NET 9 runtime. - tags: - - platform - justification: | - .NET 9 is a standard-term support (STS) release providing newer features and performance - improvements. Supporting .NET 9 allows users to leverage the latest .NET capabilities while - the framework is current. - tests: - - net9.0@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully - - net9.0@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds + - id: SpdxModel-Platform-Linux + title: The library shall build and run on Linux platforms. + tags: + - platform + justification: | + DEMA Consulting libraries must support Linux for CI/CD pipelines and server + deployments in cloud-native environments. + tests: + - "ubuntu@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully" + - "ubuntu@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds" - - id: SpdxModel-Platform-Net10 - title: The library shall support .NET 10 runtime. - tags: - - platform - justification: | - .NET 10 represents the latest .NET platform release. Supporting .NET 10 ensures users - can adopt the most recent framework version and benefit from the latest performance, - security, and feature improvements. - tests: - - net10.0@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully - - net10.0@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds + - id: SpdxModel-Platform-Net8 + title: The library shall support .NET 8 runtime. + tags: + - platform + justification: | + .NET 8 is a long-term support (LTS) release and provides a stable foundation for + enterprise applications. Supporting .NET 8 ensures the library can be used in production + environments requiring long-term stability and support. + tests: + - net8.0@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully + - net8.0@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds - - id: SpdxModel-Platform-NetStd20 - title: The library shall support the .NET Standard 2.0 target framework. - tags: - - platform - justification: | - .NET Standard 2.0 is a widely-supported target framework that enables the library to - be used in MSBuild extensions and other tooling that requires .NET Standard compatibility. - Supporting this target framework ensures the library can be integrated into a broader - range of .NET projects, including those targeting .NET Framework and older .NET Core versions. - The net481 test target on Windows provides direct runtime evidence of .NET Standard 2.0 - compatibility, as .NET Framework 4.8.1 fully implements the .NET Standard 2.0 API surface. - tests: - - "net481@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully" - - "net481@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds" + - id: SpdxModel-Platform-Net9 + title: The library shall support .NET 9 runtime. + tags: + - platform + justification: | + .NET 9 is a standard-term support (STS) release providing newer features and performance + improvements. Supporting .NET 9 allows users to leverage the latest .NET capabilities while + the framework is current. + tests: + - net9.0@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully + - net9.0@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds + + - id: SpdxModel-Platform-Net10 + title: The library shall support .NET 10 runtime. + tags: + - platform + justification: | + .NET 10 represents the latest .NET platform release. Supporting .NET 10 ensures users + can adopt the most recent framework version and benefit from the latest performance, + security, and feature improvements. + tests: + - net10.0@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully + - net10.0@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds + + - id: SpdxModel-Platform-NetStd20 + title: The library shall support the .NET Standard 2.0 target framework. + tags: + - platform + justification: | + .NET Standard 2.0 is a widely-supported target framework that enables the library to + be used in MSBuild extensions and other tooling that requires .NET Standard compatibility. + Supporting this target framework ensures the library can be integrated into a broader + range of .NET projects, including those targeting .NET Framework and older .NET Core versions. + The net481 test target on Windows provides direct runtime evidence of .NET Standard 2.0 + compatibility, as .NET Framework 4.8.1 fully implements the .NET Standard 2.0 API surface. + tests: + - "net481@SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully" + - "net481@SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds" diff --git a/docs/reqstream/spdx-model/spdx-annotation.yaml b/docs/reqstream/spdx-model/spdx-annotation.yaml index 55666c2..638cae1 100644 --- a/docs/reqstream/spdx-model/spdx-annotation.yaml +++ b/docs/reqstream/spdx-model/spdx-annotation.yaml @@ -1,28 +1,29 @@ --- -# SpdxAnnotation Unit Requirements -# -# This file defines requirements for the SpdxAnnotation unit. +# SpdxAnnotation unit requirements sections: - - title: SpdxAnnotation Unit Requirements - requirements: - - id: SpdxModel-Data-Annotations - title: The library shall support SPDX annotations. - tags: - - data-model - justification: | - Annotations allow adding review and assessment information to SPDX elements. This supports - compliance workflows where reviewers need to document their findings and decisions about - software components. - tests: - - SpdxAnnotation_SameComparer_ComparesCorrectly - - SpdxAnnotation_DeepCopy_CreatesEqualButDistinctInstance - - SpdxAnnotation_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxAnnotation_Validate_InvalidAnnotator - - SpdxAnnotation_Validate_InvalidDate - - SpdxAnnotation_Validate_InvalidType - - SpdxAnnotation_Validate_InvalidComment - - SpdxAnnotationTypeExtensions_FromText_Valid - - SpdxAnnotationTypeExtensions_FromText_Invalid - - SpdxAnnotationTypeExtensions_ToText_Valid - - SpdxAnnotationTypeExtensions_ToText_Invalid + - title: SpdxModel Requirements + sections: + - title: SpdxAnnotation Requirements + requirements: + - id: SpdxModel-Data-Annotations + title: The library shall support SPDX annotations. + tags: + - data-model + justification: | + Annotations allow adding review and assessment information to SPDX elements. This supports + compliance workflows where reviewers need to document their findings and decisions about + software components. + tests: + - SpdxAnnotation_SameComparer_ComparesCorrectly + - SpdxAnnotation_DeepCopy_CreatesEqualButDistinctInstance + - SpdxAnnotation_Enhance_AddsOrUpdatesInformationCorrectly + - SpdxAnnotation_Validate_InvalidAnnotator + - SpdxAnnotation_Validate_InvalidDate + - SpdxAnnotation_Validate_InvalidType + - SpdxAnnotation_Validate_InvalidComment + - SpdxAnnotationTypeExtensions_FromText_Valid + - SpdxAnnotationTypeExtensions_FromText_Invalid + - SpdxAnnotationTypeExtensions_ToText_Valid + - SpdxAnnotationTypeExtensions_ToText_Invalid + - SpdxAnnotationTypeExtensions_ToText_Missing diff --git a/docs/reqstream/spdx-model/spdx-checksum.yaml b/docs/reqstream/spdx-model/spdx-checksum.yaml index 5739569..8cf059b 100644 --- a/docs/reqstream/spdx-model/spdx-checksum.yaml +++ b/docs/reqstream/spdx-model/spdx-checksum.yaml @@ -1,26 +1,72 @@ --- -# SpdxChecksum Unit Requirements -# -# This file defines requirements for the SpdxChecksum unit. +# SpdxChecksum unit requirements sections: - - title: SpdxChecksum Unit Requirements - requirements: - - id: SpdxModel-Data-Checksums - title: The library shall support SPDX checksums with multiple algorithms. - tags: - - data-model - justification: | - Checksums with multiple algorithms provide integrity verification for files and packages. - Supporting multiple algorithms ensures flexibility and compatibility with different security - requirements and organizational policies. - tests: - - SpdxChecksum_SameComparer_ComparesCorrectly - - SpdxChecksum_DeepCopy_CreatesEqualButDistinctInstance - - SpdxChecksum_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxChecksum_Validate_InvalidAlgorithm - - SpdxChecksum_Validate_InvalidValue - - SpdxChecksumAlgorithmExtensions_FromText_Valid - - SpdxChecksumAlgorithmExtensions_FromText_InvalidAlgorithm - - SpdxChecksumAlgorithmExtensions_ToText_Valid - - SpdxChecksumAlgorithmExtensions_ToText_InvalidAlgorithm + - title: SpdxModel Requirements + sections: + - title: SpdxChecksum Requirements + requirements: + - id: SpdxModel-Data-Checksum-Compare + title: The library shall support equality comparison of SPDX checksums by algorithm and value. + tags: + - data-model + justification: | + Checksums must be compared by algorithm and value to support array merging and + deduplication across SPDX documents. + tests: + - SpdxChecksum_SameComparer_SameOrDifferentValues_ReturnsCorrectEquality + - SpdxChecksum_SameComparer_NullFirstArgument_ReturnsFalse + - SpdxChecksum_SameComparer_NullSecondArgument_ReturnsFalse + - SpdxChecksum_SameComparer_BothArgumentsNull_ReturnsTrue + - id: SpdxModel-Data-Checksum-DeepCopy + title: The library shall support creating independent deep copies of SPDX checksums. + tags: + - data-model + justification: | + Deep copies are required so that merging operations do not mutate the original + checksum instances. + tests: + - SpdxChecksum_DeepCopy_PopulatedChecksum_CreatesEqualButDistinctInstance + - id: SpdxModel-Data-Checksum-Enhance + title: The library shall support merging SPDX checksum arrays by algorithm and value identity. + tags: + - data-model + justification: | + Enhance enables combining checksum information from multiple SPDX documents into + a single deduplicated record. + tests: + - SpdxChecksum_Enhance_ExistingAndNewAlgorithms_AddsOrUpdatesInformation + - id: SpdxModel-Data-Checksum-Validate + title: The library shall validate SPDX checksum fields and report all issues found. + tags: + - data-model + - validation + justification: | + Validation ensures that checksum data is complete and consistent before + serialization or use by downstream consumers. + tests: + - SpdxChecksum_Validate_MissingAlgorithm_ReportsAlgorithmIssue + - SpdxChecksum_Validate_UnknownNumericAlgorithm_ReportsAlgorithmIssue + - SpdxChecksum_Validate_EmptyValue_ReportsValueIssue + - id: SpdxModel-Data-Checksum-FromText + title: The library shall convert SPDX algorithm text strings to the SpdxChecksumAlgorithm enumeration. + tags: + - data-model + justification: | + Deserialization from SPDX JSON and tag-value formats requires mapping algorithm + name strings to typed enum values. + tests: + - SpdxChecksumAlgorithmExtensions_FromText_KnownAlgorithmStrings_ReturnsCorrectEnumValues + - SpdxChecksumAlgorithmExtensions_FromText_UnknownAlgorithmString_ThrowsInvalidOperationException + - SpdxChecksumAlgorithmExtensions_FromText_EmptyString_ReturnsMissing + - id: SpdxModel-Data-Checksum-ToText + title: The library shall convert SpdxChecksumAlgorithm enumeration values to SPDX algorithm text strings. + tags: + - data-model + justification: | + Serialization to SPDX JSON and tag-value formats requires mapping typed enum + values back to their canonical algorithm name strings. + tests: + - SpdxChecksumAlgorithmExtensions_ToText_KnownAlgorithmEnums_ReturnsCorrectStrings + - SpdxChecksumAlgorithmExtensions_ToText_OutOfRangeEnum_ThrowsInvalidOperationException + - SpdxChecksumAlgorithmExtensions_ToText_MissingAlgorithm_ThrowsInvalidOperationException diff --git a/docs/reqstream/spdx-model/spdx-creation-information.yaml b/docs/reqstream/spdx-model/spdx-creation-information.yaml index d463e00..f19946c 100644 --- a/docs/reqstream/spdx-model/spdx-creation-information.yaml +++ b/docs/reqstream/spdx-model/spdx-creation-information.yaml @@ -1,23 +1,26 @@ --- -# SpdxCreationInformation Unit Requirements -# -# This file defines requirements for the SpdxCreationInformation unit. +# SpdxCreationInformation unit requirements sections: - - title: SpdxCreationInformation Unit Requirements - requirements: - - id: SpdxModel-Data-CreationInformation - title: The library shall support SPDX document creation information. - tags: - - data-model - justification: | - Creation information is a required element of SPDX documents that provides metadata about - who created the document and when. Supporting this element is essential for SPDX compliance - and traceability of document provenance. - tests: - - SpdxCreationInformation_DeepCopy_CreatesEqualButDistinctInstance - - SpdxCreationInformation_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxCreationInformation_Validate_MissingCreators - - SpdxCreationInformation_Validate_InvalidCreator - - SpdxCreationInformation_Validate_InvalidCreatedDate - - SpdxCreationInformation_Validate_InvalidVersion + - title: SpdxModel Requirements + sections: + - title: SpdxCreationInformation Requirements + requirements: + - id: SpdxModel-Data-CreationInformation + title: The library shall support SPDX document creation information. + tags: + - data-model + justification: | + Creation information is a required element of SPDX documents that provides metadata about + who created the document and when. Supporting this element is essential for SPDX compliance + and traceability of document provenance. + tests: + - SpdxCreationInformation_DeepCopy_WithAllFieldsPopulated_CreatesEqualButDistinctInstance + - SpdxCreationInformation_Enhance_WithMissingFieldsInBase_AddsOrUpdatesInformationCorrectly + - SpdxCreationInformation_Enhance_DuplicateCreators_DeduplicatesCreators + - SpdxCreationInformation_Validate_ValidInformation_NoIssues + - SpdxCreationInformation_Validate_MissingCreators_ReportsIssue + - SpdxCreationInformation_Validate_InvalidCreator_ReportsIssue + - SpdxCreationInformation_Validate_InvalidCreatedDate_ReportsIssue + - SpdxCreationInformation_Validate_InvalidVersion_ReportsIssue + - SpdxCreationInformation_Validate_EmptyCreatedField_NoDateIssue diff --git a/docs/reqstream/spdx-model/spdx-document.yaml b/docs/reqstream/spdx-model/spdx-document.yaml index e4984ba..4ce88bd 100644 --- a/docs/reqstream/spdx-model/spdx-document.yaml +++ b/docs/reqstream/spdx-model/spdx-document.yaml @@ -1,65 +1,78 @@ --- -# SpdxDocument Unit Requirements -# -# This file defines requirements for the SpdxDocument unit. +# SpdxDocument unit requirements sections: - - title: SpdxDocument Unit Requirements - requirements: - - id: SpdxModel-Data-Document - title: The library shall support SPDX documents as the root container of the SPDX object model. - tags: - - data-model - justification: | - The SPDX document is the top-level artifact in an SPDX SBOM. It aggregates all - packages, files, snippets, relationships, and annotations, and is required for - expressing any SPDX-compliant bill of materials. - tests: - - SpdxDocument_SameComparer_ComparesCorrectly - - SpdxDocument_DeepCopy_CreatesEqualButDistinctInstance + - title: SpdxModel Requirements + sections: + - title: SpdxDocument Requirements + requirements: + - id: SpdxModel-Data-Document + title: The library shall support SPDX documents as the root container of the SPDX object model. + tags: + - data-model + justification: | + The SPDX document is the top-level artifact in an SPDX SBOM. It aggregates all + packages, files, snippets, relationships, and annotations, and is required for + expressing any SPDX-compliant bill of materials. + tests: + - SpdxDocument_Same_DocumentsWithMatchingRootPackages_AreEqual + - SpdxDocument_DeepCopy_WithPopulatedDocument_CreatesEqualButDistinctInstance - - id: SpdxModel-Data-Document-Validate - title: The library shall validate SPDX document fields and report all violations. - tags: - - data-model - - validation - justification: | - Validation ensures that SPDX documents conform to the specification before they are - consumed or distributed. Reporting all violations rather than failing on the first - allows tooling to produce complete diagnostic output in a single pass. - tests: - - SpdxDocument_Validate_NoIssues - - SpdxDocument_Validate_InvalidId - - SpdxDocument_Validate_InvalidName - - SpdxDocument_Validate_InvalidVersion - - SpdxDocument_Validate_InvalidDataLicense - - SpdxDocument_Validate_InvalidNameSpace - - SpdxDocument_Validate_DuplicatePackageIds - - SpdxDocument_Validate_InvalidRelationship - - SpdxDocument_Validate_InvalidAnnotation + - id: SpdxModel-Data-Document-Validate + title: The library shall validate SPDX document fields and report all violations. + tags: + - data-model + - validation + justification: | + Validation ensures that SPDX documents conform to the specification before they are + consumed or distributed. Reporting all violations rather than failing on the first + allows tooling to produce complete diagnostic output in a single pass. + tests: + - SpdxDocument_Validate_ValidDocument_ReportsNoIssues + - SpdxDocument_Validate_InvalidId_ReportsIssue + - SpdxDocument_Validate_InvalidName_ReportsIssue + - SpdxDocument_Validate_InvalidVersion_ReportsIssue + - SpdxDocument_Validate_InvalidDataLicense_ReportsIssue + - SpdxDocument_Validate_InvalidNameSpace_ReportsIssue + - SpdxDocument_Validate_DuplicatePackageIds_ReportsIssue + - SpdxDocument_Validate_InvalidRelationship_ReportsIssue + - SpdxDocument_Validate_InvalidAnnotation_ReportsIssue - - id: SpdxModel-Data-Document-Ntia - title: The library shall validate SPDX documents for NTIA minimum SBOM element compliance. - tags: - - data-model - - validation - - ntia - justification: | - The NTIA Minimum Elements for a Software Bill of Materials defines a baseline set of - data fields that must be present in a conforming SBOM. Supporting this check allows - downstream tooling to assess NTIA compliance without reimplementing the rules. - tests: - - SpdxDocument_Validate_NtiaIssues + - id: SpdxModel-Data-Document-Ntia + title: The library shall validate SPDX documents for NTIA minimum SBOM element compliance. + tags: + - data-model + - validation + - ntia + justification: | + The NTIA Minimum Elements for a Software Bill of Materials defines a baseline set of + data fields that must be present in a conforming SBOM. Supporting this check allows + downstream tooling to assess NTIA compliance without reimplementing the rules. + tests: + - SpdxDocument_Validate_NtiaMinimumElements_ReportsIssues - - id: SpdxModel-Data-Document-Query - title: The library shall support querying SPDX document elements by ID and by root-package relationship. - tags: - - data-model - justification: | - Consumers of SPDX documents frequently need to locate specific elements or determine - which packages are directly described by the document. Providing these query helpers - reduces boilerplate in downstream tools and ensures consistent traversal semantics. - tests: - - SpdxDocument_GetRootPackages_CorrectPackages - - SpdxDocument_GetAllElements_Correct - - SpdxDocument_GetElement_Correct + - id: SpdxModel-Data-Document-ElementQuery + title: The library shall support querying SPDX document elements by ID. + tags: + - data-model + justification: | + Consumers of SPDX documents frequently need to locate specific elements by their + SPDX identifier. Providing a GetElement helper reduces boilerplate in downstream + tools and ensures consistent traversal semantics. + tests: + - SpdxDocument_GetAllElements_WithMixedElements_ReturnsAllNonRelationshipElements + - SpdxDocument_GetElement_Document_ReturnsDocumentElement + - SpdxDocument_GetElement_File_ReturnsFileElement + - SpdxDocument_GetElement_Package_ReturnsPackageElement + - SpdxDocument_GetElement_Snippet_ReturnsSnippetElement + + - id: SpdxModel-Data-Document-RootPackageQuery + title: The library shall support querying SPDX documents for root packages via relationship traversal. + tags: + - data-model + justification: | + Consumers of SPDX documents frequently need to determine which packages are directly + described by the document. Providing a GetRootPackages helper reduces boilerplate in + downstream tools and ensures consistent traversal semantics. + tests: + - SpdxDocument_GetRootPackages_WithDescribesAndRelationships_ReturnsCorrectPackages diff --git a/docs/reqstream/spdx-model/spdx-element.yaml b/docs/reqstream/spdx-model/spdx-element.yaml index 31c8211..7cbfc1f 100644 --- a/docs/reqstream/spdx-model/spdx-element.yaml +++ b/docs/reqstream/spdx-model/spdx-element.yaml @@ -1,20 +1,19 @@ --- -# SpdxElement Unit Requirements -# -# This file defines requirements for the SpdxElement unit. +# SpdxElement unit requirements sections: - - title: SpdxElement Unit Requirements - requirements: - - id: SpdxModel-Data-Element - title: Every SPDX element shall carry a unique identifier in the form SPDXRef-. - tags: - - data-model - justification: | - Every SPDX element (document, package, file, snippet) must carry a unique SPDX - identifier in the form SPDXRef-. Consistent identity handling across the - entire object model enables element lookup and traversal. - tests: - - SpdxDocument_Validate_InvalidId - - SpdxDocument_GetAllElements_Correct - - SpdxDocument_GetElement_Correct + - title: SpdxModel Requirements + sections: + - title: SpdxElement Requirements + requirements: + - id: SpdxModel-Data-Element + title: Every SPDX element shall carry a unique identifier in the form SPDXRef-. + tags: + - data-model + justification: | + Every SPDX element (document, package, file, snippet) must carry a unique SPDX + identifier in the form SPDXRef-. Consistent identity handling across the + entire object model enables element lookup and traversal. + tests: + - SpdxElement_Id_ValidFormat_PassesValidation + - SpdxElement_Id_InvalidFormat_ReportsValidationIssue diff --git a/docs/reqstream/spdx-model/spdx-external-document-reference.yaml b/docs/reqstream/spdx-model/spdx-external-document-reference.yaml index cf03d62..3046174 100644 --- a/docs/reqstream/spdx-model/spdx-external-document-reference.yaml +++ b/docs/reqstream/spdx-model/spdx-external-document-reference.yaml @@ -1,22 +1,23 @@ --- -# SpdxExternalDocumentReference Unit Requirements -# -# This file defines requirements for the SpdxExternalDocumentReference unit. +# SpdxExternalDocumentReference unit requirements sections: - - title: SpdxExternalDocumentReference Unit Requirements - requirements: - - id: SpdxModel-Data-ExternalDocumentReferences - title: The library shall support SPDX external document references. - tags: - - data-model - justification: | - External document references allow SPDX documents to reference other SPDX documents, - enabling modular SBOM construction and linking between related software inventories. This - is essential for managing complex multi-component software systems. - tests: - - SpdxExternalDocumentReference_SameComparer_ComparesCorrectly - - SpdxExternalDocumentReference_DeepCopy_CreatesEqualButDistinctInstance - - SpdxExternalDocumentReference_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxExternalDocumentReference_Validate_MissingId - - SpdxExternalDocumentReference_Validate_MissingDocument + - title: SpdxModel Requirements + sections: + - title: SpdxExternalDocumentReference Requirements + requirements: + - id: SpdxModel-Data-ExternalDocumentReferences + title: The library shall support SPDX external document references. + tags: + - data-model + justification: | + External document references allow SPDX documents to reference other SPDX documents, + enabling modular SBOM construction and linking between related software inventories. This + is essential for managing complex multi-component software systems. + tests: + - SpdxExternalDocumentReference_SameComparer_SameDocument_ReturnsEqual + - SpdxExternalDocumentReference_DeepCopy_ValidInstance_ReturnsEqualButDistinctInstance + - SpdxExternalDocumentReference_Enhance_WithNewAndMatchingEntries_MergesAndAppendsCorrectly + - SpdxExternalDocumentReference_Validate_MissingId_ReportsIssue + - SpdxExternalDocumentReference_Validate_MissingDocument_ReportsIssue + - SpdxExternalDocumentReference_Validate_InvalidChecksum_ReportsIssue diff --git a/docs/reqstream/spdx-model/spdx-external-reference.yaml b/docs/reqstream/spdx-model/spdx-external-reference.yaml index 21cd1ed..27ee42b 100644 --- a/docs/reqstream/spdx-model/spdx-external-reference.yaml +++ b/docs/reqstream/spdx-model/spdx-external-reference.yaml @@ -1,28 +1,28 @@ --- -# SpdxExternalReference Unit Requirements -# -# This file defines requirements for the SpdxExternalReference unit. +# SpdxExternalReference unit requirements sections: - - title: SpdxExternalReference Unit Requirements - requirements: - - id: SpdxModel-Data-ExternalReferences - title: The library shall support SPDX external references. - tags: - - data-model - justification: | - External references enable linking SPDX elements to external resources like package - registries, vulnerability databases, and documentation. This enriches SBOMs with contextual - information from authoritative sources. - tests: - - SpdxExternalReference_SameComparer_ComparesCorrectly - - SpdxExternalReference_DeepCopy_CreatesEqualButDistinctInstance - - SpdxExternalReference_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxExternalReference_Validate_InvalidCategory - - SpdxExternalReference_Validate_InvalidType - - SpdxExternalReference_Validate_InvalidLocator - - SpdxReferenceCategoryExtensions_FromText_Valid - - SpdxReferenceCategoryExtensions_FromText_Invalid - - SpdxReferenceCategoryExtensions_ToText_Valid - - SpdxReferenceCategoryExtensions_ToText_InvalidCategory - - SpdxReferenceCategoryExtensions_ToText_MissingCategory + - title: SpdxModel Requirements + sections: + - title: SpdxExternalReference Requirements + requirements: + - id: SpdxModel-Data-ExternalReferences + title: The library shall support SPDX external references. + tags: + - data-model + justification: | + External references enable linking SPDX elements to external resources like package + registries, vulnerability databases, and documentation. This enriches SBOMs with + contextual information from authoritative sources. + tests: + - SpdxExternalReference_SameComparer_EqualAndUnequalInstances_ComparesCorrectly + - SpdxExternalReference_DeepCopy_WithAllFields_CreatesEqualButDistinctInstance + - SpdxExternalReference_Enhance_WithMatchingAndNewEntries_MergesCorrectly + - SpdxExternalReference_Validate_InvalidCategory_ReportsIssue + - SpdxExternalReference_Validate_InvalidType_ReportsIssue + - SpdxExternalReference_Validate_InvalidLocator_ReportsIssue + - SpdxReferenceCategoryExtensions_FromText_ValidInput_ParsesCorrectly + - SpdxReferenceCategoryExtensions_FromText_InvalidInput_ThrowsInvalidOperationException + - SpdxReferenceCategoryExtensions_ToText_ValidReference_FormatsCorrectly + - SpdxReferenceCategoryExtensions_ToText_InvalidCategory_ThrowsInvalidOperationException + - SpdxReferenceCategoryExtensions_ToText_MissingCategory_ThrowsInvalidOperationException diff --git a/docs/reqstream/spdx-model/spdx-extracted-licensing-info.yaml b/docs/reqstream/spdx-model/spdx-extracted-licensing-info.yaml index 7c2343e..41d2e5e 100644 --- a/docs/reqstream/spdx-model/spdx-extracted-licensing-info.yaml +++ b/docs/reqstream/spdx-model/spdx-extracted-licensing-info.yaml @@ -1,22 +1,23 @@ --- -# SpdxExtractedLicensingInfo Unit Requirements -# -# This file defines requirements for the SpdxExtractedLicensingInfo unit. +# SpdxExtractedLicensingInfo unit requirements sections: - - title: SpdxExtractedLicensingInfo Unit Requirements - requirements: - - id: SpdxModel-Data-ExtractedLicensingInformation - title: The library shall support SPDX extracted licensing information. - tags: - - data-model - justification: | - Extracted licensing information supports documenting non-standard licenses found in - software packages. This is critical for compliance when software contains licenses not - in the SPDX license list. - tests: - - SpdxExtractedLicensingInfo_SameComparer_ComparesCorrectly - - SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance - - SpdxExtractedLicensingInfo_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxExtractedLicensingInfo_Validate_InvalidLicenseId - - SpdxExtractedLicensingInfo_Validate_InvalidExtractedText + - title: SpdxModel Requirements + sections: + - title: SpdxExtractedLicensingInfo Requirements + requirements: + - id: SpdxModel-Data-ExtractedLicensingInformation + title: The library shall support SPDX extracted licensing information. + tags: + - data-model + justification: | + Extracted licensing information supports documenting non-standard licenses found in + software packages. This is critical for compliance when software contains licenses not + in the SPDX license list. + tests: + - SpdxExtractedLicensingInfo_SameComparer_ComparesCorrectly + - SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance + - SpdxExtractedLicensingInfo_Enhance_AddsOrUpdatesInformationCorrectly + - SpdxExtractedLicensingInfo_Validate_ValidInput_ReturnsNoIssues + - SpdxExtractedLicensingInfo_Validate_InvalidLicenseId_ReportsIssue + - SpdxExtractedLicensingInfo_Validate_InvalidExtractedText_ReportsIssue diff --git a/docs/reqstream/spdx-model/spdx-file.yaml b/docs/reqstream/spdx-model/spdx-file.yaml index f41e575..8594975 100644 --- a/docs/reqstream/spdx-model/spdx-file.yaml +++ b/docs/reqstream/spdx-model/spdx-file.yaml @@ -1,26 +1,28 @@ --- -# SpdxFile Unit Requirements -# -# This file defines requirements for the SpdxFile unit. +# SpdxFile unit requirements sections: - - title: SpdxFile Unit Requirements - requirements: - - id: SpdxModel-Data-Files - title: The library shall support SPDX files. - tags: - - data-model - justification: | - Files are essential components in SPDX documents for detailed SBOM creation. Supporting - file elements enables fine-grained tracking of individual source files, binaries, and - their associated licensing and copyright information. - tests: - - SpdxFile_SameComparer_ComparesCorrectly - - SpdxFile_DeepCopy_CreatesEqualButDistinctInstance - - SpdxFile_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxFile_Validate_ReportsInvalidFileId - - SpdxFile_Validate_Success - - SpdxFileTypeExtensions_FromText_Valid - - SpdxFileTypeExtensions_FromText_Invalid - - SpdxFileTypeExtensions_ToText_Valid - - SpdxFileTypeExtensions_ToText_Invalid + - title: SpdxModel Requirements + sections: + - title: SpdxFile Requirements + requirements: + - id: SpdxModel-Data-Files + title: The library shall support SPDX files. + tags: + - data-model + justification: | + Files are essential components in SPDX documents for detailed SBOM creation. Supporting + file elements enables fine-grained tracking of individual source files, binaries, and + their associated licensing and copyright information. + tests: + - SpdxFile_SameComparer_MatchingAndDistinctFiles_ComparesCorrectly + - SpdxFile_DeepCopy_FullyPopulatedFile_CreatesEqualButDistinctCopy + - SpdxFile_Enhance_MatchingAndNewFiles_MergesCorrectly + - SpdxFile_Validate_InvalidFileId_ReportsIssue + - SpdxFile_Validate_InvalidFileName_ReportsIssue + - SpdxFile_Validate_MissingSha1Checksum_ReportsIssue + - SpdxFile_Validate_ValidFile_ReportsNoIssues + - SpdxFileTypeExtensions_FromText_ValidInput_ParsesCorrectly + - SpdxFileTypeExtensions_FromText_InvalidInput_ThrowsException + - SpdxFileTypeExtensions_ToText_ValidEnum_FormatsCorrectly + - SpdxFileTypeExtensions_ToText_InvalidEnum_ThrowsException diff --git a/docs/reqstream/spdx-model/spdx-helpers.yaml b/docs/reqstream/spdx-model/spdx-helpers.yaml index 9cae8e5..43b3e89 100644 --- a/docs/reqstream/spdx-model/spdx-helpers.yaml +++ b/docs/reqstream/spdx-model/spdx-helpers.yaml @@ -1,35 +1,43 @@ --- -# SpdxHelpers Unit Requirements -# -# This file defines requirements for the SpdxHelpers unit. +# SpdxHelpers unit requirements sections: - - title: SpdxHelpers Unit Requirements - requirements: - - id: SpdxModel-Data-Helpers-DateTime - title: The library shall validate SPDX date-time strings against the ISO 8601 UTC format. - tags: - - data-model - - validation - justification: | - SPDX requires all date-time fields to conform to ISO 8601 UTC format - (YYYY-MM-DDThh:mm:ssZ). Consistent validation of this format across all - date-time fields ensures compliant SPDX documents are produced. - tests: - - SpdxCreationInformation_Validate_InvalidCreatedDate + - title: SpdxModel Requirements + sections: + - title: SpdxHelpers Requirements + requirements: + - id: SpdxModel-Data-Helpers-DateTime + title: The library shall validate SPDX date-time strings against the ISO 8601 UTC format. + tags: + - data-model + - validation + justification: | + SPDX requires all date-time fields to conform to ISO 8601 UTC format + (YYYY-MM-DDThh:mm:ssZ). Consistent validation of this format across all + date-time fields ensures compliant SPDX documents are produced. + Absent or empty date-time values (null, empty string) shall be treated as + not-set and shall be considered valid. + tests: + - SpdxCreationInformation_Validate_InvalidCreatedDate_ReportsIssue + - SpdxHelpers_IsValidSpdxDateTime_NullInput_ReturnsTrue + - SpdxHelpers_IsValidSpdxDateTime_EmptyInput_ReturnsTrue + - SpdxHelpers_IsValidSpdxDateTime_ValidFormat_ReturnsTrue + - SpdxHelpers_IsValidSpdxDateTime_InvalidFormat_ReturnsFalse - - id: SpdxModel-Data-Helpers-EnhanceString - title: >- - When merging optional SPDX fields, the library shall prefer a concrete - value over NOASSERTION or absent values. - tags: - - data-model - justification: | - When combining partial SPDX data from multiple sources, individual fields may be - null, empty, NOASSERTION, or a concrete value. Preferring a concrete value ensures - meaningful data is preserved regardless of the order in which sources are merged. - tests: - - SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly - - SpdxFile_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxSnippet_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxCreationInformation_Enhance_AddsOrUpdatesInformationCorrectly + - id: SpdxModel-Data-Helpers-EnhanceString + title: >- + When merging optional SPDX fields, the library shall prefer a concrete + value over NOASSERTION or absent values. + tags: + - data-model + justification: | + When combining partial SPDX data from multiple sources, individual fields may be + null, empty, NOASSERTION, or a concrete value. Preferring a concrete value ensures + meaningful data is preserved regardless of the order in which sources are merged. + tests: + - SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly + - SpdxFile_Enhance_MatchingAndNewFiles_MergesCorrectly + - SpdxSnippet_Enhance_MatchingAndNewSnippets_MergesCorrectly + - SpdxCreationInformation_Enhance_WithMissingFieldsInBase_AddsOrUpdatesInformationCorrectly + - SpdxHelpers_EnhanceString_ConcretePreferredOverNoAssertion_ReturnsConcreteValue + - SpdxHelpers_EnhanceString_NullInputs_ReturnsNull diff --git a/docs/reqstream/spdx-model/spdx-license-element.yaml b/docs/reqstream/spdx-model/spdx-license-element.yaml index b6937be..d196088 100644 --- a/docs/reqstream/spdx-model/spdx-license-element.yaml +++ b/docs/reqstream/spdx-model/spdx-license-element.yaml @@ -1,38 +1,63 @@ --- -# SpdxLicenseElement Unit Requirements -# -# This file defines requirements for the SpdxLicenseElement unit. +# SpdxLicenseElement unit requirements sections: - - title: SpdxLicenseElement Unit Requirements - requirements: - - id: SpdxModel-Data-LicenseElement - title: >- - Packages, files, and snippets shall each carry concluded license, - copyright text, license comments, attribution notices, and annotations. - tags: - - data-model - justification: | - Packages, files, and snippets all carry the same set of license and copyright fields - as required by the SPDX specification. Consistent semantics across all three element - types enables uniform processing of license and copyright information. - tests: - - SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance - - SpdxFile_DeepCopy_CreatesEqualButDistinctInstance - - SpdxSnippet_DeepCopy_CreatesEqualButDistinctInstance - - - id: SpdxModel-Data-LicenseElement-Enhance - title: >- - The library shall merge license and copyright fields from a secondary - source when the primary fields are absent. - tags: - - data-model - justification: | - When enriching an incomplete SPDX element with data from a supplementary source, - concluded license, copyright text, attribution texts, and annotations may be absent - from the primary record. These fields shall be populated from the secondary source - only when the primary record lacks a concrete value. - tests: - - SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly - - SpdxFile_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxSnippet_Enhance_AddsOrUpdatesInformationCorrectly + - title: SpdxModel Requirements + sections: + - title: SpdxLicenseElement Requirements + requirements: + - id: SpdxModel-Data-LicenseElement + title: >- + Packages, files, and snippets shall each carry concluded license, + copyright text, license comments, attribution notices, and annotations. + tags: + - data-model + justification: | + Packages, files, and snippets all carry the same set of license and copyright fields + as required by the SPDX specification. Consistent semantics across all three element + types enables uniform processing of license and copyright information. + This is a parent summary requirement; its behavioral responsibilities are decomposed + into the child requirements SpdxModel-Data-LicenseElement-Data and + SpdxModel-Data-LicenseElement-Enhance below. + tests: + - SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance + - SpdxFile_DeepCopy_FullyPopulatedFile_CreatesEqualButDistinctCopy + - SpdxSnippet_DeepCopy_FullyPopulatedSnippet_CreatesEqualButDistinctCopy + children: + - SpdxModel-Data-LicenseElement-Data + - SpdxModel-Data-LicenseElement-Enhance + - id: SpdxModel-Data-LicenseElement-Data + title: >- + The library shall store concluded license, copyright text, license comments, + attribution notices, and annotations on each element type. + tags: + - data-model + justification: | + Each SPDX element type (package, file, snippet) must independently carry the full + set of license and copyright fields so that per-element license information can be + reported and validated without reference to other elements. + tests: + - SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance + - SpdxFile_DeepCopy_FullyPopulatedFile_CreatesEqualButDistinctCopy + - SpdxSnippet_DeepCopy_FullyPopulatedSnippet_CreatesEqualButDistinctCopy + - id: SpdxModel-Data-LicenseElement-Enhance + title: >- + The library shall merge concluded license, copyright text, license comments, + attribution texts, and annotations from a secondary source when the primary + fields are null, empty, or set to NOASSERTION. + tags: + - data-model + justification: | + When enriching an incomplete SPDX element with data from a supplementary source, + concluded license, copyright text, license comments, attribution texts, and + annotations may be absent from the primary record. These fields shall be populated + from the secondary source only when the primary record lacks a concrete value. + tests: + - SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly + - SpdxFile_Enhance_MatchingAndNewFiles_MergesCorrectly + - SpdxSnippet_Enhance_MatchingAndNewSnippets_MergesCorrectly + - SpdxLicenseElement_Enhance_EmptyAndNullFields_ReplacedByConcreteValues + - SpdxLicenseElement_Enhance_NoAssertionFields_ReplacedByConcreteValues + - SpdxLicenseElement_Enhance_ConcreteFields_NotReplacedBySecondaryValues + - SpdxLicenseElement_Enhance_AttributionText_MergedByDeduplication + - SpdxLicenseElement_Enhance_Annotations_MergedByIdentityAndAppend diff --git a/docs/reqstream/spdx-model/spdx-model.yaml b/docs/reqstream/spdx-model/spdx-model.yaml index c12b234..4a87b6b 100644 --- a/docs/reqstream/spdx-model/spdx-model.yaml +++ b/docs/reqstream/spdx-model/spdx-model.yaml @@ -1,12 +1,49 @@ --- -# SpdxModel System Requirements -# -# This file defines the system-level and cross-cutting requirements -# for the SpdxModel library. +# SpdxModel system-level requirements sections: - - title: SpdxModel Library Requirements + - title: SpdxModel Requirements sections: + - title: SPDX Elements + requirements: + - id: SpdxModel-Data-Elements + title: The library shall provide an in-memory model for all SPDX 2.x element types. + tags: + - data-model + justification: | + Supporting all SPDX 2.x element types ensures complete coverage of the specification + and enables applications to work with any SPDX document without information loss. + tests: + - SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully + - SpdxModel_ReadSpdxJson_Spdx23Example_ParsesSuccessfully + children: + - SpdxModel-Data-Document-Ntia + - SpdxModel-Data-Document-ElementQuery + - SpdxModel-Data-ExternalDocumentReferences + - SpdxModel-Data-ExternalReferences + - SpdxModel-Data-ExtractedLicensingInformation + - SpdxModel-Data-Snippet-DataModel + - SpdxModel-Data-LicenseElement + - SpdxModel-Data-RelationshipType-Conversion + - id: SpdxModel-Data-Helpers + title: The library shall provide helper utilities for SPDX data processing. + tags: + - data-model + justification: | + Helper utilities provide shared functionality used across the data model, including + date-time validation and string enhancement operations. + tests: + - SpdxModel_Helpers_DateTimeValidation_IsObservableThroughDocumentModel + children: + - SpdxModel-Data-Helpers-DateTime + - SpdxModel-Data-Helpers-EnhanceString + - SpdxModel-Data-LicenseElement-Enhance + - SpdxModel-Data-Package-Enhance + - SpdxModel-Data-PackageVerificationCode-Enhance + - SpdxModel-Data-Checksum-Enhance + - SpdxModel-Data-Relationship-Enhance + - SpdxModel-Data-Snippet-Enhance + - title: Data Model requirements: - id: SpdxModel-Data-RootPackages @@ -18,7 +55,7 @@ sections: for understanding the primary software packages in a document and navigating the dependency graph from its entry points. children: - - SpdxModel-Data-Document-Query + - SpdxModel-Data-Document-RootPackageQuery tests: - SpdxModel_ReadSpdxJson_Spdx23Example_RootPackagesIdentified @@ -32,7 +69,11 @@ sections: filtering, or transforming documents. children: - SpdxModel-Data-Document - - SpdxModel-Data-Packages + - SpdxModel-Data-Package-DeepCopy + - SpdxModel-Data-PackageVerificationCode-DeepCopy + - SpdxModel-Data-Checksum-DeepCopy + - SpdxModel-Data-Relationship-DeepCopy + - SpdxModel-Data-Snippet-DeepCopy - SpdxModel-Data-Files tests: - SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocument @@ -47,9 +88,9 @@ sections: children: - SpdxModel-Data-Element - SpdxModel-Data-Document - - SpdxModel-Data-Packages + - SpdxModel-Data-Package-Validate tests: - - SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocument + - SpdxModel_FieldOptionality_RequiredFieldsNotNull_OptionalFieldsNullable - id: SpdxModel-Data-ComparisonUtilities title: The library shall provide comparison utilities for SPDX elements. @@ -61,9 +102,12 @@ sections: like document merging or deduplication. children: - SpdxModel-Data-Document - - SpdxModel-Data-Packages + - SpdxModel-Data-Package-Compare + - SpdxModel-Data-PackageVerificationCode-Compare + - SpdxModel-Data-Checksum-Compare - SpdxModel-Data-Files - - SpdxModel-Data-Relationships + - SpdxModel-Data-Relationship-Compare + - SpdxModel-Data-Snippet-DataModel tests: - SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocument @@ -80,8 +124,15 @@ sections: children: - SpdxModel-Data-Document-Validate - SpdxModel-Data-Annotations - - SpdxModel-Data-Checksums + - SpdxModel-Data-Checksum-Validate + - SpdxModel-Data-Checksum-FromText + - SpdxModel-Data-Checksum-ToText - SpdxModel-Data-CreationInformation + - SpdxModel-Data-Package-Validate + - SpdxModel-Data-PackageVerificationCode-Validate + - SpdxModel-Data-Package-ValidateNtia + - SpdxModel-Data-Relationship-Validate + - SpdxModel-Data-Snippet-Validate tests: - SpdxModel_ReadSpdxJson_Spdx22Example_PassesValidation - SpdxModel_ReadSpdxJson_Spdx23Example_PassesValidation @@ -98,7 +149,7 @@ sections: other tools. Supporting both SPDX 2.2 and 2.3 formats ensures compatibility with the broad ecosystem of SPDX tooling and enables consumers to work with existing SBOMs. children: - - SpdxModel-IO-Serialization + - SpdxModel-IO-Deserialization tests: - SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully - SpdxModel_ReadSpdxJson_Spdx23Example_ParsesSuccessfully @@ -116,3 +167,17 @@ sections: - SpdxModel-IO-Serialization tests: - SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds + + - title: Transform + requirements: + - id: SpdxModel-Transform + title: The library shall provide utilities for transforming SPDX documents. + tags: + - transform + justification: | + Transform utilities enable programmatic modification of SPDX documents, supporting + use cases where documents need to be modified or enriched after initial creation. + children: + - SpdxModel-Transform-Utilities + tests: + - SpdxModel_Transform_AddRelationship_IsObservableThroughDocumentModel diff --git a/docs/reqstream/spdx-model/spdx-package-verification-code.yaml b/docs/reqstream/spdx-model/spdx-package-verification-code.yaml index a755508..f4c25b9 100644 --- a/docs/reqstream/spdx-model/spdx-package-verification-code.yaml +++ b/docs/reqstream/spdx-model/spdx-package-verification-code.yaml @@ -1,21 +1,46 @@ --- -# SpdxPackageVerificationCode Unit Requirements -# -# This file defines requirements for the SpdxPackageVerificationCode unit. +# SpdxPackageVerificationCode unit requirements sections: - - title: SpdxPackageVerificationCode Unit Requirements - requirements: - - id: SpdxModel-Data-PackageVerificationCodes - title: The library shall support SPDX package verification codes. - tags: - - data-model - justification: | - Package verification codes provide a way to verify package contents integrity. This - cryptographic verification mechanism is important for ensuring that package contents - have not been tampered with or corrupted. - tests: - - SpdxPackageVerificationCode_SameComparer_ComparesCorrectly - - SpdxPackageVerificationCode_DeepCopy_CreatesEqualButDistinctInstance - - SpdxPackageVerificationCode_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxPackageVerificationCode_Validate_InvalidValue + - title: SpdxModel Requirements + sections: + - title: SpdxPackageVerificationCode Requirements + requirements: + - id: SpdxModel-Data-PackageVerificationCode-Compare + title: The library shall support equality comparison of package verification codes by value. + tags: + - data-model + justification: | + Verification codes must be compared by value to support deduplication and merging + operations. Equality is determined by the SHA1 digest value alone. + tests: + - SpdxPackageVerificationCode_SameComparer_SameValueDifferentExcludedFiles_ReturnsEqual + - id: SpdxModel-Data-PackageVerificationCode-DeepCopy + title: The library shall support creating independent deep copies of package verification codes. + tags: + - data-model + justification: | + Deep copies are required so that merging operations do not mutate the original + verification code instance. + tests: + - SpdxPackageVerificationCode_DeepCopy_FullyPopulatedCode_CreatesEqualButDistinctCopy + - id: SpdxModel-Data-PackageVerificationCode-Enhance + title: The library shall merge missing fields into a package verification code from another instance. + tags: + - data-model + justification: | + Enhance enables combining partial verification code information from multiple SPDX + documents into a single complete record. + tests: + - SpdxPackageVerificationCode_Enhance_MissingFields_MergesCorrectly + - id: SpdxModel-Data-PackageVerificationCode-Validate + title: The library shall validate that a package verification code value is a 40-character SHA1 hex digest. + tags: + - data-model + justification: | + The SPDX specification requires the verification code to be a valid SHA1 hex digest. + Validation ensures the value is exactly 40 lowercase or uppercase hexadecimal characters. + tests: + - SpdxPackageVerificationCode_Validate_ValidValue_ReportsNoIssues + - SpdxPackageVerificationCode_Validate_InvalidValue_ReportsIssue + - SpdxPackageVerificationCode_Validate_NonHexValue_ReportsIssue diff --git a/docs/reqstream/spdx-model/spdx-package.yaml b/docs/reqstream/spdx-model/spdx-package.yaml index 54bc3df..5199cd4 100644 --- a/docs/reqstream/spdx-model/spdx-package.yaml +++ b/docs/reqstream/spdx-model/spdx-package.yaml @@ -1,30 +1,64 @@ --- -# SpdxPackage Unit Requirements -# -# This file defines requirements for the SpdxPackage unit. +# SpdxPackage unit requirements sections: - - title: SpdxPackage Unit Requirements - requirements: - - id: SpdxModel-Data-Packages - title: The library shall support SPDX packages. - tags: - - data-model - justification: | - Packages are core elements in SPDX documents representing software packages in an SBOM. - Supporting package elements is fundamental to the library's purpose of managing software - bill of materials and dependency information. - tests: - - SpdxPackage_SameComparer_ComparesCorrectly - - SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance - - SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly - - SpdxPackage_Validate_Success - - SpdxPackage_Validate_MissingPackageName - - SpdxPackage_Validate_InvalidPackageId - - SpdxPackage_Validate_MissingDownload - - SpdxPackage_Validate_InvalidSupplier - - SpdxPackage_Validate_InvalidOriginator - - SpdxPackage_Validate_InvalidReleaseDate - - SpdxPackage_Validate_InvalidBuiltDate - - SpdxPackage_Validate_InvalidValidUntilDate - - SpdxPackage_Validate_InvalidAnnotation + - title: SpdxModel Requirements + sections: + - title: SpdxPackage Requirements + requirements: + - id: SpdxModel-Data-Package-Compare + title: The library shall support equality comparison of SPDX packages by name and version. + tags: + - data-model + justification: | + Packages must be compared by name and version to support array merging and + deduplication across SPDX documents. + tests: + - SpdxPackage_SameComparer_ComparesCorrectly + - id: SpdxModel-Data-Package-DeepCopy + title: The library shall support creating independent deep copies of SPDX packages. + tags: + - data-model + justification: | + Deep copies are required so that merging operations do not mutate the original + package instances. + tests: + - SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance + - id: SpdxModel-Data-Package-Enhance + title: The library shall support merging missing fields into an SPDX package from another instance. + tags: + - data-model + justification: | + Enhance enables combining partial package information from multiple SPDX documents + into a single complete record. + tests: + - SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly + - id: SpdxModel-Data-Package-Validate + title: The library shall validate SPDX package fields and report all issues found. + tags: + - data-model + justification: | + Validation ensures that package data conforms to the SPDX specification before + serialization or use by downstream consumers. + tests: + - SpdxPackage_Validate_Success + - SpdxPackage_Validate_MissingPackageName_ReportsIssue + - SpdxPackage_Validate_InvalidPackageId_ReportsIssue + - SpdxPackage_Validate_MissingDownload_ReportsIssue + - SpdxPackage_Validate_InvalidSupplier_ReportsIssue + - SpdxPackage_Validate_InvalidOriginator_ReportsIssue + - SpdxPackage_Validate_InvalidReleaseDate_ReportsIssue + - SpdxPackage_Validate_InvalidBuiltDate_ReportsIssue + - SpdxPackage_Validate_InvalidValidUntilDate_ReportsIssue + - SpdxPackage_Validate_InvalidAnnotation_ReportsIssue + - SpdxPackage_Validate_HasFilesReferencesMissingFile_ReportsIssue + - id: SpdxModel-Data-Package-ValidateNtia + title: The library shall validate NTIA minimum element requirements for SPDX packages when requested. + tags: + - data-model + justification: | + NTIA minimum elements (supplier and version) are an opt-in compliance check + that consumers may request independently of general validation. + tests: + - SpdxPackage_ValidateNtia_MissingSupplier_ReportsIssue + - SpdxPackage_ValidateNtia_MissingVersion_ReportsIssue diff --git a/docs/reqstream/spdx-model/spdx-relationship.yaml b/docs/reqstream/spdx-model/spdx-relationship.yaml index 51baa84..b413d3a 100644 --- a/docs/reqstream/spdx-model/spdx-relationship.yaml +++ b/docs/reqstream/spdx-model/spdx-relationship.yaml @@ -1,28 +1,64 @@ --- -# SpdxRelationship Unit Requirements -# -# This file defines requirements for the SpdxRelationship unit. +# SpdxRelationship unit requirements sections: - - title: SpdxRelationship Unit Requirements - requirements: - - id: SpdxModel-Data-Relationships - title: The library shall support SPDX relationships. - tags: - - data-model - justification: | - Relationships define connections between SPDX elements and are critical for expressing - dependency graphs, containment hierarchies, and other associations in SBOMs. This is - fundamental to representing complex software structures. - tests: - - SpdxRelationship_SameComparer_ComparesCorrectly - - SpdxRelationship_SameElementsComparer_ComparesCorrectly - - SpdxRelationship_DeepCopy_CreatesEqualButDistinctInstance - - SpdxRelationship_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxRelationship_Validate_MissingId - - SpdxRelationship_Validate_MissingRelatedId - - SpdxRelationship_Validate_MissingRelationship - - SpdxRelationshipTypeExtensions_FromText_Valid - - SpdxRelationshipTypeExtensions_FromText_Invalid - - SpdxRelationshipTypeExtensions_ToText_Valid - - SpdxRelationshipTypeExtensions_ToText_Invalid + - title: SpdxModel Requirements + sections: + - title: SpdxRelationship Requirements + requirements: + - id: SpdxModel-Data-Relationship-Compare + title: The library shall support equality comparison of SPDX relationships. + tags: + - data-model + justification: | + Relationships must be compared to support array merging and deduplication across + SPDX documents. + tests: + - SpdxRelationship_SameComparer_MatchingRelationships_ReturnsTrue + - SpdxRelationship_SameComparer_DifferentRelationships_ReturnsFalse + - SpdxRelationship_SameComparer_MatchingRelationships_ReturnsSameHashCode + - SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsTrue + - SpdxRelationship_SameElementsComparer_DifferentElements_ReturnsFalse + - SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsSameHashCode + - id: SpdxModel-Data-Relationship-DeepCopy + title: The library shall support creating independent deep copies of SPDX relationships. + tags: + - data-model + justification: | + Deep copies are required so that merging operations do not mutate the original + relationship instances. + tests: + - SpdxRelationship_DeepCopy_FullyPopulatedRelationship_CreatesEqualButDistinctCopy + - id: SpdxModel-Data-Relationship-Enhance + title: The library shall support merging missing fields into an SPDX relationship from another instance. + tags: + - data-model + justification: | + Enhance enables combining partial relationship information from multiple SPDX + documents into a single complete record. + tests: + - SpdxRelationship_Enhance_MatchingAndNewRelationships_MergesCorrectly + - id: SpdxModel-Data-Relationship-Validate + title: The library shall validate SPDX relationship fields and report all issues found. + tags: + - data-model + justification: | + Relationships define connections between SPDX elements and are critical for expressing + dependency graphs, containment hierarchies, and other associations in SBOMs. + tests: + - SpdxRelationship_Validate_MissingRelationshipId_ReportsIssue + - SpdxRelationship_Validate_MissingRelatedElementId_ReportsIssue + - SpdxRelationship_Validate_MissingRelationshipType_ReportsIssue + - id: SpdxModel-Data-RelationshipType-Conversion + title: The library shall support round-trip text conversion for all 45 SPDX relationship type tokens. + tags: + - data-model + justification: | + Relationship types must be serialized to and deserialized from their SPDX string + representations to support reading and writing SPDX JSON documents. + tests: + - SpdxRelationshipTypeExtensions_FromText_KnownText_ReturnsMappedEnum + - SpdxRelationshipTypeExtensions_FromText_UnknownText_ThrowsInvalidOperationException + - SpdxRelationshipTypeExtensions_ToText_KnownEnum_ReturnsMappedText + - SpdxRelationshipTypeExtensions_ToText_MissingSentinel_ThrowsInvalidOperationException + - SpdxRelationshipTypeExtensions_ToText_UnknownEnum_ThrowsInvalidOperationException diff --git a/docs/reqstream/spdx-model/spdx-snippet.yaml b/docs/reqstream/spdx-model/spdx-snippet.yaml index d284aa5..52b0f18 100644 --- a/docs/reqstream/spdx-model/spdx-snippet.yaml +++ b/docs/reqstream/spdx-model/spdx-snippet.yaml @@ -1,23 +1,52 @@ --- -# SpdxSnippet Unit Requirements -# -# This file defines requirements for the SpdxSnippet unit. +# SpdxSnippet unit requirements sections: - - title: SpdxSnippet Unit Requirements - requirements: - - id: SpdxModel-Data-Snippets - title: The library shall support SPDX snippets. - tags: - - data-model - justification: | - Snippets represent portions of files and are important for documenting code reuse at a - granular level. This supports compliance scenarios where specific code segments have - different licensing or provenance than their containing files. - tests: - - SpdxSnippet_SameComparer_ComparesCorrectly - - SpdxSnippet_DeepCopy_CreatesEqualButDistinctInstance - - SpdxSnippet_Enhance_AddsOrUpdatesInformationCorrectly - - SpdxSnippet_Validate_ReportsInvalidSnippetId - - SpdxSnippet_Validate_Success - - SpdxSnippet_Validate_InvalidAnnotation + - title: SpdxModel Requirements + sections: + - title: SpdxSnippet Requirements + requirements: + - id: SpdxModel-Data-Snippet-DataModel + title: The library shall represent SPDX snippet data including byte ranges and licensing information. + tags: + - data-model + justification: | + Snippets represent portions of files and are important for documenting code reuse at a + granular level, supporting compliance scenarios where specific code segments have + different licensing or provenance than their containing files. + tests: + - SpdxSnippet_SameComparer_SameFileAndByteRange_ReturnsEqual + - id: SpdxModel-Data-Snippet-DeepCopy + title: The library shall support creating independent deep copies of SPDX snippets. + tags: + - data-model + justification: | + Deep copies are required so that merging operations do not mutate the original + snippet instances. + tests: + - SpdxSnippet_DeepCopy_FullyPopulatedSnippet_CreatesEqualButDistinctCopy + - id: SpdxModel-Data-Snippet-Enhance + title: The library shall support merging missing fields into an SPDX snippet from another instance. + tags: + - data-model + justification: | + Enhance enables combining partial snippet information from multiple SPDX documents + into a single complete record. + tests: + - SpdxSnippet_Enhance_MatchingAndNewSnippets_MergesCorrectly + - id: SpdxModel-Data-Snippet-Validate + title: The library shall validate SPDX snippet fields and report all issues found. + tags: + - data-model + justification: | + Validation ensures that snippet data conforms to the SPDX specification including + valid byte ranges, required fields, and correct ID format. + tests: + - SpdxSnippet_Validate_InvalidSnippetId_ReportsIssue + - SpdxSnippet_Validate_AllRequiredFieldsPresent_ReturnsNoIssues + - SpdxSnippet_Validate_InvalidAnnotation_ReportsIssue + - SpdxSnippet_Validate_EmptySnippetFromFile_ReportsIssue + - SpdxSnippet_Validate_InvalidByteStart_ReportsIssue + - SpdxSnippet_Validate_InvalidByteEnd_ReportsIssue + - SpdxSnippet_Validate_EmptyConcludedLicense_ReportsIssue + - SpdxSnippet_Validate_EmptyCopyrightText_ReportsIssue diff --git a/docs/reqstream/spdx-model/transform/spdx-relationships.yaml b/docs/reqstream/spdx-model/transform/spdx-relationships.yaml index c9419df..0772bde 100644 --- a/docs/reqstream/spdx-model/transform/spdx-relationships.yaml +++ b/docs/reqstream/spdx-model/transform/spdx-relationships.yaml @@ -1,25 +1,58 @@ --- -# SpdxModel SpdxRelationships Unit Requirements -# -# This file defines the requirements for the SpdxRelationships unit -# in the Transform subsystem of the SpdxModel library. +# SpdxRelationships unit requirements sections: - - title: SpdxRelationships Requirements - requirements: - - id: SpdxModel-Data-RelationshipUtilities - title: The library shall provide utilities for manipulating SPDX relationships. - tags: - - data-model - justification: | - Relationship manipulation utilities simplify common operations on SPDX documents such as - adding and managing relationships between elements. This improves developer productivity - and reduces errors when constructing complex SBOMs. - tests: - - SpdxRelationships_AddSingle_Success - - SpdxRelationships_AddSingle_MissingId - - SpdxRelationships_AddSingle_MissingRelatedElement - - SpdxRelationships_AddSingle_Duplicate - - SpdxRelationships_AddMultiple_Success - - SpdxRelationships_AddMultiple_Duplicate - - SpdxRelationships_AddMultiple_Replace + - title: SpdxModel Requirements + sections: + - title: Transform Requirements + sections: + - title: SpdxRelationships Requirements + requirements: + - id: SpdxModel-Transform-SpdxRelationships-AddSingle + title: The library shall provide a utility to add a single relationship to an SPDX document. + tags: + - data-model + justification: | + Adding a single relationship is the fundamental mutation operation. Atomic requirements + allow each behaviour to be verified and traced independently. + tests: + - SpdxRelationships_AddSingle_ValidRelationship_AddsRelationship + - SpdxRelationships_AddSingle_DuplicateRelationship_EnhancesExistingRelationship + - SpdxRelationships_AddSingle_NoAssertionTarget_AddsRelationship + - SpdxRelationships_AddSingle_DocumentRefTarget_AddsRelationship + + - id: SpdxModel-Transform-SpdxRelationships-AddMultiple + title: >- + The library shall provide a utility to batch-add relationships to an + SPDX document with optional replacement. + tags: + - data-model + justification: | + Batch operations improve developer productivity when constructing or updating relationship + graphs. The replace flag enables idempotent updates to existing relationships. + tests: + - SpdxRelationships_AddMultiple_SingleRelationship_AddsRelationship + - SpdxRelationships_AddMultiple_DuplicateRelationships_DeduplicatesRelationships + - SpdxRelationships_AddMultiple_Replace_RemovesAndReplacesExistingRelationships + + - id: SpdxModel-Transform-SpdxRelationships-Validate + title: The library shall validate relationship element IDs before adding them to an SPDX document. + tags: + - data-model + justification: | + Validation prevents corrupt or incomplete relationships from being written into the document + and gives callers actionable error messages when IDs cannot be resolved. + tests: + - SpdxRelationships_AddSingle_MissingId_ThrowsArgumentException + - SpdxRelationships_AddSingle_MissingRelatedElement_ThrowsArgumentException + - SpdxRelationships_AddMultiple_InvalidRelationship_LeavesDocumentUnmodified + + - id: SpdxModel-Transform-SpdxRelationships-Atomicity + title: The library shall guarantee that a failed batch-add leaves the document in its original state. + tags: + - data-model + justification: | + Atomicity prevents partial writes when one of several incoming relationships is invalid, + ensuring the document is never left in a half-updated state. + tests: + - SpdxRelationships_AddMultiple_InvalidRelationship_LeavesDocumentUnmodified diff --git a/docs/reqstream/spdx-model/transform/transform.yaml b/docs/reqstream/spdx-model/transform/transform.yaml index bef1680..e5a6b20 100644 --- a/docs/reqstream/spdx-model/transform/transform.yaml +++ b/docs/reqstream/spdx-model/transform/transform.yaml @@ -1,22 +1,23 @@ --- -# SpdxModel Transform Subsystem Requirements -# -# This is the subsystem-level requirements file for the Transform subsystem. -# Unit requirements are in the sibling unit file: -# spdx-relationships.yaml +# Transform subsystem requirements sections: - - title: Transform Subsystem Requirements - requirements: - - id: SpdxModel-Transform-Utilities - title: The library shall provide utilities for transforming SPDX document relationships. - tags: - - transform - justification: | - Transform utilities enable programmatic modification of SPDX documents, starting with - relationship management. This supports use cases where documents need to be modified or - enriched after initial creation. - children: - - SpdxModel-Data-RelationshipUtilities - tests: - - SpdxModelTransform_AddRelationship_ToDocument_RelationshipPersists + - title: SpdxModel Requirements + sections: + - title: Transform Requirements + requirements: + - id: SpdxModel-Transform-Utilities + title: The library shall provide utilities for transforming SPDX document relationships. + tags: + - transform + justification: | + Transform utilities enable programmatic modification of SPDX documents, starting with + relationship management. This supports use cases where documents need to be modified or + enriched after initial creation. + children: + - SpdxModel-Transform-SpdxRelationships-AddSingle + - SpdxModel-Transform-SpdxRelationships-AddMultiple + - SpdxModel-Transform-SpdxRelationships-Validate + - SpdxModel-Transform-SpdxRelationships-Atomicity + tests: + - SpdxModelTransform_AddRelationship_ToDocument_RelationshipPersists diff --git a/docs/requirements_doc/definition.yaml b/docs/requirements_doc/definition.yaml index 0f4ccd2..bc9c807 100644 --- a/docs/requirements_doc/definition.yaml +++ b/docs/requirements_doc/definition.yaml @@ -1,12 +1,10 @@ --- -resource-path: - - docs/requirements_doc - - docs/template +resource-path: [docs/requirements_doc, docs/template] 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 # Generated by ReqStream (requirements listing) + - docs/requirements_doc/generated/justifications.md # Generated by ReqStream (requirement justifications) template: template.html table-of-contents: true number-sections: true diff --git a/docs/requirements_doc/introduction.md b/docs/requirements_doc/introduction.md index b15eab3..30d7e30 100644 --- a/docs/requirements_doc/introduction.md +++ b/docs/requirements_doc/introduction.md @@ -1,30 +1,16 @@ # Introduction -This document specifies the requirements for the SpdxModel library, a C# library for working with -SPDX (Software Package Data Exchange) documents. +This document lists all requirements for SpdxModel. ## Purpose -The purpose of this document is to: - -- Define the functional and quality requirements for the SpdxModel library -- Provide traceability between requirements and test cases -- Serve as a baseline for development and validation +To provide a complete, traceable record of all requirements for SpdxModel, +including requirements at the system, subsystem, and unit levels, plus OTS and Shared Package requirements. ## Scope -The SpdxModel library is designed to: - -- Provide a comprehensive in-memory model for SPDX documents -- Support SPDX 2.2 and 2.3 specifications -- Enable reading and writing SPDX documents in JSON format -- Offer utilities for manipulating SPDX relationships -- Target .NET 8, 9, and 10 frameworks - -## Audience +This document covers all requirements defined in `docs/reqstream/` for SpdxModel. -This document is intended for: +## References -- Software developers implementing the library -- Quality assurance engineers validating the requirements -- Project stakeholders reviewing the scope and capabilities +- [SpdxModel releases](https://github.com/demaconsulting/SpdxModel/releases) diff --git a/docs/requirements_doc/title.txt b/docs/requirements_doc/title.txt index f82e162..af678f1 100644 --- a/docs/requirements_doc/title.txt +++ b/docs/requirements_doc/title.txt @@ -1,15 +1,14 @@ --- title: SpdxModel Requirements -subtitle: Requirements Specification +subtitle: SPDX document model for .NET author: DEMA Consulting -description: Requirements specification for the SpdxModel C# library for working with SPDX (Software Package Data Exchange) documents +description: "Requirements Document for SpdxModel" lang: en-US keywords: - - SpdxModel - Requirements + - SpdxModel - C# - .NET - SPDX - SBOM - - Software Bill of Materials --- diff --git a/docs/requirements_report/definition.yaml b/docs/requirements_report/definition.yaml index 918a645..e8b5571 100644 --- a/docs/requirements_report/definition.yaml +++ b/docs/requirements_report/definition.yaml @@ -1,11 +1,9 @@ --- -resource-path: - - docs/requirements_report - - docs/template +resource-path: [docs/requirements_report, docs/template] 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 # Generated by ReqStream (requirements traceability matrix) template: template.html table-of-contents: true number-sections: true diff --git a/docs/requirements_report/introduction.md b/docs/requirements_report/introduction.md index d25068d..d5a59fb 100644 --- a/docs/requirements_report/introduction.md +++ b/docs/requirements_report/introduction.md @@ -1,29 +1,17 @@ # Introduction -This document provides a requirements traceability matrix for the SpdxModel library, showing -the relationship between requirements and test cases. +This document provides the requirements Trace Matrix for SpdxModel, +mapping each requirement to its corresponding test evidence. ## Purpose -The purpose of this document is to: - -- Demonstrate that all requirements are covered by test cases -- Provide traceability from requirements to implementation verification -- Support compliance and validation activities +To demonstrate that every requirement is covered by at least one passing test, +providing compliance evidence for SpdxModel. ## Scope -This trace matrix covers: - -- All functional, quality, and documentation requirements -- Test cases that verify each requirement -- Pass/fail status for each requirement verification - -## How to Read This Document +This document covers all requirements in `docs/reqstream/` and their test evidence. -The trace matrix shows: +## References -- **Requirement ID**: Unique identifier for each requirement -- **Requirement Title**: Brief description of the requirement -- **Test Cases**: Tests that verify the requirement -- **Status**: Pass/fail status based on test execution results +- [SpdxModel releases](https://github.com/demaconsulting/SpdxModel/releases) diff --git a/docs/requirements_report/title.txt b/docs/requirements_report/title.txt index 8fd6f31..c011281 100644 --- a/docs/requirements_report/title.txt +++ b/docs/requirements_report/title.txt @@ -1,16 +1,14 @@ --- title: SpdxModel Trace Matrix -subtitle: Requirements Trace Matrix +subtitle: SPDX document model for .NET author: DEMA Consulting -description: Requirements Traceability Matrix for the SpdxModel C# library showing the relationship between requirements and test cases +description: Trace Matrix for SpdxModel lang: en-US keywords: - - SpdxModel - - Trace Matrix - Requirements + - Trace Matrix + - SpdxModel - C# - .NET - SPDX - - Testing - - Traceability --- diff --git a/docs/user_guide/definition.yaml b/docs/user_guide/definition.yaml index affad56..0377821 100644 --- a/docs/user_guide/definition.yaml +++ b/docs/user_guide/definition.yaml @@ -1,13 +1,11 @@ --- -resource-path: - - docs/user_guide - - docs/template - +resource-path: [docs/user_guide, docs/template] input-files: - docs/user_guide/title.txt - docs/user_guide/introduction.md + - docs/user_guide/installation.md + - docs/user_guide/usage.md + - docs/user_guide/troubleshooting.md template: template.html - table-of-contents: true - number-sections: true diff --git a/docs/user_guide/installation.md b/docs/user_guide/installation.md new file mode 100644 index 0000000..ada5a20 --- /dev/null +++ b/docs/user_guide/installation.md @@ -0,0 +1,31 @@ +# Installation + +## Prerequisites + +Using SpdxModel requires: + +- .NET Standard 2.0, .NET 8.0, 9.0, or 10.0 +- C# 12 or later + +## Installing via NuGet + +Install the SpdxModel package via the .NET CLI: + +```bash +dotnet add package DemaConsulting.SpdxModel +``` + +Or via the Package Manager Console in Visual Studio: + +```powershell +Install-Package DemaConsulting.SpdxModel +``` + +## Verifying Installation + +After installation, verify that you can reference the library namespaces: + +```csharp +using DemaConsulting.SpdxModel; +using DemaConsulting.SpdxModel.IO; +``` diff --git a/docs/user_guide/introduction.md b/docs/user_guide/introduction.md index 351d148..5d51d8d 100644 --- a/docs/user_guide/introduction.md +++ b/docs/user_guide/introduction.md @@ -1,685 +1,30 @@ # Introduction -## Purpose - -The SpdxModel library is a modern C# library designed for working with SPDX (Software Package Data Exchange) documents. -SPDX is an open standard for communicating software bill of materials (SBOM) information, including components, -licenses, copyrights, and security references. +This guide describes how to install, configure, and use SpdxModel. -This library provides a comprehensive in-memory model for reading, manipulating, and writing SPDX SBOM files in JSON -format. +## Purpose -The library offers the following key features: +SpdxModel is a modern C# library for working with SPDX (Software Package Data Exchange) documents. +SPDX is an open standard for communicating software bill of materials (SBOM) information, including +components, licenses, copyrights, and security references. -- 🚀 **Full SPDX Support**: Complete implementation of SPDX 2.2 and 2.3 specifications -- 📦 **In-Memory Model**: Efficient object model for SPDX documents -- 🔄 **JSON Serialization**: Read and write SPDX documents in JSON format -- 🎯 **Type-Safe**: Strongly-typed C# API with nullable reference types -- 🔍 **Transform Support**: Built-in utilities for manipulating SPDX relationships -- ⚡ **Multi-Target**: Supports .NET Standard 2.0, .NET 8, 9, and 10 -- 🧪 **Well-Tested**: Comprehensive test suite with high code coverage -- 📚 **Well-Documented**: XML documentation for all public APIs +The library provides a comprehensive in-memory model for reading, manipulating, and writing SPDX SBOM +files in JSON format, with full support for SPDX 2.2 and 2.3 specifications. It targets .NET Standard +2.0, .NET 8, 9, and 10, and builds and runs on Windows, Linux, and macOS. It is suitable for use in +.NET applications, tools, and CI/CD pipelines. ## Scope -This library is ideal for: - -- **SBOM Generation**: Creating software bill of materials for your applications -- **SBOM Analysis**: Parsing and analyzing existing SPDX documents -- **License Compliance**: Tracking and managing software licenses -- **Supply Chain Security**: Managing software component relationships and dependencies -- **CI/CD Integration**: Automating SBOM creation and validation in build pipelines -- **Tool Integration**: Building tools that work with SPDX documents - -The library fully supports the following SPDX specifications: - -- **SPDX 2.2**: Full support for SPDX 2.2 specification -- **SPDX 2.3**: Full support for SPDX 2.3 specification - -# Continuous Compliance - -This library follows the [Continuous Compliance][continuous-compliance] methodology, which ensures -compliance evidence is generated automatically on every CI run. - -## Key Practices +This guide covers installation of the SpdxModel NuGet package, basic and advanced usage of the library +API, and troubleshooting common issues. It includes usage examples for reading and writing SPDX documents, +working with packages, files, snippets, relationships, and custom license references. -- **Requirements Traceability**: Every requirement is linked to passing tests, and a trace matrix is - auto-generated on each release -- **Linting Enforcement**: markdownlint, cspell, and yamllint are enforced before any build proceeds -- **Automated Audit Documentation**: Each release ships with generated requirements, justifications, - trace matrix, and quality reports -- **CodeQL and SonarCloud**: Security and quality analysis runs on every build - -# Installation - -## Prerequisites +Prerequisites for using SpdxModel: - .NET Standard 2.0, .NET 8.0, 9.0, or 10.0 - C# 12 or later -## Installing via NuGet - -You can install the SpdxModel package via NuGet Package Manager: - -```bash -dotnet add package DemaConsulting.SpdxModel -``` - -Or via the Package Manager Console in Visual Studio: - -```powershell -Install-Package DemaConsulting.SpdxModel -``` - -## Verifying Installation - -After installation, verify that you can import the library: - -```csharp -using DemaConsulting.SpdxModel; -using DemaConsulting.SpdxModel.IO; -``` - -# Quick Start - -## Reading an SPDX Document - -The simplest way to get started is to read an existing SPDX document: - -```csharp -using DemaConsulting.SpdxModel; -using DemaConsulting.SpdxModel.IO; - -// Read SPDX document from JSON file -var json = File.ReadAllText("sbom.spdx.json"); -var document = Spdx2JsonDeserializer.Deserialize(json); - -// Access document properties -Console.WriteLine($"Document: {document.Name}"); -Console.WriteLine($"Version: {document.Version}"); -Console.WriteLine($"Namespace: {document.DocumentNamespace}"); -Console.WriteLine($"Packages: {document.Packages.Length}"); -Console.WriteLine($"Files: {document.Files.Length}"); -``` - -## Creating a Simple SPDX Document - -Here's how to create a minimal SPDX document: - -```csharp -using DemaConsulting.SpdxModel; -using DemaConsulting.SpdxModel.IO; - -// Create a new SPDX document -var document = new SpdxDocument -{ - Id = "SPDXRef-DOCUMENT", - Name = "My Software", - Version = "SPDX-2.3", - DocumentNamespace = "https://example.com/my-software/1.0.0", - CreationInformation = new SpdxCreationInformation - { - Created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), - Creators = ["Tool: MyTool-1.0", "Organization: Example Corp"] - }, - Packages = - [ - new SpdxPackage - { - Id = "SPDXRef-Package", - Name = "MyPackage", - Version = "1.0.0", - DownloadLocation = "https://example.com/package", - FilesAnalyzed = false, - LicenseConcluded = "MIT", - LicenseDeclared = "MIT", - CopyrightText = "Copyright (c) 2024 Example Corp" - } - ] -}; - -// Serialize to JSON -var json = Spdx2JsonSerializer.Serialize(document); -File.WriteAllText("output.spdx.json", json); -``` - -# Core Concepts - -## SPDX Elements - -SPDX documents consist of several key element types: - -- **Document**: The root element containing metadata and all other elements -- **Package**: Represents a software package or component -- **File**: Represents individual files within packages -- **Snippet**: Represents code snippets within files -- **Relationship**: Describes relationships between elements - -## SPDX Identifiers - -Every SPDX element must have a unique identifier within the document. Identifiers follow the format `SPDXRef-{name}`: - -```csharp -var package = new SpdxPackage -{ - Id = "SPDXRef-MyPackage", - Name = "MyPackage" -}; -``` - -## Document Namespace - -Every SPDX document must have a unique namespace URI that identifies the document: - -```csharp -var document = new SpdxDocument -{ - DocumentNamespace = "https://example.com/my-software/1.0.0" -}; -``` - -The namespace should be unique for each version of your software. - -# Working with Documents - -## Document Properties - -A complete SPDX document includes: - -```csharp -var document = new SpdxDocument -{ - // Required fields - Id = "SPDXRef-DOCUMENT", - Name = "My Software SBOM", - Version = "SPDX-2.3", - DataLicense = "CC0-1.0", - DocumentNamespace = "https://example.com/my-software/1.0.0", - - // Creation information - CreationInformation = new SpdxCreationInformation - { - Created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), - Creators = - [ - "Tool: MyTool-1.0", - "Organization: Example Corp", - "Person: John Doe (john@example.com)" - ], - LicenseListVersion = "3.21" - }, - - // Optional fields - Comment = "This SBOM describes the software components", - - // Collections - Packages = [], - Files = [], - Snippets = [], - Relationships = [] -}; -``` - -## Document Describes Relationship - -Every SPDX document should have at least one "DESCRIBES" relationship indicating what the document describes: - -```csharp -document.Relationships = -[ - new SpdxRelationship - { - Id = "SPDXRef-DOCUMENT", - RelationshipType = SpdxRelationshipType.Describes, - RelatedSpdxElement = "SPDXRef-RootPackage" - } -]; -``` - -# Working with Packages - -## Creating Packages - -A package represents a software component or library: - -```csharp -var package = new SpdxPackage -{ - // Required fields - Id = "SPDXRef-Package", - Name = "MyPackage", - DownloadLocation = "https://github.com/example/mypackage", - FilesAnalyzed = false, - - // Version information - Version = "1.0.0", - - // License information - LicenseConcluded = "MIT", - LicenseDeclared = "MIT", - LicenseComments = "This package is licensed under MIT", - - // Copyright information - CopyrightText = "Copyright (c) 2024 Example Corp", - - // Optional metadata - Summary = "A useful package for doing things", - Description = "This package provides functionality for...", - Homepage = "https://example.com/mypackage", - - // Source information - SourceInfo = "Built from commit abc123", - - // Package supplier and originator - Supplier = "Organization: Example Corp", - Originator = "Organization: Original Corp" -}; -``` - -## Package with Files - -When a package includes file information: - -```csharp -var package = new SpdxPackage -{ - Id = "SPDXRef-Package", - Name = "MyPackage", - DownloadLocation = "https://github.com/example/mypackage", - FilesAnalyzed = true, // Set to true when analyzing files - VerificationCode = new SpdxPackageVerificationCode - { - Value = "d6a770ba38583ed4bb4525bd96e50461655d2758", - ExcludedFiles = ["./package.spdx"] - } -}; - -// Add files (see next section) -document.Files = -[ - new SpdxFile - { - Id = "SPDXRef-File1", - FileName = "./src/main.cs" - } -]; - -// Add relationship linking package to files -document.Relationships = -[ - new SpdxRelationship - { - Id = "SPDXRef-Package", - RelationshipType = SpdxRelationshipType.Contains, - RelatedSpdxElement = "SPDXRef-File1" - } -]; -``` - -## External References - -Packages can include external references for security and other metadata: - -```csharp -var package = new SpdxPackage -{ - Id = "SPDXRef-Package", - Name = "MyPackage", - ExternalReferences = - [ - new SpdxExternalReference - { - Category = SpdxReferenceCategory.Security, - Type = "cpe23Type", - Locator = "cpe:2.3:a:example:my-package:1.0.0:*:*:*:*:*:*:*" - }, - new SpdxExternalReference - { - Category = SpdxReferenceCategory.PackageManager, - Type = "purl", - Locator = "pkg:nuget/MyPackage@1.0.0" - } - ] -}; -``` - -# Working with Files - -## Creating Files - -Files represent individual files within packages: - -```csharp -var file = new SpdxFile -{ - // Required fields - Id = "SPDXRef-File1", - FileName = "./src/main.cs", - - // Checksums - Checksums = - [ - new SpdxChecksum - { - Algorithm = SpdxChecksumAlgorithm.SHA256, - Value = "abc123def456..." - } - ], - - // License information - LicenseConcluded = "MIT", - LicenseInfoInFiles = ["MIT"], - CopyrightText = "Copyright (c) 2024 Example Corp", - - // File type - FileTypes = [SpdxFileType.Source], - - // Optional fields - Comment = "Main application file" -}; -``` - -## File Checksums - -Files should include checksums for integrity verification: - -```csharp -var file = new SpdxFile -{ - Id = "SPDXRef-File1", - FileName = "./lib/library.dll", - Checksums = - [ - new SpdxChecksum - { - Algorithm = SpdxChecksumAlgorithm.SHA256, - Value = "abc123..." - }, - new SpdxChecksum - { - Algorithm = SpdxChecksumAlgorithm.SHA1, - Value = "def456..." - } - ] -}; -``` - -# Working with Relationships - -## Relationship Types - -SPDX defines many relationship types. Common ones include: - -- **DESCRIBES**: Document describes an element -- **CONTAINS**: Package contains files or other packages -- **DEPENDS_ON**: Package depends on another package -- **DEPENDENCY_OF**: Inverse of DEPENDS_ON -- **BUILD_DEPENDENCY_OF**: Build-time dependency -- **DEV_DEPENDENCY_OF**: Development dependency -- **GENERATED_FROM**: File generated from another file - -## Adding Relationships - -```csharp -using DemaConsulting.SpdxModel.Transform; - -// Add a relationship -var relationship = new SpdxRelationship -{ - Id = "SPDXRef-Package1", - RelationshipType = SpdxRelationshipType.DependsOn, - RelatedSpdxElement = "SPDXRef-Package2" -}; - -SpdxRelationships.Add(document, relationship); -``` - -## Finding Related Elements - -```csharp -// Get packages described by the document -var rootPackages = document.GetRootPackages(); -``` - -# Advanced Usage - -## Custom License References - -For licenses not in the SPDX license list: - -```csharp -var document = new SpdxDocument -{ - // ... other fields ... - - ExtractedLicensingInfo = - [ - new SpdxExtractedLicensingInfo - { - LicenseId = "LicenseRef-CustomLicense", - ExtractedText = "Full license text here...", - Name = "Custom License", - CrossReferences = ["https://example.com/license"] - } - ] -}; - -// Reference the custom license -var package = new SpdxPackage -{ - Id = "SPDXRef-Package", - Name = "MyPackage", - LicenseConcluded = "LicenseRef-CustomLicense" -}; -``` - -## Document Annotations - -Add annotations to provide additional information: - -```csharp -var annotation = new SpdxAnnotation -{ - Annotator = "Person: John Doe", - Date = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), - Type = SpdxAnnotationType.Review, - Comment = "This package has been reviewed for security concerns" -}; - -document.Annotations = [annotation]; -``` - -## Working with Snippets - -Snippets represent portions of files: - -```csharp -var snippet = new SpdxSnippet -{ - Id = "SPDXRef-Snippet1", - SnippetFromFile = "SPDXRef-File1", - Name = "Authentication Function", - SnippetLineStart = 100, - SnippetLineEnd = 150, - LicenseConcluded = "MIT" -}; - -document.Snippets = [snippet]; -``` - -# Best Practices - -## Use Meaningful Identifiers - -Choose descriptive SPDX identifiers: - -```csharp -// Good -Id = "SPDXRef-Package-MyLibrary-1.0.0" - -// Not as good -Id = "SPDXRef-Package1" -``` - -## Include Complete License Information - -Always specify both concluded and declared licenses: - -```csharp -var package = new SpdxPackage -{ - LicenseConcluded = "MIT", // What you determined - LicenseDeclared = "MIT" // What the package declares -}; -``` - -## Use Checksums for Files - -Always include checksums for files when possible: - -```csharp -var file = new SpdxFile -{ - FileName = "./app.exe", - Checksums = - [ - new SpdxChecksum - { - Algorithm = SpdxChecksumAlgorithm.SHA256, - Value = "..." - } - ] -}; -``` - -## Maintain Unique Document Namespaces - -Each version of your software should have a unique namespace: - -```csharp -// Good - includes version -DocumentNamespace = "https://example.com/myapp/1.0.0" - -// Not as good - no version -DocumentNamespace = "https://example.com/myapp" -``` - -## Document Relationships - -Explicitly document relationships between components: - -```csharp -// Document what it describes -document.Relationships = -[ - new SpdxRelationship - { - Id = "SPDXRef-DOCUMENT", - RelationshipType = SpdxRelationshipType.Describes, - RelatedSpdxElement = "SPDXRef-RootPackage" - } -]; -``` - -## Include Creator Information - -Provide complete creator information: - -```csharp -CreationInformation = new SpdxCreationInformation -{ - Created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), - Creators = - [ - "Tool: MyTool-1.0", - "Organization: Example Corp", - "Person: Build System" - ] -}; -``` - -## Error Handling - -Always handle potential errors when deserializing: - -```csharp -try -{ - var json = File.ReadAllText("sbom.spdx.json"); - var document = Spdx2JsonDeserializer.Deserialize(json); -} -catch (JsonException ex) -{ - Console.WriteLine($"Failed to parse SPDX document: {ex.Message}"); -} -catch (IOException ex) -{ - Console.WriteLine($"Failed to read file: {ex.Message}"); -} -``` - -# Troubleshooting - -## Common Issues - -### Invalid SPDX Identifier Format - -SPDX identifiers must start with "SPDXRef-": - -```csharp -// Correct -Id = "SPDXRef-MyPackage" - -// Incorrect -Id = "MyPackage" -``` - -### Missing Required Fields - -Ensure all required fields are populated: - -```csharp -var document = new SpdxDocument -{ - Id = "SPDXRef-DOCUMENT", // Required - Name = "MyDoc", // Required - Version = "SPDX-2.3", // Required - DataLicense = "CC0-1.0", // Required - DocumentNamespace = "...", // Required - CreationInformation = new ... // Required -}; -``` - -# Additional Resources - -- [SPDX Specification][spdx-spec] - Official SPDX specification -- [API Documentation][api-docs] - Detailed API reference -- [GitHub Repository][github-repo] - Source code and issues -- [NuGet Package][nuget-package] - Package downloads -- [spdx-tool][spdx-tool] - Command-line tool for SPDX documents - -# Support - -For help and support: - -- 📫 **Issues**: [GitHub Issues][github-issues] -- 💬 **Discussions**: [GitHub Discussions][github-discussions] -- 📧 **Email**: Contact DEMA Consulting for enterprise support - -# License - -This library is licensed under the MIT License. See the LICENSE file for details. - ---- - -Made with ❤️ by [DEMA Consulting][dema-consulting] +## References -[spdx-spec]: https://spdx.dev/ -[api-docs]: https://github.com/demaconsulting/SpdxModel/wiki -[github-repo]: https://github.com/demaconsulting/SpdxModel -[nuget-package]: https://www.nuget.org/packages/DemaConsulting.SpdxModel/ -[spdx-tool]: https://github.com/demaconsulting/spdx-tool -[github-issues]: https://github.com/demaconsulting/SpdxModel/issues -[github-discussions]: https://github.com/demaconsulting/SpdxModel/discussions -[dema-consulting]: https://github.com/demaconsulting -[continuous-compliance]: https://github.com/demaconsulting/ContinuousCompliance +- [REF-1] SpdxModel releases, +- [REF-2] SPDX Specification, diff --git a/docs/user_guide/title.txt b/docs/user_guide/title.txt index c17bd05..1f70118 100644 --- a/docs/user_guide/title.txt +++ b/docs/user_guide/title.txt @@ -1,15 +1,14 @@ --- -title: SpdxModel User Guide -subtitle: User Guide -author: DEMA Consulting -description: A C# library for serializing and deserializing SPDX SBOMs +title: "SpdxModel User Guide" +subtitle: "SPDX document model for .NET" +author: "DEMA Consulting" +description: "User Guide for SpdxModel" lang: en-US keywords: - SpdxModel - - C# - - .NET + - User Guide - SPDX - SBOM - - Software Bill of Materials - - Documentation + - C# + - .NET --- diff --git a/docs/user_guide/troubleshooting.md b/docs/user_guide/troubleshooting.md new file mode 100644 index 0000000..0d5dcb7 --- /dev/null +++ b/docs/user_guide/troubleshooting.md @@ -0,0 +1,63 @@ +# Troubleshooting + +## Common Issues + +### Invalid SPDX Identifier Format + +SPDX identifiers must start with `SPDXRef-`. Using any other prefix causes validation errors: + +```csharp +// Correct +Id = "SPDXRef-MyPackage" + +// Incorrect +Id = "MyPackage" +``` + +### Missing Required Fields + +Ensure all required fields are populated before serializing a document: + +```csharp +var document = new SpdxDocument +{ + Id = "SPDXRef-DOCUMENT", // Required + Name = "MyDoc", // Required + Version = "SPDX-2.3", // Required + DataLicense = "CC0-1.0", // Required + DocumentNamespace = "...", // Required + CreationInformation = new ... // Required +}; +``` + +### Error Handling During Deserialization + +Always handle potential errors when reading SPDX documents from untrusted sources: + +```csharp +using System.IO; +using System.Text.Json; +using DemaConsulting.SpdxModel.IO; + +try +{ + var json = File.ReadAllText("sbom.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); +} +catch (JsonException ex) +{ + Console.WriteLine($"Failed to parse SPDX document: {ex.Message}"); +} +catch (IOException ex) +{ + Console.WriteLine($"Failed to read file: {ex.Message}"); +} +``` + +## Additional Resources + +- [SPDX Specification](https://spdx.dev/) — Official SPDX specification +- [SpdxModel NuGet Package](https://www.nuget.org/packages/DemaConsulting.SpdxModel/) — Package downloads +- [spdx-tool](https://github.com/demaconsulting/spdx-tool) — Command-line tool for SPDX documents +- [GitHub Issues](https://github.com/demaconsulting/SpdxModel/issues) — Bug reports and feature requests +- [GitHub Discussions](https://github.com/demaconsulting/SpdxModel/discussions) — Community questions and support diff --git a/docs/user_guide/usage.md b/docs/user_guide/usage.md new file mode 100644 index 0000000..0d4bc7d --- /dev/null +++ b/docs/user_guide/usage.md @@ -0,0 +1,570 @@ +# Usage + +## Quick Start + +### Reading an SPDX Document + +The simplest way to get started is to read an existing SPDX document: + +```csharp +using DemaConsulting.SpdxModel; +using DemaConsulting.SpdxModel.IO; + +// Read SPDX document from JSON file +var json = File.ReadAllText("sbom.spdx.json"); +var document = Spdx2JsonDeserializer.Deserialize(json); + +// Access document properties +Console.WriteLine($"Document: {document.Name}"); +Console.WriteLine($"Version: {document.Version}"); +Console.WriteLine($"Namespace: {document.DocumentNamespace}"); +Console.WriteLine($"Packages: {document.Packages.Length}"); +Console.WriteLine($"Files: {document.Files.Length}"); +``` + +### Creating a Simple SPDX Document + +Here is how to create a minimal SPDX document: + +```csharp +using DemaConsulting.SpdxModel; +using DemaConsulting.SpdxModel.IO; + +// Create a new SPDX document +var document = new SpdxDocument +{ + Id = "SPDXRef-DOCUMENT", + Name = "My Software", + Version = "SPDX-2.3", + DataLicense = "CC0-1.0", + DocumentNamespace = "https://example.com/my-software/1.0.0", + CreationInformation = new SpdxCreationInformation + { + Created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), + Creators = ["Tool: MyTool-1.0", "Organization: Example Corp"] + }, + Packages = + [ + new SpdxPackage + { + Id = "SPDXRef-Package", + Name = "MyPackage", + Version = "1.0.0", + DownloadLocation = "https://example.com/package", + FilesAnalyzed = false, + LicenseConcluded = "MIT", + LicenseDeclared = "MIT", + CopyrightText = "Copyright (c) 2024 Example Corp" + } + ] +}; + +// Serialize to JSON +var json = Spdx2JsonSerializer.Serialize(document); +File.WriteAllText("output.spdx.json", json); +``` + +## Core Concepts + +### SPDX Elements + +SPDX documents consist of several key element types: + +- **Document**: The root element containing metadata and all other elements +- **Package**: Represents a software package or component +- **File**: Represents individual files within packages +- **Snippet**: Represents code snippets within files +- **Relationship**: Describes relationships between elements + +### SPDX Identifiers + +Every SPDX element must have a unique identifier within the document. Identifiers follow the +format `SPDXRef-{name}`: + +```csharp +var package = new SpdxPackage +{ + Id = "SPDXRef-MyPackage", + Name = "MyPackage" +}; +``` + +### Document Namespace + +Every SPDX document must have a unique namespace URI that identifies the document: + +```csharp +var document = new SpdxDocument +{ + DocumentNamespace = "https://example.com/my-software/1.0.0" +}; +``` + +The namespace should be unique for each version of your software. + +## Working with Documents + +### Document Properties + +A complete SPDX document includes: + +```csharp +var document = new SpdxDocument +{ + // Required fields + Id = "SPDXRef-DOCUMENT", + Name = "My Software SBOM", + Version = "SPDX-2.3", + DataLicense = "CC0-1.0", + DocumentNamespace = "https://example.com/my-software/1.0.0", + + // Creation information + CreationInformation = new SpdxCreationInformation + { + Created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), + Creators = + [ + "Tool: MyTool-1.0", + "Organization: Example Corp", + "Person: John Doe (john@example.com)" + ], + LicenseListVersion = "3.21" + }, + + // Optional fields + Comment = "This SBOM describes the software components", + + // Collections + Packages = [], + Files = [], + Snippets = [], + Relationships = [] +}; +``` + +### Document Describes Relationship + +Every SPDX document should have at least one DESCRIBES relationship indicating what the document +describes: + +```csharp +document.Relationships = +[ + new SpdxRelationship + { + Id = "SPDXRef-DOCUMENT", + RelationshipType = SpdxRelationshipType.Describes, + RelatedSpdxElement = "SPDXRef-RootPackage" + } +]; +``` + +## Working with Packages + +### Creating Packages + +A package represents a software component or library: + +```csharp +var package = new SpdxPackage +{ + // Required fields + Id = "SPDXRef-Package", + Name = "MyPackage", + DownloadLocation = "https://github.com/example/mypackage", + FilesAnalyzed = false, + + // Version information + Version = "1.0.0", + + // License information + LicenseConcluded = "MIT", + LicenseDeclared = "MIT", + LicenseComments = "This package is licensed under MIT", + + // Copyright information + CopyrightText = "Copyright (c) 2024 Example Corp", + + // Optional metadata + Summary = "A useful package for doing things", + Description = "This package provides functionality for...", + Homepage = "https://example.com/mypackage", + + // Source information + SourceInfo = "Built from commit abc123", + + // Package supplier and originator + Supplier = "Organization: Example Corp", + Originator = "Organization: Original Corp" +}; +``` + +### Package with Files + +When a package includes file information, set `FilesAnalyzed` to `true` and provide a verification +code: + +```csharp +var package = new SpdxPackage +{ + Id = "SPDXRef-Package", + Name = "MyPackage", + DownloadLocation = "https://github.com/example/mypackage", + FilesAnalyzed = true, + VerificationCode = new SpdxPackageVerificationCode + { + Value = "d6a770ba38583ed4bb4525bd96e50461655d2758", + ExcludedFiles = ["./package.spdx"] + } +}; + +// Add files and a relationship linking the package to those files +document.Files = +[ + new SpdxFile + { + Id = "SPDXRef-File1", + FileName = "./src/main.cs" + } +]; + +document.Relationships = +[ + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelationshipType = SpdxRelationshipType.Contains, + RelatedSpdxElement = "SPDXRef-File1" + } +]; +``` + +### External References + +Packages can include external references for security and package-manager metadata: + +```csharp +var package = new SpdxPackage +{ + Id = "SPDXRef-Package", + Name = "MyPackage", + ExternalReferences = + [ + new SpdxExternalReference + { + Category = SpdxReferenceCategory.Security, + Type = "cpe23Type", + Locator = "cpe:2.3:a:example:my-package:1.0.0:*:*:*:*:*:*:*" + }, + new SpdxExternalReference + { + Category = SpdxReferenceCategory.PackageManager, + Type = "purl", + Locator = "pkg:nuget/MyPackage@1.0.0" + } + ] +}; +``` + +## Working with Files + +### Creating Files + +Files represent individual files within packages: + +```csharp +var file = new SpdxFile +{ + // Required fields + Id = "SPDXRef-File1", + FileName = "./src/main.cs", + + // Checksums + Checksums = + [ + new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.SHA256, + Value = "abc123def456..." + } + ], + + // License information + LicenseConcluded = "MIT", + LicenseInfoInFiles = ["MIT"], + CopyrightText = "Copyright (c) 2024 Example Corp", + + // File type + FileTypes = [SpdxFileType.Source], + + // Optional fields + Comment = "Main application file" +}; +``` + +### File Checksums + +Include multiple checksum algorithms for stronger integrity verification: + +```csharp +var file = new SpdxFile +{ + Id = "SPDXRef-File1", + FileName = "./lib/library.dll", + Checksums = + [ + new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.SHA256, + Value = "abc123..." + }, + new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.SHA1, + Value = "def456..." + } + ] +}; +``` + +## Working with Relationships + +### Relationship Types + +SPDX defines many relationship types. Common ones include: + +- **DESCRIBES**: Document describes an element +- **CONTAINS**: Package contains files or other packages +- **DEPENDS_ON**: Package depends on another package +- **DEPENDENCY_OF**: Inverse of DEPENDS_ON +- **BUILD_DEPENDENCY_OF**: Build-time dependency +- **DEV_DEPENDENCY_OF**: Development dependency +- **GENERATED_FROM**: File generated from another file + +### Adding Relationships + +```csharp +using DemaConsulting.SpdxModel.Transform; + +// Add a relationship +var relationship = new SpdxRelationship +{ + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.DependsOn, + RelatedSpdxElement = "SPDXRef-Package2" +}; + +SpdxRelationships.Add(document, relationship); +``` + +### Finding Related Elements + +```csharp +// Get packages described by the document +var rootPackages = document.GetRootPackages(); +``` + +## Advanced Usage + +### Custom License References + +For licenses not in the SPDX license list, use a `LicenseRef-` identifier: + +```csharp +var document = new SpdxDocument +{ + // ... other fields ... + ExtractedLicensingInfo = + [ + new SpdxExtractedLicensingInfo + { + LicenseId = "LicenseRef-CustomLicense", + ExtractedText = "Full license text here...", + Name = "Custom License", + CrossReferences = ["https://example.com/license"] + } + ] +}; + +// Reference the custom license in a package +var package = new SpdxPackage +{ + Id = "SPDXRef-Package", + Name = "MyPackage", + LicenseConcluded = "LicenseRef-CustomLicense" +}; +``` + +### Document Annotations + +Add annotations to provide review or informational comments: + +```csharp +var annotation = new SpdxAnnotation +{ + Annotator = "Person: John Doe", + Date = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), + Type = SpdxAnnotationType.Review, + Comment = "This package has been reviewed for security concerns" +}; + +document.Annotations = [annotation]; +``` + +### Working with Snippets + +Snippets represent portions of files and carry their own license information: + +```csharp +var snippet = new SpdxSnippet +{ + Id = "SPDXRef-Snippet1", + SnippetFromFile = "SPDXRef-File1", + Name = "Authentication Function", + SnippetLineStart = 100, + SnippetLineEnd = 150, + LicenseConcluded = "MIT" +}; + +document.Snippets = [snippet]; +``` + +### Deep Copying Elements + +The library supports deep copying of any SPDX element or an entire document. This creates +a completely independent copy with no shared references to the original: + +```csharp +// Deep copy an entire document +var original = Spdx2JsonDeserializer.Deserialize(json); +var copy = original.DeepCopy(); + +// Deep copy individual elements +var packageCopy = package.DeepCopy(); +var fileCopy = file.DeepCopy(); +``` + +Deep copying is useful when you need to modify an element without affecting the original, +such as when merging SPDX documents or creating variants. + +### Comparing SPDX Elements + +The library provides equality comparers for all SPDX element types. Each element class +exposes a `Same` static comparer that checks all significant fields: + +```csharp +// Compare two documents for equality +bool areEqual = SpdxDocument.Same.Equals(doc1, doc2); + +// Compare packages +bool samePackage = SpdxPackage.Same.Equals(pkg1, pkg2); + +// Compare relationships (ignoring comment field) +bool sameRelationship = SpdxRelationship.Same.Equals(rel1, rel2); + +// Compare relationships by elements only (ignoring relationship type) +bool sameElements = SpdxRelationship.SameElements.Equals(rel1, rel2); +``` + +These comparers are used internally by the deep-copy verification logic and by the +`SpdxRelationships.Add` deduplication logic. + +### Validating SPDX Documents + +Call `Validate()` on any SPDX document to get a list of validation issues. An empty list +means the document is valid: + +```csharp +var issues = new List(); +document.Validate(issues); + +if (issues.Count == 0) +{ + Console.WriteLine("Document is valid."); +} +else +{ + Console.WriteLine("Validation issues found:"); + foreach (var issue in issues) + { + Console.WriteLine($" - {issue}"); + } +} +``` + +The recommended workflow is to validate after deserialization to catch malformed or +incomplete SPDX documents early, before processing them further. + +## Best Practices + +### Use Meaningful Identifiers + +Choose descriptive SPDX identifiers so documents are human-readable: + +```csharp +// Good +Id = "SPDXRef-Package-MyLibrary-1.0.0" + +// Not as good +Id = "SPDXRef-Package1" +``` + +### Include Complete License Information + +Always specify both concluded and declared licenses: + +```csharp +var package = new SpdxPackage +{ + LicenseConcluded = "MIT", // What you determined after analysis + LicenseDeclared = "MIT" // What the package itself declares +}; +``` + +### Use Checksums for Files + +Always include checksums for files when possible to support integrity verification: + +```csharp +var file = new SpdxFile +{ + FileName = "./app.exe", + Checksums = + [ + new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.SHA256, + Value = "..." + } + ] +}; +``` + +### Maintain Unique Document Namespaces + +Each version of your software should have a unique namespace to avoid conflicts: + +```csharp +// Good - includes version +DocumentNamespace = "https://example.com/myapp/1.0.0" + +// Not as good - no version +DocumentNamespace = "https://example.com/myapp" +``` + +### Include Creator Information + +Provide complete creator information to support audit and traceability: + +```csharp +CreationInformation = new SpdxCreationInformation +{ + Created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), + Creators = + [ + "Tool: MyTool-1.0", + "Organization: Example Corp", + "Person: Build System" + ] +}; +``` diff --git a/docs/verification/definition.yaml b/docs/verification/definition.yaml new file mode 100644 index 0000000..dbf7a98 --- /dev/null +++ b/docs/verification/definition.yaml @@ -0,0 +1,30 @@ +--- +resource-path: [docs/verification, docs/template] +input-files: + - docs/verification/title.txt + - docs/verification/introduction.md + - docs/verification/spdx-model.md + - docs/verification/spdx-model/spdx-annotation.md + - docs/verification/spdx-model/spdx-checksum.md + - docs/verification/spdx-model/spdx-creation-information.md + - docs/verification/spdx-model/spdx-document.md + - docs/verification/spdx-model/spdx-element.md + - docs/verification/spdx-model/spdx-external-document-reference.md + - docs/verification/spdx-model/spdx-external-reference.md + - docs/verification/spdx-model/spdx-extracted-licensing-info.md + - docs/verification/spdx-model/spdx-file.md + - docs/verification/spdx-model/spdx-helpers.md + - docs/verification/spdx-model/spdx-license-element.md + - docs/verification/spdx-model/spdx-package-verification-code.md + - docs/verification/spdx-model/spdx-package.md + - docs/verification/spdx-model/spdx-relationship.md + - docs/verification/spdx-model/spdx-snippet.md + - docs/verification/spdx-model/io/io.md + - docs/verification/spdx-model/io/spdx-constants.md + - docs/verification/spdx-model/io/spdx-2-json-serializer.md + - docs/verification/spdx-model/io/spdx-2-json-deserializer.md + - docs/verification/spdx-model/transform/transform.md + - docs/verification/spdx-model/transform/spdx-relationships.md +template: template.html +table-of-contents: true +number-sections: true diff --git a/docs/verification/introduction.md b/docs/verification/introduction.md new file mode 100644 index 0000000..0aec151 --- /dev/null +++ b/docs/verification/introduction.md @@ -0,0 +1,48 @@ +# Introduction + +This document describes how each software item in the SpdxModel library is verified. + +## Purpose + +This document provides the verification design for the SpdxModel library. For each local +software item — system, subsystems, and units — the document names the test scenarios that +verify the item's requirements. A reviewer can confirm coverage completeness by reading this +document without consulting test source code. + +## Scope + +The following items are in scope for this document: + +- SpdxModel system verification +- IO subsystem and unit verification +- Transform subsystem and unit verification +- All software unit verifications within the SpdxModel system + +The following items are out of scope: + +- OTS item verification: xUnit v3, ReqStream, BuildMark, VersionMark, SarifMark, SonarMark, + ReviewMark, FileAssert, Pandoc, WeasyPrint +- Test infrastructure and test helpers + +## Companion Artifact Structure + +The following companion artifacts are related to this verification document: + +- Requirements artifacts are located in `docs/reqstream/spdx-model/` +- Software design documents are located in `docs/design/spdx-model/` +- Production source code is located in `src/DemaConsulting.SpdxModel/` +- Automated test suite is located in `test/DemaConsulting.SpdxModel.Tests/` + +## Structural Deviation + +The companion artifact layout described in this document places subsystem verification files at +the subsystem level (e.g., `docs/verification/spdx-model/io/` for the IO subsystem). In practice, +subsystem verification files (`transform.md`, `io.md`) and their children are located inside their +respective subsystem subfolders rather than at the parent `spdx-model/` level. This deviation +mirrors the layout adopted in the design documentation and is accepted as a project-wide structural +deviation. Existing file references in review-sets and traceability tooling reflect the actual folder +layout and do not require updating. + +## References + +- [REF-1] SpdxModel releases, diff --git a/docs/verification/spdx-model.md b/docs/verification/spdx-model.md new file mode 100644 index 0000000..66cf42b --- /dev/null +++ b/docs/verification/spdx-model.md @@ -0,0 +1,66 @@ +# SpdxModel + +## Verification Approach + +The SpdxModel library is verified through automated unit and integration tests using the xUnit v3 +framework. Tests are organized in `test/DemaConsulting.SpdxModel.Tests/`. Unit tests verify +individual data model classes in isolation; integration tests verify the IO subsystem end-to-end +and the Transform subsystem with real document instances. No external dependencies are mocked. + +## Test Environment + +N/A - standard test environment. + +## Acceptance Criteria + +All automated tests pass with zero failures. + +## Test Scenarios + +**SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully**: Verifies that the library +successfully reads and parses an SPDX 2.2 example JSON document, returning a non-null +SpdxDocument without throwing exceptions. + +**SpdxModel_ReadSpdxJson_Spdx23Example_ParsesSuccessfully**: Verifies that the library +successfully reads and parses an SPDX 2.3 example JSON document, returning a non-null +SpdxDocument without throwing exceptions. + +**SpdxModel_ReadSpdxJson_Spdx22Example_PassesValidation**: Verifies that the document parsed +from the SPDX 2.2 example JSON passes all validation checks with no reported issues. + +**SpdxModel_ReadSpdxJson_Spdx23Example_PassesValidation**: Verifies that the document parsed +from the SPDX 2.3 example JSON passes all validation checks with no reported issues. + +**SpdxModel_ReadSpdxJson_Spdx23Example_RootPackagesIdentified**: Verifies that the root +packages are correctly identified in the SPDX 2.3 example document after parsing, confirming +DESCRIBES relationship traversal works as expected. + +**SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocument**: Verifies that a +deep copy of the parsed SPDX 2.3 document is structurally equal to the original, confirming +that all nested objects are fully duplicated. + +**SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds**: Verifies that an SPDX 2.3 +document serialized to JSON and then deserialized produces a document equivalent to the +original, confirming end-to-end serialization fidelity. + +**SpdxModel_Deserialize_MalformedJson_ThrowsJsonException**: Verifies that passing malformed +JSON to `Spdx2JsonDeserializer.Deserialize` throws a `JsonException` rather than returning a +partially-populated document. + +**SpdxModel_Validate_InvalidDocument_ReportsIssues**: Verifies that calling `Validate()` on a +deliberately incomplete SPDX document produces a non-empty issues list with the expected +validation error messages. + +**SpdxModel_FieldOptionality_RequiredFieldsNotNull_OptionalFieldsNullable**: Verifies that +required fields on key SPDX data model types are non-nullable string types with default empty +values, and that optional fields are nullable. + +**SpdxModel_Helpers_DateTimeValidation_IsObservableThroughDocumentModel**: Verifies that the +date-time validation utility (`SpdxHelpers.IsValidSpdxDateTime`) is exercised through the +document model by confirming that an invalid creation date is caught by document-level +validation. Linked from `SpdxModel-Data-Helpers`. + +**SpdxModel_Transform_AddRelationship_IsObservableThroughDocumentModel**: Verifies that the +`AddRelationship` transform utility correctly adds a new relationship to an SPDX document and +that the addition is observable through the document model's relationship collection. Linked +from `SpdxModel-Transform`. diff --git a/docs/verification/spdx-model/io/io.md b/docs/verification/spdx-model/io/io.md new file mode 100644 index 0000000..440011c --- /dev/null +++ b/docs/verification/spdx-model/io/io.md @@ -0,0 +1,34 @@ +### IO + +#### Verification Approach + +The IO subsystem is verified through automated integration tests using the xUnit v3 framework. +Tests are located in `test/DemaConsulting.SpdxModel.Tests/IO/SpdxModelIOTests.cs`. Integration +tests verify the IO subsystem end-to-end using real JSON input and output. +System.Text.Json is not mocked as JSON parsing is part of the verification scope. +Round-trip tests serialize and then deserialize documents to confirm fidelity. +Element-level field preservation is verified by the unit-level IO tests (Spdx2JsonDeserializer +and Spdx2JsonSerializer test classes). The subsystem-level tests verify end-to-end document +fidelity only. + +#### Test Environment + +N/A - standard test environment. + +#### Acceptance Criteria + +All integration tests pass with zero failures. + +#### Test Scenarios + +**SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidDocument**: Verifies +that an SPDX 2.2 document read from JSON and then written back to JSON and re-read produces +a document that passes all validation checks. + +**SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidDocument**: Verifies +that an SPDX 2.3 document read from JSON and then written back to JSON and re-read produces +a document that passes all validation checks. + +**SpdxModelIO_ReadSpdxJson_InvalidJson_ThrowsJsonException**: Verifies that passing malformed +JSON to `Spdx2JsonDeserializer.Deserialize` throws a `JsonException`, confirming that the +subsystem correctly propagates fatal parsing errors to callers. diff --git a/docs/verification/spdx-model/io/spdx-2-json-deserializer.md b/docs/verification/spdx-model/io/spdx-2-json-deserializer.md new file mode 100644 index 0000000..7887ae4 --- /dev/null +++ b/docs/verification/spdx-model/io/spdx-2-json-deserializer.md @@ -0,0 +1,142 @@ +### Spdx2JsonDeserializer + +#### Verification Approach + +Spdx2JsonDeserializer is verified through automated unit tests using the xUnit v3 framework. +Tests are located in the IO subdirectory under +`test/DemaConsulting.SpdxModel.Tests/`. Each test provides representative JSON input and +verifies the deserialized SPDX model objects match the expected values. System.Text.Json +is not mocked as JSON parsing is part of the verification scope. + +#### Test Environment + +N/A - standard test environment. + +#### Acceptance Criteria + +All automated tests pass with zero failures. + +#### Test Scenarios + +**Spdx2JsonDeserializer_Deserialize_ValidSpdx22Json_ReturnsExpectedDocument**: Verifies that +a complete SPDX 2.2 JSON document is deserialized to a fully populated SpdxDocument with +all fields correctly mapped. +This scenario is tested by +`Spdx2JsonDeserializer_Deserialize_ValidSpdx22Json_ReturnsExpectedDocument`. + +**Spdx2JsonDeserializer_Deserialize_ValidSpdx23Json_ReturnsExpectedDocument**: Verifies that +a complete SPDX 2.3 JSON document is deserialized to a fully populated SpdxDocument with +all fields correctly mapped. +This scenario is tested by +`Spdx2JsonDeserializer_Deserialize_ValidSpdx23Json_ReturnsExpectedDocument`. + +**Spdx2JsonDeserializer_DeserializeAnnotation_ValidInput_CorrectResults**: Verifies that a single +annotation JSON object is deserialized to an SpdxAnnotation with all fields correctly +populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeAnnotation_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeAnnotations_ValidInput_CorrectResults**: Verifies that a JSON array +of annotation objects is deserialized to a collection of SpdxAnnotation instances. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeAnnotations_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeChecksum_ValidInput_CorrectResults**: Verifies that a single checksum +JSON object is deserialized to an SpdxChecksum with algorithm and value fields correctly +populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeChecksum_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeChecksums_ValidInput_CorrectResults**: Verifies that a JSON array of +checksum objects is deserialized to a collection of SpdxChecksum instances. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeChecksums_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeCreationInformation_ValidInput_CorrectResults**: Verifies that the +creationInfo JSON object is deserialized to an SpdxCreationInformation with all fields +correctly populated. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeCreationInformation_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeDocument_ValidInput_CorrectResults**: Verifies that the top-level +document JSON fields are deserialized to the SpdxDocument with all document-level fields +correctly populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeDocument_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeExternalDocumentReference_ValidInput_CorrectResults**: Verifies that +a single external document reference JSON object is deserialized to an +SpdxExternalDocumentReference with all fields correctly populated. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeExternalDocumentReference_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_ValidInput_CorrectResults**: Verifies that +a JSON array of external document reference objects is deserialized to a collection of +SpdxExternalDocumentReference instances. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeExternalReference_ValidInput_CorrectResults**: Verifies that a single +external reference JSON object is deserialized to an SpdxExternalReference with category, +type, and locator fields correctly populated. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeExternalReference_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeExternalReferences_ValidInput_CorrectResults**: Verifies that a JSON +array of external reference objects is deserialized to a collection of SpdxExternalReference +instances. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeExternalReferences_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_ValidInput_CorrectResults**: Verifies that a +single extracted licensing info JSON object is deserialized to an SpdxExtractedLicensingInfo +with all fields correctly populated. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_ValidInput_CorrectResults**: Verifies that a +JSON array of extracted licensing info objects is deserialized to a collection of +SpdxExtractedLicensingInfo instances. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeFile_ValidInput_CorrectResults**: Verifies that a single file JSON +object is deserialized to an SpdxFile with all fields correctly populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeFile_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeFiles_ValidInput_CorrectResults**: Verifies that a JSON array of file +objects is deserialized to a collection of SpdxFile instances. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeFiles_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializePackage_ValidInput_CorrectResults**: Verifies that a single package +JSON object is deserialized to an SpdxPackage with all fields correctly populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializePackage_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializePackages_ValidInput_CorrectResults**: Verifies that a JSON array of +package objects is deserialized to a collection of SpdxPackage instances. +This scenario is tested by `Spdx2JsonDeserializer_DeserializePackages_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializePackageVerificationCode_ValidInput_CorrectResults**: Verifies that a +package verification code JSON object is deserialized to an SpdxPackageVerificationCode with +all fields correctly populated. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializePackageVerificationCode_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeRelationship_ValidInput_CorrectResults**: Verifies that a single +relationship JSON object is deserialized to an SpdxRelationship with all fields correctly +populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeRelationship_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeRelationships_ValidInput_CorrectResults**: Verifies that a JSON array +of relationship objects is deserialized to a collection of SpdxRelationship instances. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeRelationships_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeSnippet_ValidInput_CorrectResults**: Verifies that a single snippet +JSON object is deserialized to an SpdxSnippet with byte ranges and line ranges correctly +populated. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeSnippet_ValidInput_CorrectResults`. + +**Spdx2JsonDeserializer_DeserializeSnippet_WithoutLineRanges_DefaultsToZero**: Verifies that +when a snippet JSON object omits line range fields, the deserialized SpdxSnippet defaults +those fields to zero. +This scenario is tested by +`Spdx2JsonDeserializer_DeserializeSnippet_WithoutLineRanges_DefaultsToZero`. + +**Spdx2JsonDeserializer_DeserializeSnippets_ValidInput_CorrectResults**: Verifies that a JSON array of +snippet objects is deserialized to a collection of SpdxSnippet instances. +This scenario is tested by `Spdx2JsonDeserializer_DeserializeSnippets_ValidInput_CorrectResults`. diff --git a/docs/verification/spdx-model/io/spdx-2-json-serializer.md b/docs/verification/spdx-model/io/spdx-2-json-serializer.md new file mode 100644 index 0000000..3352121 --- /dev/null +++ b/docs/verification/spdx-model/io/spdx-2-json-serializer.md @@ -0,0 +1,142 @@ +### Spdx2JsonSerializer + +#### Verification Approach + +Spdx2JsonSerializer is verified through automated unit tests using the xUnit v3 framework. +Tests are located in the IO subdirectory under +`test/DemaConsulting.SpdxModel.Tests/`. Each test constructs the relevant SPDX model +objects directly and verifies the serialized JSON output. System.Text.Json is not mocked +as JSON output is part of the verification scope. + +#### Test Environment + +N/A - standard test environment. + +#### Acceptance Criteria + +All automated tests pass with zero failures. + +#### Test Scenarios + +**Spdx2JsonSerializer_SerializeAnnotation_ValidInput_CorrectResults**: Verifies that a single +SpdxAnnotation is serialized to the expected JSON structure with all fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeAnnotation_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeAnnotations_ValidInput_CorrectResults**: Verifies that a collection of +SpdxAnnotation instances is serialized to the expected JSON array structure. +This scenario is tested by `Spdx2JsonSerializer_SerializeAnnotations_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeAnnotation_NoId_OmitsSpdxId**: Verifies that when an +`SpdxAnnotation` has an empty `Id` string, the serializer omits the `SPDXID` field from the +JSON output while still emitting all other annotation fields (annotator, annotationDate, +annotationType, comment). +This scenario is tested by `Spdx2JsonSerializer_SerializeAnnotation_NoId_OmitsSpdxId`. + +**Spdx2JsonSerializer_SerializeChecksum_ValidInput_CorrectResults**: Verifies that a single SpdxChecksum +is serialized to the expected JSON structure with algorithm and value fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeChecksum_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeChecksums_ValidInput_CorrectResults**: Verifies that a collection of +SpdxChecksum instances is serialized to the expected JSON array structure. +This scenario is tested by `Spdx2JsonSerializer_SerializeChecksums_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeCreationInformation_ValidInput_CorrectResults**: Verifies that +SpdxCreationInformation is serialized to the expected JSON structure with all creation fields +correctly mapped. +This scenario is tested by +`Spdx2JsonSerializer_SerializeCreationInformation_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeDocument_ValidInput_CorrectResults**: Verifies that the top-level +SpdxDocument structure (excluding nested collections) is serialized to the expected JSON +with all document-level fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeDocument_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_Serialize_ValidInput_CorrectResults**: Verifies that a complete SpdxDocument +including all nested packages, files, snippets, and relationships is serialized to the +expected JSON output. +This scenario is tested by `Spdx2JsonSerializer_Serialize_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeExternalDocumentReference_ValidInput_CorrectResults**: Verifies that a +single SpdxExternalDocumentReference is serialized to the expected JSON structure. +This scenario is tested by +`Spdx2JsonSerializer_SerializeExternalDocumentReference_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeExternalDocumentReferences_ValidInput_CorrectResults**: Verifies that a +collection of SpdxExternalDocumentReference instances is serialized to the expected JSON +array. +This scenario is tested by +`Spdx2JsonSerializer_SerializeExternalDocumentReferences_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeExternalReference_ValidInput_CorrectResults**: Verifies that a single +SpdxExternalReference is serialized to the expected JSON structure with category, type, and +locator fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeExternalReference_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeExternalReferences_ValidInput_CorrectResults**: Verifies that a +collection of SpdxExternalReference instances is serialized to the expected JSON array. +This scenario is tested by `Spdx2JsonSerializer_SerializeExternalReferences_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeExtractedLicensingInfo_ValidInput_CorrectResults**: Verifies that a +single SpdxExtractedLicensingInfo is serialized to the expected JSON structure. +This scenario is tested by +`Spdx2JsonSerializer_SerializeExtractedLicensingInfo_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeExtractedLicensingInfos_ValidInput_CorrectResults**: Verifies that a +collection of SpdxExtractedLicensingInfo instances is serialized to the expected JSON array. +This scenario is tested by +`Spdx2JsonSerializer_SerializeExtractedLicensingInfos_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeFile_ValidInput_CorrectResults**: Verifies that a single SpdxFile is +serialized to the expected JSON structure with all file fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeFile_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeFiles_ValidInput_CorrectResults**: Verifies that a collection of SpdxFile +instances is serialized to the expected JSON array. +This scenario is tested by `Spdx2JsonSerializer_SerializeFiles_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializePackage_ValidInput_CorrectResults**: Verifies that a single SpdxPackage +is serialized to the expected JSON structure with all package fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializePackage_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializePackages_ValidInput_CorrectResults**: Verifies that a collection of +SpdxPackage instances is serialized to the expected JSON array. +This scenario is tested by `Spdx2JsonSerializer_SerializePackages_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeVerificationCode_ValidInput_CorrectResults**: Verifies that an +SpdxPackageVerificationCode is serialized to the expected JSON structure. +This scenario is tested by +`Spdx2JsonSerializer_SerializeVerificationCode_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeRelationship_ValidInput_CorrectResults**: Verifies that a single +SpdxRelationship is serialized to the expected JSON structure with all relationship fields +correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeRelationship_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeRelationships_ValidInput_CorrectResults**: Verifies that a collection of +SpdxRelationship instances is serialized to the expected JSON array. +This scenario is tested by `Spdx2JsonSerializer_SerializeRelationships_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeSnippet_ValidInput_CorrectResults**: Verifies that a single SpdxSnippet +is serialized to the expected JSON structure with byte ranges and line ranges correctly +mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeSnippet_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeSnippets_ValidInput_CorrectResults**: Verifies that a collection of +SpdxSnippet instances is serialized to the expected JSON array. +This scenario is tested by `Spdx2JsonSerializer_SerializeSnippets_ValidInput_CorrectResults`. + +**Spdx2JsonSerializer_SerializeSnippet_WithAnnotation_IncludesAnnotation**: Verifies that +when an `SpdxSnippet` includes an annotations array, the serialized JSON output contains +the `annotations` array with all annotation fields correctly mapped. +This scenario is tested by `Spdx2JsonSerializer_SerializeSnippet_WithAnnotation_IncludesAnnotation`. + +**Spdx2JsonSerializer_SerializeSnippet_NoLineRange_EmitsByteRangeOnly**: Verifies that when +a snippet has both `SnippetLineStart` and `SnippetLineEnd` set to zero, the serialized +`ranges` array contains only the byte-range entry and no line-range entry is emitted. +This scenario is tested by `Spdx2JsonSerializer_SerializeSnippet_NoLineRange_EmitsByteRangeOnly`. + +**Spdx2JsonSerializer_SerializeSnippet_PartialLineRange_EmitsByteRangeOnly**: Verifies that +when a snippet has only one of `SnippetLineStart` or `SnippetLineEnd` set to a non-zero +value (AND logic), the serialized `ranges` array contains only the byte-range entry — a +partial line range is not emitted. +This scenario is tested by `Spdx2JsonSerializer_SerializeSnippet_PartialLineRange_EmitsByteRangeOnly`. diff --git a/docs/verification/spdx-model/io/spdx-constants.md b/docs/verification/spdx-model/io/spdx-constants.md new file mode 100644 index 0000000..14f3d8d --- /dev/null +++ b/docs/verification/spdx-model/io/spdx-constants.md @@ -0,0 +1,22 @@ +### SpdxConstants + +#### Verification Approach + +SpdxConstants defines string constants used by the Spdx2JsonDeserializer and +Spdx2JsonSerializer units. No dedicated unit test file exists for SpdxConstants. The +correctness of these constants is verified implicitly through the IO round-trip and unit +tests for the deserializer and serializer. + +#### Test Environment + +N/A - standard test environment. + +#### Acceptance Criteria + +All automated tests for Spdx2JsonDeserializer and Spdx2JsonSerializer pass with zero +failures. + +#### Test Scenarios + +N/A — SpdxConstants defines string constants verified implicitly through +`Spdx2JsonDeserializer` and `Spdx2JsonSerializer` unit tests. diff --git a/docs/verification/spdx-model/spdx-annotation.md b/docs/verification/spdx-model/spdx-annotation.md new file mode 100644 index 0000000..6890ee2 --- /dev/null +++ b/docs/verification/spdx-model/spdx-annotation.md @@ -0,0 +1,72 @@ +## SpdxAnnotation + +### Verification Approach + +SpdxAnnotation is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxAnnotationTests.cs`. Each test constructs +an SpdxAnnotation instance directly and exercises the method under test with no mocked +dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxAnnotation_SameComparer_ComparesCorrectly**: Verifies that SameComparer correctly +identifies two SpdxAnnotation instances as equal when all fields match and as distinct when +any field differs. +This scenario is tested by `SpdxAnnotation_SameComparer_ComparesCorrectly`. + +**SpdxAnnotation_DeepCopy_CreatesEqualButDistinctInstance**: Verifies that a deep copy +produces a new SpdxAnnotation instance with equal field values but a distinct object reference, +confirming no shared state between original and copy. +This scenario is tested by `SpdxAnnotation_DeepCopy_CreatesEqualButDistinctInstance`. + +**SpdxAnnotation_Enhance_AddsOrUpdatesInformationCorrectly**: Verifies that Enhance merges +annotation data by adding missing fields from the source while preserving existing field +values on the target. +This scenario is tested by `SpdxAnnotation_Enhance_AddsOrUpdatesInformationCorrectly`. + +**SpdxAnnotation_Validate_InvalidAnnotator**: Verifies that validation reports an issue when +the Annotator field is missing or does not conform to the required format. +This scenario is tested by `SpdxAnnotation_Validate_InvalidAnnotator`. + +**SpdxAnnotation_Validate_InvalidDate**: Verifies that validation reports an issue when the +annotation date is missing or does not conform to ISO 8601 date-time format. +This scenario is tested by `SpdxAnnotation_Validate_InvalidDate`. + +**SpdxAnnotation_Validate_InvalidType**: Verifies that validation reports an issue when the +annotation type is set to an unrecognized or unsupported value. +This scenario is tested by `SpdxAnnotation_Validate_InvalidType`. + +**SpdxAnnotation_Validate_InvalidComment**: Verifies that validation reports an issue when the +annotation comment is missing or empty. +This scenario is tested by `SpdxAnnotation_Validate_InvalidComment`. + +**SpdxAnnotationTypeExtensions_FromText_Valid**: Verifies that FromText correctly parses a +recognized annotation type string to its corresponding enum value. +This scenario is tested by `SpdxAnnotationTypeExtensions_FromText_Valid`. + +**SpdxAnnotationTypeExtensions_FromText_Invalid**: Verifies that `FromText` throws +`InvalidOperationException` with the message `"Unsupported SPDX Annotation Type 'invalid'"` when +given an unrecognized annotation type string. +This scenario is tested by `SpdxAnnotationTypeExtensions_FromText_Invalid`. + +**SpdxAnnotationTypeExtensions_ToText_Valid**: Verifies that ToText correctly converts a +recognized annotation type enum value to its SPDX text representation. +This scenario is tested by `SpdxAnnotationTypeExtensions_ToText_Valid`. + +**SpdxAnnotationTypeExtensions_ToText_Invalid**: Verifies that ToText always throws +`InvalidOperationException` when given an unknown or unsupported annotation type enum value. +The method never returns an empty string for an invalid input. +This scenario is tested by `SpdxAnnotationTypeExtensions_ToText_Invalid`. + +**SpdxAnnotationTypeExtensions_ToText_Missing**: Verifies that calling +`SpdxAnnotationType.Missing.ToText()` throws `InvalidOperationException` with the message +"Attempt to serialize missing SPDX Annotation Type". +This scenario is tested by `SpdxAnnotationTypeExtensions_ToText_Missing`. diff --git a/docs/verification/spdx-model/spdx-checksum.md b/docs/verification/spdx-model/spdx-checksum.md new file mode 100644 index 0000000..811c00f --- /dev/null +++ b/docs/verification/spdx-model/spdx-checksum.md @@ -0,0 +1,88 @@ +## SpdxChecksum + +### Verification Approach + +SpdxChecksum is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxChecksumTests.cs`. Each test constructs +an SpdxChecksum instance directly and exercises the method under test with no mocked +dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxChecksum_SameComparer_SameOrDifferentValues_ReturnsCorrectEquality**: Verifies that +SameComparer correctly identifies two SpdxChecksum instances as equal when algorithm and +value both match, and as distinct when either field differs. +This scenario is tested by `SpdxChecksum_SameComparer_SameOrDifferentValues_ReturnsCorrectEquality`. + +**SpdxChecksum_SameComparer_NullFirstArgument_ReturnsFalse**: Verifies that the Same comparer +returns false when the first argument is null. +This scenario is tested by `SpdxChecksum_SameComparer_NullFirstArgument_ReturnsFalse`. + +**SpdxChecksum_SameComparer_NullSecondArgument_ReturnsFalse**: Verifies that the Same comparer +returns false when the second argument is null. +This scenario is tested by `SpdxChecksum_SameComparer_NullSecondArgument_ReturnsFalse`. + +**SpdxChecksum_SameComparer_BothArgumentsNull_ReturnsTrue**: Verifies that the Same comparer +returns true when both arguments are null. +This scenario is tested by `SpdxChecksum_SameComparer_BothArgumentsNull_ReturnsTrue`. + +**SpdxChecksum_DeepCopy_PopulatedChecksum_CreatesEqualButDistinctInstance**: Verifies that a +deep copy produces a new SpdxChecksum instance with equal field values but a distinct object +reference. +This scenario is tested by `SpdxChecksum_DeepCopy_PopulatedChecksum_CreatesEqualButDistinctInstance`. + +**SpdxChecksum_Enhance_ExistingAndNewAlgorithms_AddsOrUpdatesInformation**: Verifies that +Enhance merges checksum data by adding missing fields from the source while preserving existing +field values on the target. +This scenario is tested by `SpdxChecksum_Enhance_ExistingAndNewAlgorithms_AddsOrUpdatesInformation`. + +**SpdxChecksum_Validate_MissingAlgorithm_ReportsAlgorithmIssue**: Verifies that validation +reports an issue when the checksum algorithm field is set to Missing. +This scenario is tested by `SpdxChecksum_Validate_MissingAlgorithm_ReportsAlgorithmIssue`. + +**SpdxChecksum_Validate_EmptyValue_ReportsValueIssue**: Verifies that validation reports an +issue when the checksum value field is empty. +This scenario is tested by `SpdxChecksum_Validate_EmptyValue_ReportsValueIssue`. + +**SpdxChecksum_Validate_UnknownNumericAlgorithm_ReportsAlgorithmIssue**: Verifies that +validation reports an issue when the checksum algorithm field holds a numeric value that is +not a named member of the SpdxChecksumAlgorithm enumeration. +This scenario is tested by `SpdxChecksum_Validate_UnknownNumericAlgorithm_ReportsAlgorithmIssue`. + +**SpdxChecksumAlgorithmExtensions_FromText_KnownAlgorithmStrings_ReturnsCorrectEnumValues**: +Verifies that FromText correctly parses all recognized checksum algorithm strings +(case-insensitive) to their corresponding enum values. +This scenario is tested by `SpdxChecksumAlgorithmExtensions_FromText_KnownAlgorithmStrings_ReturnsCorrectEnumValues`. + +**SpdxChecksumAlgorithmExtensions_FromText_UnknownAlgorithmString_ThrowsInvalidOperationException**: +Verifies that FromText throws `InvalidOperationException` when given an unrecognized checksum +algorithm string. +This scenario is tested by `SpdxChecksumAlgorithmExtensions_FromText_UnknownAlgorithmString_ThrowsInvalidOperationException`. + +**SpdxChecksumAlgorithmExtensions_FromText_EmptyString_ReturnsMissing**: Verifies that FromText +returns `Missing` when given an empty string. +This scenario is tested by `SpdxChecksumAlgorithmExtensions_FromText_EmptyString_ReturnsMissing`. + +**SpdxChecksumAlgorithmExtensions_ToText_KnownAlgorithmEnums_ReturnsCorrectStrings**: Verifies +that ToText correctly converts all recognized checksum algorithm enum values to their SPDX text +representations. +This scenario is tested by `SpdxChecksumAlgorithmExtensions_ToText_KnownAlgorithmEnums_ReturnsCorrectStrings`. + +**SpdxChecksumAlgorithmExtensions_ToText_OutOfRangeEnum_ThrowsInvalidOperationException**: +Verifies that ToText throws `InvalidOperationException` when given a numeric enum value that +does not correspond to any named algorithm member (e.g., `(SpdxChecksumAlgorithm)1000`). +This scenario is tested by `SpdxChecksumAlgorithmExtensions_ToText_OutOfRangeEnum_ThrowsInvalidOperationException`. + +**SpdxChecksumAlgorithmExtensions_ToText_MissingAlgorithm_ThrowsInvalidOperationException**: +Verifies that ToText throws `InvalidOperationException` when given the `Missing` sentinel +value, which must never be serialized to SPDX text form. +This scenario is tested by +`SpdxChecksumAlgorithmExtensions_ToText_MissingAlgorithm_ThrowsInvalidOperationException`. diff --git a/docs/verification/spdx-model/spdx-creation-information.md b/docs/verification/spdx-model/spdx-creation-information.md new file mode 100644 index 0000000..12b20c3 --- /dev/null +++ b/docs/verification/spdx-model/spdx-creation-information.md @@ -0,0 +1,63 @@ +## SpdxCreationInformation + +### Verification Approach + +SpdxCreationInformation is verified through automated unit tests using the xUnit v3 framework. +Tests are located in +`test/DemaConsulting.SpdxModel.Tests/SpdxCreationInformationTests.cs`. Each test constructs +an SpdxCreationInformation instance directly and exercises the method under test with no +mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxCreationInformation_DeepCopy_WithAllFieldsPopulated_CreatesEqualButDistinctInstance**: Verifies that a deep +copy produces a new SpdxCreationInformation instance with equal field values but a distinct +object reference, confirming no shared state between original and copy. +This scenario is tested by +`SpdxCreationInformation_DeepCopy_WithAllFieldsPopulated_CreatesEqualButDistinctInstance`. + +**SpdxCreationInformation_Enhance_WithMissingFieldsInBase_AddsOrUpdatesInformationCorrectly**: Verifies that Enhance +merges creation information by adding missing fields from the source while preserving +existing field values on the target. +This scenario is tested by +`SpdxCreationInformation_Enhance_WithMissingFieldsInBase_AddsOrUpdatesInformationCorrectly`. + +**SpdxCreationInformation_Validate_ValidInformation_NoIssues**: Verifies that validation +reports no issues for a fully populated valid SpdxCreationInformation instance. +This scenario is tested by `SpdxCreationInformation_Validate_ValidInformation_NoIssues`. + +**SpdxCreationInformation_Validate_MissingCreators_ReportsIssue**: Verifies that validation reports an +issue when the Creators list is empty or absent. +This scenario is tested by `SpdxCreationInformation_Validate_MissingCreators_ReportsIssue`. + +**SpdxCreationInformation_Validate_InvalidCreator_ReportsIssue**: Verifies that validation reports an issue +when one or more entries in the Creators list do not conform to the required tool or +organization format. +This scenario is tested by `SpdxCreationInformation_Validate_InvalidCreator_ReportsIssue`. + +**SpdxCreationInformation_Validate_InvalidCreatedDate_ReportsIssue**: Verifies that validation reports an +issue when the Created timestamp is missing or does not conform to ISO 8601 date-time format. +This scenario is tested by `SpdxCreationInformation_Validate_InvalidCreatedDate_ReportsIssue`. + +**SpdxCreationInformation_Validate_InvalidVersion_ReportsIssue**: Verifies that validation reports an issue +when the LicenseListVersion field is present but does not conform to the expected version +format. +This scenario is tested by `SpdxCreationInformation_Validate_InvalidVersion_ReportsIssue`. + +**SpdxCreationInformation_Validate_EmptyCreatedField_NoDateIssue**: Verifies that validation +does not report a date issue when the Created field is empty (empty is a permitted value for +partially-constructed documents). +This scenario is tested by `SpdxCreationInformation_Validate_EmptyCreatedField_NoDateIssue`. + +**SpdxCreationInformation_Enhance_DuplicateCreators_DeduplicatesCreators**: Verifies that +Enhance deduplicates the Creators array when merging, ensuring no duplicate entries appear in +the result. +This scenario is tested by `SpdxCreationInformation_Enhance_DuplicateCreators_DeduplicatesCreators`. diff --git a/docs/verification/spdx-model/spdx-document.md b/docs/verification/spdx-model/spdx-document.md new file mode 100644 index 0000000..5d3324c --- /dev/null +++ b/docs/verification/spdx-model/spdx-document.md @@ -0,0 +1,94 @@ +## SpdxDocument + +### Verification Approach + +SpdxDocument is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxDocumentTests.cs`. Each test constructs +an SpdxDocument instance directly and exercises the method under test with no mocked +dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxDocument_GetRootPackages_WithDescribesAndRelationships_ReturnsCorrectPackages**: Verifies that +GetRootPackages returns only the packages that are the targets of DESCRIBES relationships from the +document element. +This scenario is tested by `SpdxDocument_GetRootPackages_WithDescribesAndRelationships_ReturnsCorrectPackages`. + +**SpdxDocument_Same_DocumentsWithMatchingRootPackages_AreEqual**: Verifies that SameComparer correctly +identifies two SpdxDocument instances as equal when all fields match and as distinct when any +field differs. +This scenario is tested by `SpdxDocument_Same_DocumentsWithMatchingRootPackages_AreEqual`. + +**SpdxDocument_DeepCopy_WithPopulatedDocument_CreatesEqualButDistinctInstance**: Verifies that a deep copy produces +a new SpdxDocument instance with equal field values but a distinct object reference, including +all nested packages, files, snippets, and relationships. +This scenario is tested by `SpdxDocument_DeepCopy_WithPopulatedDocument_CreatesEqualButDistinctInstance`. + +**SpdxDocument_Validate_ValidDocument_ReportsNoIssues**: Verifies that a fully populated valid SpdxDocument passes +all validation checks without reporting any issues. +This scenario is tested by `SpdxDocument_Validate_ValidDocument_ReportsNoIssues`. + +**SpdxDocument_Validate_InvalidId_ReportsIssue**: Verifies that validation reports an issue when the +document SPDX-ID field is missing or does not conform to the required format. +This scenario is tested by `SpdxDocument_Validate_InvalidId_ReportsIssue`. + +**SpdxDocument_Validate_InvalidName_ReportsIssue**: Verifies that validation reports an issue when the +document name field is missing or empty. +This scenario is tested by `SpdxDocument_Validate_InvalidName_ReportsIssue`. + +**SpdxDocument_Validate_InvalidVersion_ReportsIssue**: Verifies that validation reports an issue when the +SPDX version field is missing or does not match the expected SPDX-2.x format. +This scenario is tested by `SpdxDocument_Validate_InvalidVersion_ReportsIssue`. + +**SpdxDocument_Validate_InvalidDataLicense_ReportsIssue**: Verifies that validation reports an issue when +the data license field is missing or is not set to the required CC0-1.0 value. +This scenario is tested by `SpdxDocument_Validate_InvalidDataLicense_ReportsIssue`. + +**SpdxDocument_Validate_InvalidNameSpace_ReportsIssue**: Verifies that validation reports an issue when the +document namespace field is missing or is not a valid URI. +This scenario is tested by `SpdxDocument_Validate_InvalidNameSpace_ReportsIssue`. + +**SpdxDocument_Validate_DuplicatePackageIds_ReportsIssue**: Verifies that validation reports an issue when +two or more packages in the document share the same SPDX-ID. +This scenario is tested by `SpdxDocument_Validate_DuplicatePackageIds_ReportsIssue`. + +**SpdxDocument_Validate_InvalidRelationship_ReportsIssue**: Verifies that validation reports an issue when +a relationship references an element ID that does not exist within the document. +This scenario is tested by `SpdxDocument_Validate_InvalidRelationship_ReportsIssue`. + +**SpdxDocument_Validate_NtiaMinimumElements_ReportsIssues**: Verifies that validation reports issues when the +document does not satisfy NTIA minimum element requirements for an SBOM. +This scenario is tested by `SpdxDocument_Validate_NtiaMinimumElements_ReportsIssues`. + +**SpdxDocument_GetAllElements_WithMixedElements_ReturnsAllNonRelationshipElements**: Verifies that +`GetAllElements` returns the combined collection of all packages, files, snippets, annotations, +and the document element itself, and that `SpdxRelationship` elements are excluded. +This scenario is tested by `SpdxDocument_GetAllElements_WithMixedElements_ReturnsAllNonRelationshipElements`. + +**SpdxDocument_GetElement_Document_ReturnsDocumentElement**: Verifies that GetElement returns +the document element when queried by the document SPDX-ID. +This scenario is tested by `SpdxDocument_GetElement_Document_ReturnsDocumentElement`. + +**SpdxDocument_GetElement_File_ReturnsFileElement**: Verifies that GetElement returns a file +element when queried by a file SPDX-ID. +This scenario is tested by `SpdxDocument_GetElement_File_ReturnsFileElement`. + +**SpdxDocument_GetElement_Package_ReturnsPackageElement**: Verifies that GetElement returns a +package element when queried by a package SPDX-ID. +This scenario is tested by `SpdxDocument_GetElement_Package_ReturnsPackageElement`. + +**SpdxDocument_GetElement_Snippet_ReturnsSnippetElement**: Verifies that GetElement returns a +snippet element when queried by a snippet SPDX-ID. +This scenario is tested by `SpdxDocument_GetElement_Snippet_ReturnsSnippetElement`. + +**SpdxDocument_Validate_InvalidAnnotation_ReportsIssue**: Verifies that validation reports an issue when +an annotation within the document contains invalid fields. +This scenario is tested by `SpdxDocument_Validate_InvalidAnnotation_ReportsIssue`. diff --git a/docs/verification/spdx-model/spdx-element.md b/docs/verification/spdx-model/spdx-element.md new file mode 100644 index 0000000..11a9af4 --- /dev/null +++ b/docs/verification/spdx-model/spdx-element.md @@ -0,0 +1,33 @@ +## SpdxElement + +### Verification Approach + +SpdxElement is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxElementTests.cs`. Each test exercises +the element ID validation logic directly with no mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxElement_Id_ValidFormat_PassesValidation**: Verifies that an element with a properly +formatted SPDX-ID (matching the SPDXRef- prefix pattern) passes validation without reporting +any issues. + +**SpdxElement_Id_InvalidFormat_ReportsValidationIssue**: Verifies that an element with a +malformed SPDX-ID (missing the SPDXRef- prefix or containing invalid characters) is reported +as a validation issue. + +### Methods Without Direct Test Scenarios + +**EnhanceElement**: `EnhanceElement` is a `protected` method and therefore cannot be invoked +directly from test code. Its behavior is verified indirectly through the `Enhance` method tests +of concrete subclasses (`SpdxAnnotation`, `SpdxChecksum`, `SpdxPackage`, etc.), each of which +exercises the inherited `EnhanceElement` call as part of their own `Enhance` validation. No +separate `SpdxElement`-level scenario is required. diff --git a/docs/verification/spdx-model/spdx-external-document-reference.md b/docs/verification/spdx-model/spdx-external-document-reference.md new file mode 100644 index 0000000..55b4c97 --- /dev/null +++ b/docs/verification/spdx-model/spdx-external-document-reference.md @@ -0,0 +1,50 @@ +## SpdxExternalDocumentReference + +### Verification Approach + +SpdxExternalDocumentReference is verified through automated unit tests using the xUnit v3 +framework. Tests are located in +`test/DemaConsulting.SpdxModel.Tests/SpdxExternalDocumentReferenceTests.cs`. Each test +constructs an SpdxExternalDocumentReference instance directly and exercises the method under +test with no mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxExternalDocumentReference_SameComparer_SameDocument_ReturnsEqual**: Verifies the comparer +considers two instances equal when their `Document` URI matches, and distinct when the URI +differs (regardless of `ExternalDocumentId`). +This scenario is tested by +`SpdxExternalDocumentReference_SameComparer_SameDocument_ReturnsEqual`. + +**SpdxExternalDocumentReference_DeepCopy_ValidInstance_ReturnsEqualButDistinctInstance**: Verifies that a +deep copy produces a new SpdxExternalDocumentReference with equal field values but a distinct +object reference. +This scenario is tested by +`SpdxExternalDocumentReference_DeepCopy_ValidInstance_ReturnsEqualButDistinctInstance`. + +**SpdxExternalDocumentReference_Enhance_WithNewAndMatchingEntries_MergesAndAppendsCorrectly**: Verifies that +Enhance merges external document reference data by adding missing fields from the source +while preserving existing values on the target. +This scenario is tested by +`SpdxExternalDocumentReference_Enhance_WithNewAndMatchingEntries_MergesAndAppendsCorrectly`. + +**SpdxExternalDocumentReference_Validate_MissingId_ReportsIssue**: Verifies that validation reports an +issue when the external document reference ID field is missing or empty. +This scenario is tested by `SpdxExternalDocumentReference_Validate_MissingId_ReportsIssue`. + +**SpdxExternalDocumentReference_Validate_MissingDocument_ReportsIssue**: Verifies that validation reports +an issue when the referenced external document URI is missing or empty. +This scenario is tested by `SpdxExternalDocumentReference_Validate_MissingDocument_ReportsIssue`. + +**SpdxExternalDocumentReference_Validate_InvalidChecksum_ReportsIssue**: Verifies that +validation reports an issue when the external document reference contains an invalid checksum +(missing algorithm or empty value). +This scenario is tested by `SpdxExternalDocumentReference_Validate_InvalidChecksum_ReportsIssue`. diff --git a/docs/verification/spdx-model/spdx-external-reference.md b/docs/verification/spdx-model/spdx-external-reference.md new file mode 100644 index 0000000..b9fad8c --- /dev/null +++ b/docs/verification/spdx-model/spdx-external-reference.md @@ -0,0 +1,68 @@ +## SpdxExternalReference + +### Verification Approach + +SpdxExternalReference is verified through automated unit tests using the xUnit v3 framework. +Tests are located in +`test/DemaConsulting.SpdxModel.Tests/SpdxExternalReferenceTests.cs`. Each test constructs +an SpdxExternalReference instance directly and exercises the method under test with no +mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxExternalReference_SameComparer_EqualAndUnequalInstances_ComparesCorrectly**: Verifies that SameComparer +correctly identifies two SpdxExternalReference instances as equal when all fields match and +as distinct when any field differs. +This scenario is tested by `SpdxExternalReference_SameComparer_EqualAndUnequalInstances_ComparesCorrectly`. + +**SpdxExternalReference_DeepCopy_WithAllFields_CreatesEqualButDistinctInstance**: Verifies that a deep copy +produces a new SpdxExternalReference with equal field values but a distinct object reference. +This scenario is tested by `SpdxExternalReference_DeepCopy_WithAllFields_CreatesEqualButDistinctInstance`. + +**SpdxExternalReference_Enhance_WithMatchingAndNewEntries_MergesCorrectly**: Verifies that Enhance +merges external reference data by adding missing fields from the source while preserving +existing values on the target. +This scenario is tested by +`SpdxExternalReference_Enhance_WithMatchingAndNewEntries_MergesCorrectly`. + +**SpdxExternalReference_Validate_InvalidCategory_ReportsIssue**: Verifies that validation reports an issue +when the reference category is `SpdxReferenceCategory.Missing`. +This scenario is tested by `SpdxExternalReference_Validate_InvalidCategory_ReportsIssue`. + +**SpdxExternalReference_Validate_InvalidType_ReportsIssue**: Verifies that validation reports an issue when +the reference type does not conform to the expected format for the given category. +This scenario is tested by `SpdxExternalReference_Validate_InvalidType_ReportsIssue`. + +**SpdxExternalReference_Validate_InvalidLocator_ReportsIssue**: Verifies that validation reports an issue +when the reference locator field is missing or empty. +This scenario is tested by `SpdxExternalReference_Validate_InvalidLocator_ReportsIssue`. + +**SpdxReferenceCategoryExtensions_FromText_ValidInput_ParsesCorrectly**: Verifies that FromText correctly parses a +recognized reference category string to its corresponding enum value. +This scenario is tested by `SpdxReferenceCategoryExtensions_FromText_ValidInput_ParsesCorrectly`. + +**SpdxReferenceCategoryExtensions_FromText_InvalidInput_ThrowsInvalidOperationException**: Verifies that `FromText` throws +`InvalidOperationException` with a message identifying the unsupported value when given an +unrecognized reference category string. +This scenario is tested by `SpdxReferenceCategoryExtensions_FromText_InvalidInput_ThrowsInvalidOperationException`. + +**SpdxReferenceCategoryExtensions_ToText_ValidReference_FormatsCorrectly**: Verifies that ToText correctly converts a +recognized reference category enum value to its SPDX text representation. +This scenario is tested by `SpdxReferenceCategoryExtensions_ToText_ValidReference_FormatsCorrectly`. + +**SpdxReferenceCategoryExtensions_ToText_InvalidCategory_ThrowsInvalidOperationException**: Verifies that ToText throws +`InvalidOperationException` with the unsupported-category message when called with an +unrecognized enum value. +This scenario is tested by `SpdxReferenceCategoryExtensions_ToText_InvalidCategory_ThrowsInvalidOperationException`. + +**SpdxReferenceCategoryExtensions_ToText_MissingCategory_ThrowsInvalidOperationException**: Verifies that `ToText` throws +`InvalidOperationException` with a specific message when called with `SpdxReferenceCategory.Missing`. +This scenario is tested by `SpdxReferenceCategoryExtensions_ToText_MissingCategory_ThrowsInvalidOperationException`. diff --git a/docs/verification/spdx-model/spdx-extracted-licensing-info.md b/docs/verification/spdx-model/spdx-extracted-licensing-info.md new file mode 100644 index 0000000..cf4c20c --- /dev/null +++ b/docs/verification/spdx-model/spdx-extracted-licensing-info.md @@ -0,0 +1,48 @@ +## SpdxExtractedLicensingInfo + +### Verification Approach + +SpdxExtractedLicensingInfo is verified through automated unit tests using the xUnit v3 +framework. Tests are located in +`test/DemaConsulting.SpdxModel.Tests/SpdxExtractedLicensingInfoTests.cs`. Each test +constructs an SpdxExtractedLicensingInfo instance directly and exercises the method under +test with no mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxExtractedLicensingInfo_SameComparer_ComparesCorrectly**: Verifies that SameComparer +correctly identifies two SpdxExtractedLicensingInfo instances as equal when all fields match +and as distinct when any field differs. +This scenario is tested by `SpdxExtractedLicensingInfo_SameComparer_ComparesCorrectly`. + +**SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance**: Verifies that a deep +copy produces a new SpdxExtractedLicensingInfo instance with equal field values but a +distinct object reference, including array independence for CrossReferences. +This scenario is tested by +`SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance`. + +**SpdxExtractedLicensingInfo_Enhance_AddsOrUpdatesInformationCorrectly**: Verifies that +Enhance merges extracted licensing information by adding missing fields from the source while +preserving existing values on the target. +This scenario is tested by +`SpdxExtractedLicensingInfo_Enhance_AddsOrUpdatesInformationCorrectly`. + +**SpdxExtractedLicensingInfo_Validate_ValidInput_ReturnsNoIssues**: Verifies that a valid +extracted licensing info with both LicenseId and ExtractedText populated returns no issues. +This scenario is tested by `SpdxExtractedLicensingInfo_Validate_ValidInput_ReturnsNoIssues`. + +**SpdxExtractedLicensingInfo_Validate_InvalidLicenseId_ReportsIssue**: Verifies that validation reports an +issue when the LicenseId field is empty. +This scenario is tested by `SpdxExtractedLicensingInfo_Validate_InvalidLicenseId_ReportsIssue`. + +**SpdxExtractedLicensingInfo_Validate_InvalidExtractedText_ReportsIssue**: Verifies that validation reports +an issue when the extracted license text field is missing or empty. +This scenario is tested by `SpdxExtractedLicensingInfo_Validate_InvalidExtractedText_ReportsIssue`. diff --git a/docs/verification/spdx-model/spdx-file.md b/docs/verification/spdx-model/spdx-file.md new file mode 100644 index 0000000..ae43866 --- /dev/null +++ b/docs/verification/spdx-model/spdx-file.md @@ -0,0 +1,65 @@ +## SpdxFile + +### Verification Approach + +SpdxFile is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxFileTests.cs`. Each test constructs an +SpdxFile instance directly and exercises the method under test with no mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxFile_SameComparer_MatchingAndDistinctFiles_ComparesCorrectly**: Verifies that SameComparer correctly identifies +two SpdxFile instances as equal when all fields match and as distinct when any field differs. +This scenario is tested by `SpdxFile_SameComparer_MatchingAndDistinctFiles_ComparesCorrectly`. + +**SpdxFile_DeepCopy_FullyPopulatedFile_CreatesEqualButDistinctCopy**: Verifies that a deep copy produces a +new SpdxFile instance with equal field values but a distinct object reference, including all +nested checksums and annotations. +This scenario is tested by `SpdxFile_DeepCopy_FullyPopulatedFile_CreatesEqualButDistinctCopy`. + +**SpdxFile_Enhance_MatchingAndNewFiles_MergesCorrectly**: Verifies that Enhance merges file +data by adding missing fields from the source while preserving existing field values on the +target. +This scenario is tested by `SpdxFile_Enhance_MatchingAndNewFiles_MergesCorrectly`. + +**SpdxFile_Validate_InvalidFileId_ReportsIssue**: Verifies that validation reports an issue when +the file SPDX-ID does not conform to the required SPDXRef- prefix format. +This scenario is tested by `SpdxFile_Validate_InvalidFileId_ReportsIssue`. + +**SpdxFile_Validate_InvalidFileName_ReportsIssue**: Verifies that validation reports an issue when +the FileName does not start with the required "./" prefix. +This scenario is tested by `SpdxFile_Validate_InvalidFileName_ReportsIssue`. + +**SpdxFile_Validate_MissingSha1Checksum_ReportsIssue**: Verifies that validation reports an +issue when no SHA1 checksum is present in the Checksums array. +This scenario is tested by `SpdxFile_Validate_MissingSha1Checksum_ReportsIssue`. + +**SpdxFile_Validate_ValidFile_ReportsNoIssues**: Verifies that a fully populated valid SpdxFile passes all +validation checks without reporting any issues. +This scenario is tested by `SpdxFile_Validate_ValidFile_ReportsNoIssues`. + +**SpdxFileTypeExtensions_FromText_ValidInput_ParsesCorrectly**: Verifies that FromText correctly parses a +recognized file type string to its corresponding enum value, and that matching is +case-insensitive. +This scenario is tested by `SpdxFileTypeExtensions_FromText_ValidInput_ParsesCorrectly`. + +**SpdxFileTypeExtensions_FromText_InvalidInput_ThrowsException**: Verifies that `FromText` throws +`InvalidOperationException` with a message identifying the unsupported value when given an +unrecognized file type string. +This scenario is tested by `SpdxFileTypeExtensions_FromText_InvalidInput_ThrowsException`. + +**SpdxFileTypeExtensions_ToText_ValidEnum_FormatsCorrectly**: Verifies that ToText correctly converts a recognized +file type enum value to its SPDX text representation. +This scenario is tested by `SpdxFileTypeExtensions_ToText_ValidEnum_FormatsCorrectly`. + +**SpdxFileTypeExtensions_ToText_InvalidEnum_ThrowsException**: Verifies that `ToText` throws +`InvalidOperationException` when given an unsupported file type enum value. +This scenario is tested by `SpdxFileTypeExtensions_ToText_InvalidEnum_ThrowsException`. diff --git a/docs/verification/spdx-model/spdx-helpers.md b/docs/verification/spdx-model/spdx-helpers.md new file mode 100644 index 0000000..10df47f --- /dev/null +++ b/docs/verification/spdx-model/spdx-helpers.md @@ -0,0 +1,41 @@ +## SpdxHelpers + +### Verification Approach + +SpdxHelpers is verified through automated unit tests using the xUnit v3 framework. +Tests are located in +`test/DemaConsulting.SpdxModel.Tests/SpdxHelpersTests.cs`. Each test exercises the helper +method directly with no mocked dependencies. Additional coverage is provided implicitly +through the unit tests of dependent classes. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxHelpers_IsValidSpdxDateTime_NullInput_ReturnsTrue**: Verifies that `IsValidSpdxDateTime` +returns `true` when passed a null value, since null represents a not-set field which is valid. + +**SpdxHelpers_IsValidSpdxDateTime_EmptyInput_ReturnsTrue**: Verifies that `IsValidSpdxDateTime` +returns `true` when passed an empty string, since an empty string represents a not-set field +which is valid. + +**SpdxHelpers_IsValidSpdxDateTime_ValidFormat_ReturnsTrue**: Verifies that `IsValidSpdxDateTime` +returns `true` when passed a correctly formatted ISO 8601 UTC timestamp such as +`"2024-01-01T00:00:00Z"`. + +**SpdxHelpers_IsValidSpdxDateTime_InvalidFormat_ReturnsFalse**: Verifies that +`IsValidSpdxDateTime` returns `false` when passed a string that does not match the ISO 8601 +UTC format. + +**SpdxHelpers_EnhanceString_ConcretePreferredOverNoAssertion_ReturnsConcreteValue**: Verifies +that `EnhanceString` returns the concrete value when given a mix of a concrete value and +`NOASSERTION`. + +**SpdxHelpers_EnhanceString_NullInputs_ReturnsNull**: Verifies that `EnhanceString` returns +`null` when all inputs are null. diff --git a/docs/verification/spdx-model/spdx-license-element.md b/docs/verification/spdx-model/spdx-license-element.md new file mode 100644 index 0000000..d9c7486 --- /dev/null +++ b/docs/verification/spdx-model/spdx-license-element.md @@ -0,0 +1,44 @@ +## SpdxLicenseElement + +### Verification Approach + +`SpdxLicenseElement` is an abstract base class; it cannot be instantiated directly. Its +logic is exercised through `SpdxPackage`, the simplest concrete subclass. All unit tests +live in `SpdxLicenseElementTests` within the `DemaConsulting.SpdxModel.Tests` project. +Subclass-level deep-copy tests (for `SpdxPackage`, `SpdxFile`, and `SpdxSnippet`) provide +additional evidence that the inherited fields are stored and copied correctly. + +### Test Environment + +Standard test environment — no external services, files, or special configuration required. +Tests run with `dotnet test` under the standard CI pipeline. + +### Acceptance Criteria + +All automated tests in `SpdxLicenseElementTests` pass with zero failures, and all +subclass tests that exercise the inherited license-element fields (deep-copy and enhance) +also pass with zero failures. + +### Test Scenarios + +The following scenarios are covered by `SpdxLicenseElementTests`: + +- **Empty and null fields replaced by concrete values**: verifies that `ConcludedLicense`, + `CopyrightText`, and `LicenseComments` set to empty string or null (fitness 0/1) are + replaced when the source carries concrete (rank-3) values. + +- **NOASSERTION fields replaced by concrete values**: verifies that `ConcludedLicense`, + `CopyrightText`, and `LicenseComments` set to `NOASSERTION` (fitness 2) are replaced + when the source carries concrete (rank-3) values. + +- **Concrete fields not replaced by secondary values**: verifies that `ConcludedLicense`, + `CopyrightText`, and `LicenseComments` already holding concrete values are not overwritten + by any secondary value regardless of its fitness level. + +- **Attribution text merged by deduplication**: verifies that unique entries from the + secondary `AttributionText` array are appended while duplicate entries are discarded, so + each attribution notice appears exactly once in the merged result. + +- **Annotations merged by identity-match and append**: verifies that annotations matching an + existing entry (same annotator, date, type, and comment) are recognized as duplicates and + that annotations with no matching entry are appended as independent deep copies. diff --git a/docs/verification/spdx-model/spdx-model.md b/docs/verification/spdx-model/spdx-model.md new file mode 100644 index 0000000..173b2ab --- /dev/null +++ b/docs/verification/spdx-model/spdx-model.md @@ -0,0 +1,66 @@ +## SpdxModel + +### Verification Approach + +The SpdxModel library is verified through automated unit and integration tests using the xUnit v3 +framework. Tests are organized in `test/DemaConsulting.SpdxModel.Tests/`. Unit tests verify +individual data model classes in isolation; integration tests verify the IO subsystem end-to-end +and the Transform subsystem with real document instances. No external dependencies are mocked. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully**: Verifies that the library +successfully reads and parses an SPDX 2.2 example JSON document, returning a non-null +SpdxDocument without throwing exceptions. + +**SpdxModel_ReadSpdxJson_Spdx23Example_ParsesSuccessfully**: Verifies that the library +successfully reads and parses an SPDX 2.3 example JSON document, returning a non-null +SpdxDocument without throwing exceptions. + +**SpdxModel_ReadSpdxJson_Spdx22Example_PassesValidation**: Verifies that the document parsed +from the SPDX 2.2 example JSON passes all validation checks with no reported issues. + +**SpdxModel_ReadSpdxJson_Spdx23Example_PassesValidation**: Verifies that the document parsed +from the SPDX 2.3 example JSON passes all validation checks with no reported issues. + +**SpdxModel_ReadSpdxJson_Spdx23Example_RootPackagesIdentified**: Verifies that the root +packages are correctly identified in the SPDX 2.3 example document after parsing, confirming +DESCRIBES relationship traversal works as expected. + +**SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocument**: Verifies that a +deep copy of the parsed SPDX 2.3 document is structurally equal to the original, confirming +that all nested objects are fully duplicated. + +**SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds**: Verifies that an SPDX 2.3 +document serialized to JSON and then deserialized produces a document equivalent to the +original, confirming end-to-end serialization fidelity. + +**SpdxModel_Deserialize_MalformedJson_ThrowsJsonException**: Verifies that passing malformed +JSON to `Spdx2JsonDeserializer.Deserialize` throws a `JsonException` rather than returning a +partially-populated document. + +**SpdxModel_Validate_InvalidDocument_ReportsIssues**: Verifies that calling `Validate()` on a +deliberately incomplete SPDX document produces a non-empty issues list with the expected +validation error messages. + +**SpdxModel_FieldOptionality_RequiredFieldsNotNull_OptionalFieldsNullable**: Verifies that +required fields on key SPDX data model types are non-nullable string types with default empty +values, and that optional fields are nullable. + +**SpdxModel_Helpers_DateTimeValidation_IsObservableThroughDocumentModel**: Verifies that the +date-time validation utility (`SpdxHelpers.IsValidSpdxDateTime`) is exercised through the +document model by confirming that an invalid creation date is caught by document-level +validation. Linked from `SpdxModel-Data-Helpers`. + +**SpdxModel_Transform_AddRelationship_IsObservableThroughDocumentModel**: Verifies that the +`AddRelationship` transform utility correctly adds a new relationship to an SPDX document and +that the addition is observable through the document model's relationship collection. Linked +from `SpdxModel-Transform`. diff --git a/docs/verification/spdx-model/spdx-package-verification-code.md b/docs/verification/spdx-model/spdx-package-verification-code.md new file mode 100644 index 0000000..6e3ab06 --- /dev/null +++ b/docs/verification/spdx-model/spdx-package-verification-code.md @@ -0,0 +1,51 @@ +## SpdxPackageVerificationCode + +### Verification Approach + +SpdxPackageVerificationCode is verified through automated unit tests using the xUnit v3 +framework. Tests are located in +`test/DemaConsulting.SpdxModel.Tests/SpdxPackageVerificationCodeTests.cs`. Each test +constructs an SpdxPackageVerificationCode instance directly and exercises the method under +test with no mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxPackageVerificationCode_SameComparer_SameValueDifferentExcludedFiles_ReturnsEqual**: Verifies that SameComparer +correctly identifies two SpdxPackageVerificationCode instances as equal when their Value +fields are identical (even if ExcludedFiles differs), and as distinct when Values differ. +Equality is determined by Value alone. +This scenario is tested by `SpdxPackageVerificationCode_SameComparer_SameValueDifferentExcludedFiles_ReturnsEqual`. + +**SpdxPackageVerificationCode_DeepCopy_FullyPopulatedCode_CreatesEqualButDistinctCopy**: Verifies that a +deep copy produces a new SpdxPackageVerificationCode instance with equal field values but a +distinct object reference. +This scenario is tested by +`SpdxPackageVerificationCode_DeepCopy_FullyPopulatedCode_CreatesEqualButDistinctCopy`. + +**SpdxPackageVerificationCode_Enhance_MissingFields_MergesCorrectly**: Verifies that +Enhance merges verification code data by adding missing fields from the source while +preserving existing values on the target. +This scenario is tested by +`SpdxPackageVerificationCode_Enhance_MissingFields_MergesCorrectly`. + +**SpdxPackageVerificationCode_Validate_InvalidValue_ReportsIssue**: Verifies that validation reports an +issue when the verification code value field is missing or does not conform to the required +SHA1 hash format. +This scenario is tested by `SpdxPackageVerificationCode_Validate_InvalidValue_ReportsIssue`. + +**SpdxPackageVerificationCode_Validate_ValidValue_ReportsNoIssues**: Verifies that validation +reports no issues when the verification code value is a valid 40-character SHA1 hex digest. +This scenario is tested by `SpdxPackageVerificationCode_Validate_ValidValue_ReportsNoIssues`. + +**SpdxPackageVerificationCode_Validate_NonHexValue_ReportsIssue**: Verifies that validation +reports an issue when the verification code value is 40 characters but contains non-hexadecimal +characters. +This scenario is tested by `SpdxPackageVerificationCode_Validate_NonHexValue_ReportsIssue`. diff --git a/docs/verification/spdx-model/spdx-package.md b/docs/verification/spdx-model/spdx-package.md new file mode 100644 index 0000000..9714b5f --- /dev/null +++ b/docs/verification/spdx-model/spdx-package.md @@ -0,0 +1,87 @@ +## SpdxPackage + +### Verification Approach + +SpdxPackage is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxPackageTests.cs`. Each test constructs +an SpdxPackage instance directly and exercises the method under test with no mocked +dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxPackage_SameComparer_ComparesCorrectly**: Verifies that SameComparer correctly +identifies two SpdxPackage instances as equal when all fields match and as distinct when any +field differs. +This scenario is tested by `SpdxPackage_SameComparer_ComparesCorrectly`. + +**SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance**: Verifies that a deep copy produces +a new SpdxPackage instance with equal field values but a distinct object reference, including +all nested checksums, verification code, external references, and annotations. Also verifies +that the VerificationCode is deep-copied (value equality but reference distinctness). +This scenario is tested by `SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance`. + +**SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly**: Verifies that Enhance merges package +data by adding missing fields from the source while preserving existing values on the target. +This scenario is tested by `SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly`. + +**SpdxPackage_Validate_Success**: Verifies that a fully populated valid SpdxPackage passes +all validation checks without reporting any issues. +This scenario is tested by `SpdxPackage_Validate_Success`. + +**SpdxPackage_Validate_MissingPackageName_ReportsIssue**: Verifies that validation reports an issue when +the package name field is missing or empty. +This scenario is tested by `SpdxPackage_Validate_MissingPackageName_ReportsIssue`. + +**SpdxPackage_Validate_InvalidPackageId_ReportsIssue**: Verifies that validation reports an issue when +the package SPDX-ID does not conform to the required SPDXRef- prefix format. +This scenario is tested by `SpdxPackage_Validate_InvalidPackageId_ReportsIssue`. + +**SpdxPackage_Validate_MissingDownload_ReportsIssue**: Verifies that validation reports an issue when the +download location field is missing or empty. +This scenario is tested by `SpdxPackage_Validate_MissingDownload_ReportsIssue`. + +**SpdxPackage_Validate_InvalidSupplier_ReportsIssue**: Verifies that validation reports an issue when the +supplier field is present but does not conform to the required organization or tool format. +This scenario is tested by `SpdxPackage_Validate_InvalidSupplier_ReportsIssue`. + +**SpdxPackage_Validate_InvalidOriginator_ReportsIssue**: Verifies that validation reports an issue when +the originator field is present but does not conform to the required organization or tool +format. +This scenario is tested by `SpdxPackage_Validate_InvalidOriginator_ReportsIssue`. + +**SpdxPackage_Validate_InvalidReleaseDate_ReportsIssue**: Verifies that validation reports an issue when +the release date field is present but does not conform to ISO 8601 date-time format. +This scenario is tested by `SpdxPackage_Validate_InvalidReleaseDate_ReportsIssue`. + +**SpdxPackage_Validate_InvalidBuiltDate_ReportsIssue**: Verifies that validation reports an issue when the +built date field is present but does not conform to ISO 8601 date-time format. +This scenario is tested by `SpdxPackage_Validate_InvalidBuiltDate_ReportsIssue`. + +**SpdxPackage_Validate_InvalidValidUntilDate_ReportsIssue**: Verifies that validation reports an issue when +the valid-until date field is present but does not conform to ISO 8601 date-time format. +This scenario is tested by `SpdxPackage_Validate_InvalidValidUntilDate_ReportsIssue`. + +**SpdxPackage_Validate_InvalidAnnotation_ReportsIssue**: Verifies that validation reports an issue when an +annotation within the package contains invalid fields. +This scenario is tested by `SpdxPackage_Validate_InvalidAnnotation_ReportsIssue`. + +**SpdxPackage_Validate_HasFilesReferencesMissingFile_ReportsIssue**: Verifies that validation +reports an issue when the HasFiles array references a file ID that does not exist in the +owning document. +This scenario is tested by `SpdxPackage_Validate_HasFilesReferencesMissingFile_ReportsIssue`. + +**SpdxPackage_ValidateNtia_MissingSupplier_ReportsIssue**: Verifies that NTIA validation +reports an issue when the package supplier field is absent. +This scenario is tested by `SpdxPackage_ValidateNtia_MissingSupplier_ReportsIssue`. + +**SpdxPackage_ValidateNtia_MissingVersion_ReportsIssue**: Verifies that NTIA validation +reports an issue when the package version field is absent. +This scenario is tested by `SpdxPackage_ValidateNtia_MissingVersion_ReportsIssue`. diff --git a/docs/verification/spdx-model/spdx-relationship.md b/docs/verification/spdx-model/spdx-relationship.md new file mode 100644 index 0000000..bc59678 --- /dev/null +++ b/docs/verification/spdx-model/spdx-relationship.md @@ -0,0 +1,91 @@ +## SpdxRelationship + +### Verification Approach + +SpdxRelationship is verified through automated unit tests using the xUnit v3 framework. Tests +are located in `test/DemaConsulting.SpdxModel.Tests/SpdxRelationshipTests.cs`. Each test +constructs an SpdxRelationship instance directly and exercises the method under test with no +mocked dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxRelationship_SameComparer_MatchingRelationships_ReturnsTrue**: Verifies that SameComparer correctly +identifies two SpdxRelationship instances as equal when the key fields (Id, RelationshipType, RelatedSpdxElement) +match, even when Comment differs. +This scenario is tested by `SpdxRelationship_SameComparer_MatchingRelationships_ReturnsTrue`. + +**SpdxRelationship_SameComparer_DifferentRelationships_ReturnsFalse**: Verifies that SameComparer correctly +identifies two SpdxRelationship instances as distinct when any key field differs. +This scenario is tested by `SpdxRelationship_SameComparer_DifferentRelationships_ReturnsFalse`. + +**SpdxRelationship_SameComparer_MatchingRelationships_ReturnsSameHashCode**: Verifies that SameComparer +produces identical hash codes for two relationships that are considered equal, satisfying the hash/equality contract. +This scenario is tested by `SpdxRelationship_SameComparer_MatchingRelationships_ReturnsSameHashCode`. + +**SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsTrue**: Verifies that +SameElementsComparer correctly identifies two relationships as equal based solely on the +source and target element IDs, ignoring relationship type. +This scenario is tested by `SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsTrue`. + +**SpdxRelationship_SameElementsComparer_DifferentElements_ReturnsFalse**: Verifies that +SameElementsComparer correctly identifies two relationships as distinct when their source or +target element IDs differ. +This scenario is tested by `SpdxRelationship_SameElementsComparer_DifferentElements_ReturnsFalse`. + +**SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsSameHashCode**: Verifies that +SameElementsComparer produces identical hash codes for two relationships with the same element +IDs, satisfying the hash/equality contract. +This scenario is tested by `SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsSameHashCode`. + +**SpdxRelationship_DeepCopy_FullyPopulatedRelationship_CreatesEqualButDistinctCopy**: Verifies that a deep copy +produces a new SpdxRelationship instance with equal field values but a distinct object +reference. +This scenario is tested by `SpdxRelationship_DeepCopy_FullyPopulatedRelationship_CreatesEqualButDistinctCopy`. + +**SpdxRelationship_Enhance_MatchingAndNewRelationships_MergesCorrectly**: Verifies that Enhance merges +relationship data by adding missing fields from the source while preserving existing values +on the target. +This scenario is tested by `SpdxRelationship_Enhance_MatchingAndNewRelationships_MergesCorrectly`. + +**SpdxRelationship_Validate_MissingRelationshipId_ReportsIssue**: Verifies that validation reports an issue when the +relationship SPDX-ID field is missing or empty. +This scenario is tested by `SpdxRelationship_Validate_MissingRelationshipId_ReportsIssue`. + +**SpdxRelationship_Validate_MissingRelatedElementId_ReportsIssue**: Verifies that validation reports an issue +when the related element ID field is missing or empty. +This scenario is tested by `SpdxRelationship_Validate_MissingRelatedElementId_ReportsIssue`. + +**SpdxRelationship_Validate_MissingRelationshipType_ReportsIssue**: Verifies that validation reports an issue +when the relationship type field is missing or set to an unrecognized value. +This scenario is tested by `SpdxRelationship_Validate_MissingRelationshipType_ReportsIssue`. + +**SpdxRelationshipTypeExtensions_FromText_KnownText_ReturnsMappedEnum**: Verifies that FromText correctly parses a +recognized relationship type string to its corresponding enum value. +This scenario is tested by `SpdxRelationshipTypeExtensions_FromText_KnownText_ReturnsMappedEnum`. + +**SpdxRelationshipTypeExtensions_FromText_UnknownText_ThrowsInvalidOperationException**: Verifies that FromText throws an +InvalidOperationException with a descriptive message when given an unrecognized relationship +type string. +This scenario is tested by `SpdxRelationshipTypeExtensions_FromText_UnknownText_ThrowsInvalidOperationException`. + +**SpdxRelationshipTypeExtensions_ToText_KnownEnum_ReturnsMappedText**: Verifies that ToText correctly converts a +recognized relationship type enum value to its SPDX text representation. +This scenario is tested by `SpdxRelationshipTypeExtensions_ToText_KnownEnum_ReturnsMappedText`. + +**SpdxRelationshipTypeExtensions_ToText_MissingSentinel_ThrowsInvalidOperationException**: Verifies that ToText +throws an InvalidOperationException with message "Attempt to serialize missing SPDX Relationship Type" when +called on the Missing sentinel value. +This scenario is tested by `SpdxRelationshipTypeExtensions_ToText_MissingSentinel_ThrowsInvalidOperationException`. + +**SpdxRelationshipTypeExtensions_ToText_UnknownEnum_ThrowsInvalidOperationException**: Verifies that ToText throws an +InvalidOperationException with a descriptive message when given an unknown (out-of-range) +relationship type enum value. +This scenario is tested by `SpdxRelationshipTypeExtensions_ToText_UnknownEnum_ThrowsInvalidOperationException`. diff --git a/docs/verification/spdx-model/spdx-snippet.md b/docs/verification/spdx-model/spdx-snippet.md new file mode 100644 index 0000000..ef957dd --- /dev/null +++ b/docs/verification/spdx-model/spdx-snippet.md @@ -0,0 +1,65 @@ +## SpdxSnippet + +### Verification Approach + +SpdxSnippet is verified through automated unit tests using the xUnit v3 framework. Tests are +located in `test/DemaConsulting.SpdxModel.Tests/SpdxSnippetTests.cs`. Each test constructs +an SpdxSnippet instance directly and exercises the method under test with no mocked +dependencies. + +### Test Environment + +N/A - standard test environment. + +### Acceptance Criteria + +All automated tests pass with zero failures. + +### Test Scenarios + +**SpdxSnippet_SameComparer_SameFileAndByteRange_ReturnsEqual**: Verifies that SameComparer correctly +identifies two SpdxSnippet instances as equal when all fields match and as distinct when any +field differs. +This scenario is tested by `SpdxSnippet_SameComparer_SameFileAndByteRange_ReturnsEqual`. + +**SpdxSnippet_DeepCopy_FullyPopulatedSnippet_CreatesEqualButDistinctCopy**: Verifies that a deep copy produces +a new SpdxSnippet instance with equal field values but a distinct object reference, including +all nested byte and line ranges. +This scenario is tested by `SpdxSnippet_DeepCopy_FullyPopulatedSnippet_CreatesEqualButDistinctCopy`. + +**SpdxSnippet_Enhance_MatchingAndNewSnippets_MergesCorrectly**: Verifies that Enhance merges +snippet data by adding missing fields from the source while preserving existing values on +the target. +This scenario is tested by `SpdxSnippet_Enhance_MatchingAndNewSnippets_MergesCorrectly`. + +**SpdxSnippet_Validate_InvalidSnippetId_ReportsIssue**: Verifies that validation reports an issue +when the snippet SPDX-ID does not conform to the required SPDXRef- prefix format. +This scenario is tested by `SpdxSnippet_Validate_InvalidSnippetId_ReportsIssue`. + +**SpdxSnippet_Validate_AllRequiredFieldsPresent_ReturnsNoIssues**: Verifies that a fully populated valid SpdxSnippet passes +all validation checks without reporting any issues. +This scenario is tested by `SpdxSnippet_Validate_AllRequiredFieldsPresent_ReturnsNoIssues`. + +**SpdxSnippet_Validate_InvalidAnnotation_ReportsIssue**: Verifies that validation reports an issue when an +annotation within the snippet contains invalid fields. +This scenario is tested by `SpdxSnippet_Validate_InvalidAnnotation_ReportsIssue`. + +**SpdxSnippet_Validate_EmptySnippetFromFile_ReportsIssue**: Verifies that validation reports an +issue when the snippet-from-file field is empty. +This scenario is tested by `SpdxSnippet_Validate_EmptySnippetFromFile_ReportsIssue`. + +**SpdxSnippet_Validate_InvalidByteStart_ReportsIssue**: Verifies that validation reports an issue +when the snippet byte range start is less than 1. +This scenario is tested by `SpdxSnippet_Validate_InvalidByteStart_ReportsIssue`. + +**SpdxSnippet_Validate_InvalidByteEnd_ReportsIssue**: Verifies that validation reports an issue +when the snippet byte range end is less than the byte range start. +This scenario is tested by `SpdxSnippet_Validate_InvalidByteEnd_ReportsIssue`. + +**SpdxSnippet_Validate_EmptyConcludedLicense_ReportsIssue**: Verifies that validation reports an +issue when the concluded license field is empty. +This scenario is tested by `SpdxSnippet_Validate_EmptyConcludedLicense_ReportsIssue`. + +**SpdxSnippet_Validate_EmptyCopyrightText_ReportsIssue**: Verifies that validation reports an issue +when the copyright text field is empty. +This scenario is tested by `SpdxSnippet_Validate_EmptyCopyrightText_ReportsIssue`. diff --git a/docs/verification/spdx-model/transform/spdx-relationships.md b/docs/verification/spdx-model/transform/spdx-relationships.md new file mode 100644 index 0000000..ae9126d --- /dev/null +++ b/docs/verification/spdx-model/transform/spdx-relationships.md @@ -0,0 +1,59 @@ +### SpdxRelationships + +#### Verification Approach + +SpdxRelationships is verified through automated unit tests using the xUnit v3 framework. Tests +are located in `test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxRelationshipsTests.cs`. +Each test constructs an SpdxDocument with a known set of relationships and exercises the +SpdxRelationships methods directly with no mocked dependencies. + +#### Test Environment + +N/A - standard test environment. + +#### Acceptance Criteria + +All automated tests pass with zero failures. + +#### Test Scenarios + +**SpdxRelationships_AddSingle_MissingId_ThrowsArgumentException**: Verifies that attempting to add +a single relationship where the source element ID does not exist in the document throws +`ArgumentException` with a message identifying the missing element and leaves the document +unmodified. + +**SpdxRelationships_AddSingle_MissingRelatedElement_ThrowsArgumentException**: Verifies that +attempting to add a single relationship where the related element ID does not exist in the document +(and is neither NOASSERTION nor DocumentRef-prefixed) throws `ArgumentException` and leaves the +document unmodified. + +**SpdxRelationships_AddSingle_ValidRelationship_AddsRelationship**: Verifies that adding a single +valid relationship between two existing elements results in the relationship being appended to the +document's relationships collection. + +**SpdxRelationships_AddSingle_DuplicateRelationship_EnhancesExistingRelationship**: Verifies that +adding a relationship that is identical to one already present in the document enhances the existing +entry rather than creating a duplicate. + +**SpdxRelationships_AddSingle_NoAssertionTarget_AddsRelationship**: Verifies that a relationship +whose target element is `NOASSERTION` is accepted as valid and added to the document without error. + +**SpdxRelationships_AddSingle_DocumentRefTarget_AddsRelationship**: Verifies that a relationship +whose target element uses the `DocumentRef-` external-reference prefix is accepted as valid and +added to the document without error. + +**SpdxRelationships_AddMultiple_SingleRelationship_AddsRelationship**: Verifies that the batch Add +overload with a single-element array appends the relationship to the document. + +**SpdxRelationships_AddMultiple_DuplicateRelationships_DeduplicatesRelationships**: Verifies that +passing duplicate relationships in a single batch call results in only one entry being added to the +document. + +**SpdxRelationships_AddMultiple_Replace_RemovesAndReplacesExistingRelationships**: Verifies that +invoking the batch Add with `replace=true` removes pre-existing relationships between the same +source and target elements before adding the new ones. + +**SpdxRelationships_AddMultiple_InvalidRelationship_LeavesDocumentUnmodified**: Verifies that when +any relationship in a batch is invalid (e.g., missing source ID), an `ArgumentException` is thrown +and the document's relationships collection is left in its original state — no relationships are +added or removed. diff --git a/docs/verification/spdx-model/transform/transform.md b/docs/verification/spdx-model/transform/transform.md new file mode 100644 index 0000000..2081a62 --- /dev/null +++ b/docs/verification/spdx-model/transform/transform.md @@ -0,0 +1,50 @@ +### Transform + +#### Verification Approach + +The Transform subsystem is verified through automated integration tests using the xUnit v3 +framework. Tests are located in +`test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxModelTransformTests.cs`. Integration tests +verify Transform operations using real SpdxDocument instances with no mocked dependencies. + +#### Test Environment + +N/A - standard test environment. + +#### Acceptance Criteria + +All integration tests pass with zero failures. + +#### Test Scenarios + +**SpdxModelTransform_AddRelationship_ToDocument_RelationshipPersists**: Verifies that adding +a relationship to an SpdxDocument through the Transform subsystem results in the relationship +being present in the document's relationship collection after the operation. + +**SpdxModelTransform_AddRelationship_InvalidSourceId_ThrowsArgumentException**: Verifies that +providing a source element ID that does not exist in the document causes `AddRelationship` to +throw `ArgumentException`. + +**SpdxModelTransform_AddRelationship_InvalidTargetId_ThrowsArgumentException**: Verifies that +providing a target element ID that does not exist in the document (and is neither `NOASSERTION` +nor `DocumentRef-`-prefixed) causes `AddRelationship` to throw `ArgumentException`. + +**SpdxModelTransform_AddRelationship_Duplicate_EnhancesExistingRelationship**: Verifies that +adding the same relationship twice does not duplicate the entry — the second add enhances the +existing relationship rather than appending a new one. + +**SpdxModelTransform_AddRelationship_Replace_RemovesPreExistingRelationships**: Verifies that +the batch Add overload with `replace=true` removes all pre-existing relationships between the +same source and target elements before adding the new relationship. + +**SpdxModelTransform_AddRelationship_BatchMultiple_AddsAllRelationships**: Verifies that +passing multiple distinct relationships in a single batch call results in all of them being +appended to the document. + +**SpdxModelTransform_AddRelationship_NoAssertionTarget_AddsRelationship**: Verifies that a +relationship with `NOASSERTION` as the target element is accepted as valid and added without +error. + +**SpdxModelTransform_AddRelationship_DocumentRefTarget_AddsRelationship**: Verifies that a +relationship whose target uses the `DocumentRef-` external-reference prefix is accepted as +valid and added without error. diff --git a/docs/verification/title.txt b/docs/verification/title.txt new file mode 100644 index 0000000..e67288b --- /dev/null +++ b/docs/verification/title.txt @@ -0,0 +1,14 @@ +--- +title: SpdxModel Verification Design +subtitle: SPDX document model for .NET +author: DEMA Consulting +description: Verification Design Document for SpdxModel +lang: en-US +keywords: + - Verification + - Testing + - SpdxModel + - C# + - .NET + - SPDX +--- diff --git a/requirements.yaml b/requirements.yaml index c64db5b..2b297a3 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -8,6 +8,7 @@ includes: - docs/reqstream/spdx-model/spdx-model.yaml - docs/reqstream/spdx-model/platform-requirements.yaml - docs/reqstream/spdx-model/io/io.yaml + - docs/reqstream/spdx-model/io/spdx-constants.yaml - docs/reqstream/spdx-model/io/spdx-2-json-deserializer.yaml - docs/reqstream/spdx-model/io/spdx-2-json-serializer.yaml - docs/reqstream/spdx-model/transform/transform.yaml @@ -27,7 +28,7 @@ includes: - docs/reqstream/spdx-model/spdx-package-verification-code.yaml - docs/reqstream/spdx-model/spdx-relationship.yaml - docs/reqstream/spdx-model/spdx-snippet.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.SpdxModel/IO/Spdx2JsonDeserializer.cs b/src/DemaConsulting.SpdxModel/IO/Spdx2JsonDeserializer.cs index dd453b2..fd04c15 100644 --- a/src/DemaConsulting.SpdxModel/IO/Spdx2JsonDeserializer.cs +++ b/src/DemaConsulting.SpdxModel/IO/Spdx2JsonDeserializer.cs @@ -26,14 +26,22 @@ namespace DemaConsulting.SpdxModel.IO; /// /// JSON Deserializer class /// +/// +/// This class is stateless: all methods are static and carry no instance state, making +/// it safe for concurrent calls from multiple threads. Unrecognised JSON fields are +/// silently ignored during deserialization. +/// public static class Spdx2JsonDeserializer { /// /// Deserialize SPDX Document /// + /// + /// Parses the JSON string into a DOM and delegates to . + /// /// Json string /// SPDX Document - /// thrown on error + /// Thrown when is not valid JSON text or does not represent a JSON object. public static SpdxDocument Deserialize(string json) { // Deserialize the Json @@ -47,6 +55,10 @@ public static SpdxDocument Deserialize(string json) /// /// Deserialize the SPDX Document /// + /// + /// Maps all SPDX 2.x JSON top-level fields to the object model. + /// Fields absent from the JSON produce empty strings, empty arrays, or null optional values. + /// /// Json Document Node /// SPDX Document public static SpdxDocument DeserializeDocument(JsonNode json) @@ -76,8 +88,12 @@ public static SpdxDocument DeserializeDocument(JsonNode json) /// /// Deserialize SPDX Creation Information /// + /// + /// Returns a default-valued when + /// is null (i.e., the creationInfo key is absent from the document). + /// /// Json Creation Information Node - /// SPDX Document + /// Populated ; fields absent in the JSON default to empty strings or empty arrays. public static SpdxCreationInformation DeserializeCreationInformation(JsonNode? json) { return new SpdxCreationInformation @@ -92,6 +108,9 @@ public static SpdxCreationInformation DeserializeCreationInformation(JsonNode? j /// /// Deserialize SPDX External Document References /// + /// + /// Returns an empty array when is null. + /// /// Json External Document References Array /// SPDX External Document References public static SpdxExternalDocumentReference[] DeserializeExternalDocumentReferences(JsonArray? json) @@ -103,6 +122,9 @@ public static SpdxExternalDocumentReference[] DeserializeExternalDocumentReferen /// /// Deserialize SPDX External Document Reference /// + /// + /// Deserializes a single external document reference including its nested checksum object. + /// /// Json External Document Reference Node /// SPDX External Document Reference public static SpdxExternalDocumentReference DeserializeExternalDocumentReference(JsonNode? json) @@ -118,6 +140,9 @@ public static SpdxExternalDocumentReference DeserializeExternalDocumentReference /// /// Deserialize SPDX Extracted Licensing Infos /// + /// + /// Returns an empty array when is null. + /// /// Json Extracted Licensing Info Array /// SPDX Extracted Licensing Infos public static SpdxExtractedLicensingInfo[] DeserializeExtractedLicensingInfos(JsonArray? json) @@ -128,6 +153,9 @@ public static SpdxExtractedLicensingInfo[] DeserializeExtractedLicensingInfos(Js /// /// Deserialize SPDX Extracted Licensing Info /// + /// + /// Deserializes a single extracted licensing info entry; optional fields default to null. + /// /// Json Extracted Licensing Info Node /// SPDX Extracted Licensing Info public static SpdxExtractedLicensingInfo DeserializeExtractedLicensingInfo(JsonNode? json) @@ -145,6 +173,9 @@ public static SpdxExtractedLicensingInfo DeserializeExtractedLicensingInfo(JsonN /// /// Deserialize SPDX Files /// + /// + /// Returns an empty array when is null. + /// /// Json Files Array /// SPDX Files public static SpdxFile[] DeserializeFiles(JsonArray? json) @@ -155,6 +186,11 @@ public static SpdxFile[] DeserializeFiles(JsonArray? json) /// /// Deserialize SPDX File /// + /// + /// Deserializes a single SPDX file entry including file types, checksums, and annotations. + /// File type strings are converted to enum values via + /// . + /// /// Json File Node /// SPDX File public static SpdxFile DeserializeFile(JsonNode? json) @@ -181,6 +217,9 @@ [.. ParseStringArray(json, SpdxConstants.FieldFileTypes).Select(SpdxFileTypeExte /// /// Deserialize SPDX Packages /// + /// + /// Returns an empty array when is null. + /// /// Json Packages Array /// SPDX Packages public static SpdxPackage[] DeserializePackages(JsonArray? json) @@ -191,6 +230,10 @@ public static SpdxPackage[] DeserializePackages(JsonArray? json) /// /// Deserialize SPDX Package /// + /// + /// Deserializes a single SPDX package entry. Optional fields are mapped to null or empty + /// string when absent from the JSON. + /// /// Json Package Node /// SPDX Package public static SpdxPackage DeserializePackage(JsonNode? json) @@ -231,6 +274,9 @@ public static SpdxPackage DeserializePackage(JsonNode? json) /// /// Deserialize SPDX Snippets /// + /// + /// Returns an empty array when is null. + /// /// Json Snippets Array /// SPDX Snippets public static SpdxSnippet[] DeserializeSnippets(JsonArray? json) @@ -241,6 +287,10 @@ public static SpdxSnippet[] DeserializeSnippets(JsonArray? json) /// /// Deserialize SPDX Snippet /// + /// + /// Byte-range and line-range values are extracted from the nested ranges array + /// using the private Find helper. Values absent from the JSON default to 0. + /// /// Json Snippet Node /// SPDX Snippet public static SpdxSnippet DeserializeSnippet(JsonNode? json) @@ -271,6 +321,9 @@ public static SpdxSnippet DeserializeSnippet(JsonNode? json) /// /// Deserialize SPDX Relationships /// + /// + /// Returns an empty array when is null. + /// /// Json Relationships Array /// SPDX Relationships public static SpdxRelationship[] DeserializeRelationships(JsonArray? json) @@ -281,6 +334,10 @@ public static SpdxRelationship[] DeserializeRelationships(JsonArray? json) /// /// Deserialize SPDX Relationship /// + /// + /// The relationship type string is converted to via + /// . + /// /// Json Relationship Node /// SPDX Relationship public static SpdxRelationship DeserializeRelationship(JsonNode? json) @@ -298,6 +355,10 @@ public static SpdxRelationship DeserializeRelationship(JsonNode? json) /// /// Deserialize SPDX Package Verification Code /// + /// + /// Returns null when is null (i.e., the field is absent from + /// the package JSON object). + /// /// Json Package Verification Code Node /// SPDX Package Verification Code public static SpdxPackageVerificationCode? DeserializeVerificationCode(JsonNode? json) @@ -314,6 +375,9 @@ public static SpdxRelationship DeserializeRelationship(JsonNode? json) /// /// Deserialize SPDX External References /// + /// + /// Returns an empty array when is null. + /// /// Json External References Array /// SPDX External References public static SpdxExternalReference[] DeserializeExternalReferences(JsonArray? json) @@ -324,6 +388,10 @@ public static SpdxExternalReference[] DeserializeExternalReferences(JsonArray? j /// /// Deserialize SPDX External Reference /// + /// + /// The reference category string is converted to via + /// . + /// /// Json External Reference Node /// SPDX External Reference public static SpdxExternalReference DeserializeExternalReference(JsonNode? json) @@ -341,6 +409,9 @@ public static SpdxExternalReference DeserializeExternalReference(JsonNode? json) /// /// Deserialize SPDX Checksums /// + /// + /// Returns an empty array when is null. + /// /// Json Checksums Array /// SPDX Checksums public static SpdxChecksum[] DeserializeChecksums(JsonArray? json) @@ -351,6 +422,10 @@ public static SpdxChecksum[] DeserializeChecksums(JsonArray? json) /// /// Deserialize SPDX Checksum /// + /// + /// The algorithm string is converted to via + /// . + /// /// Json Checksum Node /// SPDX Checksum public static SpdxChecksum DeserializeChecksum(JsonNode? json) @@ -365,6 +440,9 @@ public static SpdxChecksum DeserializeChecksum(JsonNode? json) /// /// Deserialize SPDX Annotations /// + /// + /// Returns an empty array when is null. + /// /// Json Annotations Array /// SPDX Annotations public static SpdxAnnotation[] DeserializeAnnotations(JsonArray? json) @@ -375,6 +453,10 @@ public static SpdxAnnotation[] DeserializeAnnotations(JsonArray? json) /// /// Deserialize SPDX Annotation /// + /// + /// The annotation type string is converted to via + /// . + /// /// Json Annotation Node /// SPDX Annotation public static SpdxAnnotation DeserializeAnnotation(JsonNode? json) @@ -392,6 +474,9 @@ public static SpdxAnnotation DeserializeAnnotation(JsonNode? json) /// /// Deserialize JSON String /// + /// + /// Returns when the node or the named property is absent. + /// /// Json Node /// String Name /// String Value @@ -403,6 +488,10 @@ private static string ParseString(JsonNode? node, string name) /// /// Deserialize JSON Optional String /// + /// + /// Returns null when the node or the named property is absent, distinguishing an absent + /// optional field from an empty-string field. + /// /// Json Node /// String Name /// String Value or null @@ -414,6 +503,9 @@ private static string ParseString(JsonNode? node, string name) /// /// Deserialize Json String Array /// + /// + /// Returns an empty array when the node or the named property is absent. + /// /// Json Node /// Strings Name /// String Array @@ -425,6 +517,9 @@ private static string[] ParseStringArray(JsonNode? node, string name) /// /// Deserialize Json Boolean /// + /// + /// Returns null when the named property is absent or cannot be parsed as a boolean. + /// /// Json Node /// Bool Name /// Bool value or null @@ -436,6 +531,10 @@ private static string[] ParseStringArray(JsonNode? node, string name) /// /// Find a node /// + /// + /// Delegates to the recursive + /// overload starting at index 0. + /// /// Starting node /// Node search path /// JsonNode or null @@ -447,13 +546,18 @@ private static string[] ParseStringArray(JsonNode? node, string name) /// /// Find a named node /// + /// + /// Recursively descends through named properties. When an intermediate node is a + /// , the method searches each element in + /// order and returns the first non-null match, enabling path traversal through arrays. + /// /// Starting node /// Name index /// Names list /// JsonNode if found, else null private static JsonNode? Find(JsonNode? node, int idx, IReadOnlyList names) { - // Fail if at end + // All path segments traversed — return the current node (found) if (node == null || idx >= names.Count) { return node; diff --git a/src/DemaConsulting.SpdxModel/IO/Spdx2JsonSerializer.cs b/src/DemaConsulting.SpdxModel/IO/Spdx2JsonSerializer.cs index 7bca0c1..8fbcaed 100644 --- a/src/DemaConsulting.SpdxModel/IO/Spdx2JsonSerializer.cs +++ b/src/DemaConsulting.SpdxModel/IO/Spdx2JsonSerializer.cs @@ -25,15 +25,29 @@ namespace DemaConsulting.SpdxModel.IO; /// -/// JSON Serializer class +/// Serializes an in-memory to SPDX 2.x JSON text or a +/// DOM. /// +/// +/// This class is the counterpart to and completes +/// the round-trip serialization support for the IO subsystem. All methods are static +/// and the class carries no instance state, making it safe for concurrent calls on +/// different documents without external synchronization. +/// public static class Spdx2JsonSerializer { /// - /// Serialize SPDX Document + /// Serialize an SPDX Document to an indented JSON string. /// - /// SPDX Document - /// Json string + /// + /// Delegates to then converts the resulting + /// to an indented JSON string. + /// + /// SPDX Document to serialize. Must not be null. + /// + /// Indented JSON string conforming to the SPDX 2.3 schema. All optional fields + /// absent from the model are omitted from the output. + /// public static string Serialize(SpdxDocument document) { // Serialize the document @@ -49,10 +63,19 @@ public static string Serialize(SpdxDocument document) } /// - /// Serialize SPDX Document + /// Serialize an SPDX Document to a JSON object DOM. /// - /// SPDX Document - /// Json object + /// + /// Top-level arrays (files, packages, snippets, relationships) + /// are always emitted even when empty, as required by the SPDX 2.x schema. + /// Optional arrays (e.g., externalDocumentRefs, annotations) are omitted + /// when empty. + /// + /// SPDX Document to serialize. Must not be null. + /// + /// A representing the root SPDX document + /// element with all mandatory top-level arrays populated. + /// public static JsonObject SerializeDocument(SpdxDocument document) { var json = new JsonObject(); @@ -91,8 +114,12 @@ public static JsonObject SerializeDocument(SpdxDocument document) /// /// Serialize SPDX Creation information to JSON object /// - /// SPDX Creation Information - /// JSON object + /// + /// Serializes the creation information object including optional creators array, + /// creation date, and optional comment and license list version fields. + /// + /// SPDX Creation Information to serialize. Must not be null. + /// A containing the creation information fields. public static JsonObject SerializeCreationInformation(SpdxCreationInformation info) { var json = new JsonObject(); @@ -106,8 +133,15 @@ public static JsonObject SerializeCreationInformation(SpdxCreationInformation in /// /// Serialize array of SPDX External Document References to JSON array /// - /// SPDX External Document References - /// JSON array + /// + /// Delegates to for each element. + /// Returns a of external document reference objects. + /// + /// SPDX External Document References to serialize. Must not be null. + /// + /// A containing one serialized + /// per external document reference. + /// public static JsonArray SerializeExternalDocumentReferences(SpdxExternalDocumentReference[] references) { var json = new JsonArray(); @@ -122,8 +156,11 @@ public static JsonArray SerializeExternalDocumentReferences(SpdxExternalDocument /// /// Serialize SPDX External Document Reference to JSON object /// - /// SPDX External Document Reference - /// JSON object + /// + /// Serializes a single external document reference including its nested checksum object. + /// + /// SPDX External Document Reference to serialize. Must not be null. + /// A containing the external document reference fields. public static JsonObject SerializeExternalDocumentReference(SpdxExternalDocumentReference reference) { var json = new JsonObject(); @@ -136,8 +173,14 @@ public static JsonObject SerializeExternalDocumentReference(SpdxExternalDocument /// /// Serialize array of SPDX Extracted Licensing Infos to JSON array /// - /// SPDX Extracted Licensing Infos - /// JSON array + /// + /// Delegates to for each element. + /// + /// SPDX Extracted Licensing Infos to serialize. Must not be null. + /// + /// A containing one serialized + /// per extracted licensing info entry. + /// public static JsonArray SerializeExtractedLicensingInfos(SpdxExtractedLicensingInfo[] infos) { var json = new JsonArray(); @@ -152,8 +195,11 @@ public static JsonArray SerializeExtractedLicensingInfos(SpdxExtractedLicensingI /// /// Serialize SPDX Extracted Licensing Info to JSON object /// - /// SPDX Extracted Licensing Info - /// JSON object + /// + /// Serializes a single extracted licensing info entry; optional fields are omitted when null or empty. + /// + /// SPDX Extracted Licensing Info to serialize. Must not be null. + /// A containing the extracted licensing info fields. public static JsonObject SerializeExtractedLicensingInfo(SpdxExtractedLicensingInfo info) { var json = new JsonObject(); @@ -168,8 +214,15 @@ public static JsonObject SerializeExtractedLicensingInfo(SpdxExtractedLicensingI /// /// Serialize array of SPDX Files to JSON array /// - /// SPDX Files - /// JSON array + /// + /// Delegates to for each element. Always emits at the + /// document level even when the array is empty. + /// + /// SPDX Files to serialize. Must not be null. + /// + /// A containing one serialized + /// per file entry. + /// public static JsonArray SerializeFiles(SpdxFile[] files) { var json = new JsonArray(); @@ -184,8 +237,12 @@ public static JsonArray SerializeFiles(SpdxFile[] files) /// /// Serialize SPDX File to JSON object /// - /// SPDX File - /// JSON object + /// + /// Serializes a single SPDX file entry including file types, checksums, and optional + /// annotations. The annotations sub-array is omitted when empty. + /// + /// SPDX File to serialize. Must not be null. + /// A containing all file fields. public static JsonObject SerializeFile(SpdxFile file) { var json = new JsonObject(); @@ -213,8 +270,15 @@ public static JsonObject SerializeFile(SpdxFile file) /// /// Serialize array of SPDX Packages to JSON array /// - /// SPDX Packages - /// JSON array + /// + /// Delegates to for each element. Always emits at the + /// document level even when the array is empty. + /// + /// SPDX Packages to serialize. Must not be null. + /// + /// A containing one serialized + /// per package entry. + /// public static JsonArray SerializePackages(SpdxPackage[] packages) { var json = new JsonArray(); @@ -229,8 +293,13 @@ public static JsonArray SerializePackages(SpdxPackage[] packages) /// /// Serialize SPDX Package to JSON object /// - /// SPDX Package - /// JSON object + /// + /// Serializes a single SPDX package entry. The filesAnalyzed field is omitted + /// when null; the verification code, external references, and annotations sub-objects + /// are omitted when absent or empty. + /// + /// SPDX Package to serialize. Must not be null. + /// A containing all package fields. public static JsonObject SerializePackage(SpdxPackage package) { var json = new JsonObject(); @@ -284,8 +353,15 @@ public static JsonObject SerializePackage(SpdxPackage package) /// /// Serialize array of SPDX Snippets to JSON array /// - /// SPDX Snippets - /// JSON array + /// + /// Delegates to for each element. Always emits at the + /// document level even when the array is empty. + /// + /// SPDX Snippets to serialize. Must not be null. + /// + /// A containing one serialized + /// per snippet entry. + /// public static JsonArray SerializeSnippets(SpdxSnippet[] snippets) { var json = new JsonArray(); @@ -300,8 +376,17 @@ public static JsonArray SerializeSnippets(SpdxSnippet[] snippets) /// /// Serialize SPDX Snippet to JSON object /// - /// SPDX Snippet - /// JSON object + /// + /// Always emits a byte-range entry in the ranges array. A line-range entry is + /// only added when BOTH and + /// are non-zero; if either is zero the + /// line-range entry is omitted entirely. + /// + /// SPDX Snippet to serialize. Must not be null. + /// + /// A containing all snippet fields including + /// the ranges array. + /// public static JsonObject SerializeSnippet(SpdxSnippet snippet) { var json = new JsonObject(); @@ -336,7 +421,7 @@ public static JsonObject SerializeSnippet(SpdxSnippet snippet) } } }; - if (snippet.SnippetLineEnd > 0 || snippet.SnippetLineStart > 0) + if (snippet.SnippetLineEnd > 0 && snippet.SnippetLineStart > 0) { ranges.Add(new JsonObject { @@ -360,8 +445,15 @@ public static JsonObject SerializeSnippet(SpdxSnippet snippet) /// /// Serialize array of SPDX Relationships to JSON array /// - /// SPDX Relationships - /// JSON array + /// + /// Delegates to for each element. Always emits at the + /// document level even when the array is empty. + /// + /// SPDX Relationships to serialize. Must not be null. + /// + /// A containing one serialized + /// per relationship entry. + /// public static JsonArray SerializeRelationships(SpdxRelationship[] relationships) { var json = new JsonArray(); @@ -376,8 +468,12 @@ public static JsonArray SerializeRelationships(SpdxRelationship[] relationships) /// /// Serialize SPDX Relationship to JSON object /// - /// SPDX Relationship - /// JSON object + /// + /// Serializes a single SPDX relationship. The optional comment field is omitted when null + /// or empty. + /// + /// SPDX Relationship to serialize. Must not be null. + /// A containing the relationship fields. public static JsonObject SerializeRelationship(SpdxRelationship relationship) { var json = new JsonObject(); @@ -391,8 +487,15 @@ public static JsonObject SerializeRelationship(SpdxRelationship relationship) /// /// Serialize SPDX Package Verification Code to JSON object /// - /// SPDX Package Verification Code - /// JSON object + /// + /// Serializes the package verification code object including the hash value and optional + /// excluded files array. + /// + /// SPDX Package Verification Code to serialize. Must not be null. + /// + /// A containing the verification code value + /// and optional excluded files array. + /// public static JsonObject SerializeVerificationCode(SpdxPackageVerificationCode code) { var json = new JsonObject(); @@ -404,14 +507,20 @@ public static JsonObject SerializeVerificationCode(SpdxPackageVerificationCode c /// /// Serialize array of SPDX External References to JSON array /// - /// SPDX External References - /// JSON array + /// + /// Delegates to for each element. + /// + /// SPDX External References to serialize. Must not be null. + /// + /// A containing one serialized + /// per external reference entry. + /// public static JsonArray SerializeExternalReferences(SpdxExternalReference[] references) { var json = new JsonArray(); - foreach (var checksum in references) + foreach (var reference in references) { - json.Add(SerializeExternalReference(checksum)); + json.Add(SerializeExternalReference(reference)); } return json; @@ -420,8 +529,12 @@ public static JsonArray SerializeExternalReferences(SpdxExternalReference[] refe /// /// Serialize SPDX External Reference to JSON object /// - /// SPDX External Reference - /// JSON object + /// + /// Serializes a single external reference entry including category, type, locator, and + /// optional comment. + /// + /// SPDX External Reference to serialize. Must not be null. + /// A containing the external reference fields. public static JsonObject SerializeExternalReference(SpdxExternalReference reference) { var json = new JsonObject(); @@ -435,8 +548,14 @@ public static JsonObject SerializeExternalReference(SpdxExternalReference refere /// /// Serialize array of SPDX Checksums to JSON array /// - /// SPDX Checksums - /// JSON array + /// + /// Delegates to for each element. + /// + /// SPDX Checksums to serialize. Must not be null. + /// + /// A containing one serialized + /// per checksum entry. + /// public static JsonArray SerializeChecksums(SpdxChecksum[] checksums) { var json = new JsonArray(); @@ -451,8 +570,11 @@ public static JsonArray SerializeChecksums(SpdxChecksum[] checksums) /// /// Serialize SPDX Checksum to JSON object /// - /// SPDX Checksum - /// JSON object + /// + /// Serializes a single checksum entry with its algorithm and value fields. + /// + /// SPDX Checksum to serialize. Must not be null. + /// A containing the algorithm and checksumValue fields. public static JsonObject SerializeChecksum(SpdxChecksum checksum) { var json = new JsonObject(); @@ -464,8 +586,14 @@ public static JsonObject SerializeChecksum(SpdxChecksum checksum) /// /// Serialize array of SPDX Annotations to JSON array /// - /// SPDX Annotations - /// JSON array + /// + /// Delegates to for each element. + /// + /// SPDX Annotations to serialize. Must not be null. + /// + /// A containing one serialized + /// per annotation entry. + /// public static JsonArray SerializeAnnotations(SpdxAnnotation[] annotations) { var json = new JsonArray(); @@ -480,8 +608,13 @@ public static JsonArray SerializeAnnotations(SpdxAnnotation[] annotations) /// /// Serialize SPDX Annotation to JSON object /// - /// SPDX Annotation to serialize - /// JSON object + /// + /// The SPDXID field is conditionally omitted when + /// is null or empty, because annotations on sub-elements often do not carry their own + /// SPDX ID. + /// + /// SPDX Annotation to serialize. Must not be null. + /// A containing all annotation fields. public static JsonObject SerializeAnnotation(SpdxAnnotation annotation) { var json = new JsonObject(); @@ -496,9 +629,13 @@ public static JsonObject SerializeAnnotation(SpdxAnnotation annotation) /// /// Emit a string property into a JSON object /// - /// JSON object - /// Property name - /// Property value + /// + /// Always writes the property, even for empty strings. Use + /// for fields that should be omitted when empty. + /// + /// JSON object to write the property into. Must not be null. + /// Property name key. Must not be null or empty. + /// Property value to write. private static void EmitString(JsonNode json, string name, string value) { json[name] = value; @@ -507,9 +644,13 @@ private static void EmitString(JsonNode json, string name, string value) /// /// Emit an optional string property into a JSON object /// - /// JSON object - /// Property name - /// Optional property value + /// + /// Omits the property entirely when is null or empty, consistent + /// with the serializer convention of not writing null or empty optional fields. + /// + /// JSON object to write the property into. Must not be null. + /// Property name key. Must not be null or empty. + /// Optional property value; null or empty causes the property to be omitted. private static void EmitOptionalString(JsonNode json, string name, string? value) { // Skip if empty @@ -521,6 +662,16 @@ private static void EmitOptionalString(JsonNode json, string name, string? value json[name] = value; } + /// + /// Emit an optional string array property into a JSON object. + /// + /// + /// Omits the property entirely when is empty, consistent + /// with the serializer convention of not writing null or empty optional fields. + /// + /// JSON object to write into. Must not be null. + /// Property name key. Must not be null or empty. + /// Array of string values; an empty array causes the property to be omitted. private static void EmitOptionalStrings(JsonNode json, string name, string[] values) { // Skip if empty diff --git a/src/DemaConsulting.SpdxModel/IO/SpdxConstants.cs b/src/DemaConsulting.SpdxModel/IO/SpdxConstants.cs index 403c6c8..0739fc1 100644 --- a/src/DemaConsulting.SpdxModel/IO/SpdxConstants.cs +++ b/src/DemaConsulting.SpdxModel/IO/SpdxConstants.cs @@ -1,377 +1,477 @@ +// Copyright(c) 2024 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. + namespace DemaConsulting.SpdxModel.IO; /// /// SPDX 2.x constants. /// +/// +/// All SPDX 2.x JSON field name strings are centralised here to provide a single +/// maintenance point. Both and +/// consume these constants, ensuring that any +/// SPDX schema field name change requires an update in exactly one place. +/// internal static class SpdxConstants { /// /// Constant for SPDX ID field /// + /// The SPDX 2.x JSON field name for the SPDX element identifier. internal const string FieldSpdxId = "SPDXID"; /// /// Constant for SPDX Version field /// + /// The SPDX 2.x JSON field name for the SPDX specification version. internal const string FieldSpdxVersion = "spdxVersion"; /// /// Constant for SPDX Name field /// + /// The SPDX 2.x JSON field name for the document or element name. internal const string FieldName = "name"; /// /// Constant for SPDX Data License field /// + /// The SPDX 2.x JSON field name for the data license of the SPDX document. internal const string FieldDataLicense = "dataLicense"; /// /// Constant for SPDX Document Namespace field /// + /// The SPDX 2.x JSON field name for the document namespace URI. internal const string FieldDocumentNamespace = "documentNamespace"; /// /// Constant for SPDX Comment field /// + /// The SPDX 2.x JSON field name for an optional free-text comment. internal const string FieldComment = "comment"; /// /// Constant for SPDX Creation Info field /// + /// The SPDX 2.x JSON field name for the document creation information object. internal const string FieldCreationInfo = "creationInfo"; /// /// Constant for SPDX External Document Refs field /// + /// The SPDX 2.x JSON field name for the array of external document references. internal const string FieldExternalDocumentRefs = "externalDocumentRefs"; /// /// Constant for SPDX Has Extracted Licensing Infos field /// + /// The SPDX 2.x JSON field name for the array of extracted licensing info entries. internal const string FieldHasExtractedLicensingInfos = "hasExtractedLicensingInfos"; /// /// Constant for SPDX Annotations field /// + /// The SPDX 2.x JSON field name for the annotations array on a document or element. internal const string FieldAnnotations = "annotations"; /// /// Constant for SPDX Files field /// + /// The SPDX 2.x JSON field name for the top-level files array. internal const string FieldFiles = "files"; /// /// Constant for SPDX Packages field /// + /// The SPDX 2.x JSON field name for the top-level packages array. internal const string FieldPackages = "packages"; /// /// Constant for SPDX Snippets field /// + /// The SPDX 2.x JSON field name for the top-level snippets array. internal const string FieldSnippets = "snippets"; /// /// Constant for SPDX Relationships field /// + /// The SPDX 2.x JSON field name for the top-level relationships array. internal const string FieldRelationships = "relationships"; /// /// Constant for SPDX Document Describes field /// + /// The SPDX 2.x JSON field name for the array of element IDs described by the document. internal const string FieldDocumentDescribes = "documentDescribes"; /// /// Constant for SPDX Creators field /// + /// The SPDX 2.x JSON field name for the creators array in creation information. internal const string FieldCreators = "creators"; /// /// Constant for SPDX Created field /// + /// The SPDX 2.x JSON field name for the creation date-time in creation information. internal const string FieldCreated = "created"; /// /// Constant for SPDX License List Version field /// + /// The SPDX 2.x JSON field name for the SPDX license list version. internal const string FieldLicenseListVersion = "licenseListVersion"; /// /// Constant for SPDX External Document Id field /// + /// The SPDX 2.x JSON field name for the external document identifier. internal const string FieldExternalDocumentId = "externalDocumentId"; /// /// Constant for SPDX Checksum field /// + /// The SPDX 2.x JSON field name for a single checksum object (used in external document references). internal const string FieldChecksum = "checksum"; /// /// Constant for SPDX Checksums field /// + /// The SPDX 2.x JSON field name for the checksums array on a package or file. internal const string FieldChecksums = "checksums"; /// /// Constant for SPDX Document field /// + /// The SPDX 2.x JSON field name for the SPDX document URI in an external document reference. internal const string FieldSpdxDocument = "spdxDocument"; /// /// Constant for SPDX License ID field /// + /// The SPDX 2.x JSON field name for the license identifier in extracted licensing info. internal const string FieldLicenseId = "licenseId"; /// /// Constant for SPDX Extracted Text field /// + /// The SPDX 2.x JSON field name for the extracted license text. internal const string FieldExtractedText = "extractedText"; /// /// Constant for SPDX See Alsos field /// + /// The SPDX 2.x JSON field name for the cross-reference URLs in extracted licensing info. internal const string FieldSeeAlsos = "seeAlsos"; /// /// Constant for SPDX File Name field /// + /// The SPDX 2.x JSON field name for the file path or name. internal const string FieldFileName = "fileName"; /// /// Constant for SPDX File Types field /// + /// The SPDX 2.x JSON field name for the file type classification array. internal const string FieldFileTypes = "fileTypes"; /// /// Constant for SPDX License Concluded field /// + /// The SPDX 2.x JSON field name for the concluded license expression. internal const string FieldLicenseConcluded = "licenseConcluded"; /// /// Constant for SPDX License Info In Files field /// + /// The SPDX 2.x JSON field name for the license information found in a file. internal const string FieldLicenseInfoInFiles = "licenseInfoInFiles"; /// /// Constant for SPDX License Comments field /// + /// The SPDX 2.x JSON field name for license-related comments. internal const string FieldLicenseComments = "licenseComments"; /// /// Constant for SPDX Copyright Text field /// + /// The SPDX 2.x JSON field name for the copyright text. internal const string FieldCopyrightText = "copyrightText"; /// /// Constant for SPDX Notice Text field /// + /// The SPDX 2.x JSON field name for the notice text on a file. internal const string FieldNoticeText = "noticeText"; /// /// Constant for SPDX File Contributors field /// + /// The SPDX 2.x JSON field name for the file contributors array. internal const string FieldFileContributors = "fileContributors"; /// /// Constant for SPDX Attribution Texts field /// + /// The SPDX 2.x JSON field name for the attribution texts array. internal const string FieldAttributionTexts = "attributionTexts"; /// /// Constant for SPDX Version Info field /// + /// The SPDX 2.x JSON field name for the package version string. internal const string FieldVersionInfo = "versionInfo"; /// /// Constant for SPDX Package File Name field /// + /// The SPDX 2.x JSON field name for the package file name. internal const string FieldPackageFileName = "packageFileName"; /// /// Constant for SPDX Supplier field /// + /// The SPDX 2.x JSON field name for the package supplier. internal const string FieldSupplier = "supplier"; /// /// Constant for SPDX Originator field /// + /// The SPDX 2.x JSON field name for the package originator. internal const string FieldOriginator = "originator"; /// /// Constant for SPDX Download Location field /// + /// The SPDX 2.x JSON field name for the package download location URI. internal const string FieldDownloadLocation = "downloadLocation"; /// /// Constant for SPDX Files Analyzed field /// + /// The SPDX 2.x JSON field name for the files-analyzed flag on a package. internal const string FieldFilesAnalyzed = "filesAnalyzed"; /// /// Constant for SPDX Has Files field /// + /// The SPDX 2.x JSON field name for the array of file IDs belonging to a package. internal const string FieldHasFiles = "hasFiles"; /// /// Constant for SPDX Package Verification Code field /// + /// The SPDX 2.x JSON field name for the package verification code object. internal const string FieldPackageVerificationCode = "packageVerificationCode"; /// /// Constant for SPDX Home Page field /// + /// The SPDX 2.x JSON field name for the package home page URL. internal const string FieldHomePage = "homepage"; /// /// Constant for SPDX Source Info field /// + /// The SPDX 2.x JSON field name for the package source information text. internal const string FieldSourceInfo = "sourceInfo"; /// /// Constant for SPDX License Info From Files field /// + /// The SPDX 2.x JSON field name for the license information from files array in a package. internal const string FieldLicenseInfoFromFiles = "licenseInfoFromFiles"; /// /// Constant for SPDX License Declared field /// + /// The SPDX 2.x JSON field name for the declared license expression of a package. internal const string FieldLicenseDeclared = "licenseDeclared"; /// /// Constant for SPDX Summary field /// + /// The SPDX 2.x JSON field name for the package summary text. internal const string FieldSummary = "summary"; /// /// Constant for SPDX Description field /// + /// The SPDX 2.x JSON field name for the package description text. internal const string FieldDescription = "description"; /// /// Constant for SPDX External Refs field /// + /// The SPDX 2.x JSON field name for the external references array on a package. internal const string FieldExternalRefs = "externalRefs"; /// /// Constant for SPDX Primary Package Purpose field /// + /// The SPDX 2.x JSON field name for the primary package purpose string. internal const string FieldPrimaryPackagePurpose = "primaryPackagePurpose"; /// /// Constant for SPDX Release Date field /// + /// The SPDX 2.x JSON field name for the package release date. internal const string FieldReleaseDate = "releaseDate"; /// /// Constant for SPDX Build Date field /// + /// The SPDX 2.x JSON field name for the package build date. internal const string FieldBuiltDate = "builtDate"; /// /// Constant for SPDX Valid Until Date field /// + /// The SPDX 2.x JSON field name for the package valid-until date. internal const string FieldValidUntilDate = "validUntilDate"; /// /// Constant for SPDX Snippet From File field /// + /// The SPDX 2.x JSON field name for the file reference in a snippet. internal const string FieldSnippetFromFile = "snippetFromFile"; /// /// Constant for SPDX License Info In Snippets field /// + /// The SPDX 2.x JSON field name for the license information in snippets array. internal const string FieldLicenseInfoInSnippets = "licenseInfoInSnippets"; /// /// Constant for SPDX Ranges field /// + /// The SPDX 2.x JSON field name for the ranges array in a snippet. internal const string FieldRanges = "ranges"; /// /// Constant for SPDX Start Pointer field /// + /// The SPDX 2.x JSON field name for the start pointer object in a snippet range. internal const string FieldStartPointer = "startPointer"; /// /// Constant for SPDX End Pointer field /// + /// The SPDX 2.x JSON field name for the end pointer object in a snippet range. internal const string FieldEndPointer = "endPointer"; /// /// Constant for SPDX Offset field /// + /// The SPDX 2.x JSON field name for the byte offset in a snippet pointer. internal const string FieldOffset = "offset"; /// /// Constant for SPDX Line Number field /// + /// The SPDX 2.x JSON field name for the line number in a snippet pointer. internal const string FieldLineNumber = "lineNumber"; /// /// Constant for SPDX Reference field /// + /// The SPDX 2.x JSON field name for the file reference in a snippet pointer. internal const string FieldReference = "reference"; /// /// Constant for SPDX Element ID field /// + /// The SPDX 2.x JSON field name for the source element ID in a relationship. internal const string FieldSpdxElementId = "spdxElementId"; /// /// Constant for SPDX Related Spdx Element field /// + /// The SPDX 2.x JSON field name for the target element ID in a relationship. internal const string FieldRelatedSpdxElement = "relatedSpdxElement"; /// /// Constant for SPDX Relationship Type field /// + /// The SPDX 2.x JSON field name for the relationship type string. internal const string FieldRelationshipType = "relationshipType"; /// /// Constant for SPDX Package Verification Code Excluded Files field /// + /// The SPDX 2.x JSON field name for the excluded files array in a package verification code. internal const string FieldPackageVerificationCodeExcludedFiles = "packageVerificationCodeExcludedFiles"; /// /// Constant for SPDX Package Verification Code Value field /// + /// The SPDX 2.x JSON field name for the hash value in a package verification code. internal const string FieldPackageVerificationCodeValue = "packageVerificationCodeValue"; /// /// Constant for SPDX Reference Category field /// + /// The SPDX 2.x JSON field name for the reference category in an external reference. internal const string FieldReferenceCategory = "referenceCategory"; /// /// Constant for SPDX Reference Type field /// + /// The SPDX 2.x JSON field name for the reference type in an external reference. internal const string FieldReferenceType = "referenceType"; /// /// Constant for SPDX Reference Locator field /// + /// The SPDX 2.x JSON field name for the reference locator in an external reference. internal const string FieldReferenceLocator = "referenceLocator"; /// /// Constant for SPDX Algorithm field /// + /// The SPDX 2.x JSON field name for the checksum algorithm. internal const string FieldAlgorithm = "algorithm"; /// /// Constant for SPDX Checksum Value field /// + /// The SPDX 2.x JSON field name for the checksum hash value. internal const string FieldChecksumValue = "checksumValue"; /// /// Constant for SPDX Annotator field /// + /// The SPDX 2.x JSON field name for the annotator field in an annotation. internal const string FieldAnnotator = "annotator"; /// /// Constant for SPDX Annotation Date field /// + /// The SPDX 2.x JSON field name for the annotation date field. internal const string FieldAnnotationDate = "annotationDate"; /// /// Constant for SPDX Annotation Type field /// + /// The SPDX 2.x JSON field name for the annotation type field. internal const string FieldAnnotationType = "annotationType"; } diff --git a/src/DemaConsulting.SpdxModel/SpdxAnnotation.cs b/src/DemaConsulting.SpdxModel/SpdxAnnotation.cs index 1f91c56..69c0943 100644 --- a/src/DemaConsulting.SpdxModel/SpdxAnnotation.cs +++ b/src/DemaConsulting.SpdxModel/SpdxAnnotation.cs @@ -21,10 +21,14 @@ namespace DemaConsulting.SpdxModel; /// -/// SPDX Annotation class +/// Represents an SPDX annotation — a review or informational comment attached to an SPDX element. /// /// -/// An Annotation is a comment on an SpdxItem by an agent. +/// Annotations support compliance workflows where reviewers document findings and decisions about +/// software components. Each annotation records who made it (), when +/// (), what category it belongs to (), and the free-text +/// content (). See the SpdxAnnotation Design for the full data +/// model and method descriptions. /// public sealed class SpdxAnnotation : SpdxElement { @@ -38,37 +42,57 @@ public sealed class SpdxAnnotation : SpdxElement public static readonly IEqualityComparer Same = new SpdxAnnotationSame(); /// - /// Annotator Field (optional) + /// Annotator Field /// /// /// This field identifies the person, organization, or tool that has /// commented on a file, package, snippet, or the entire document. + /// This field is required for a valid SPDX annotation; + /// will report an error if it is absent. /// public string Annotator { get; set; } = ""; /// - /// Annotation Date Field (optional) + /// Annotation Date Field /// /// /// Identify when the comment was made. This is to be specified according /// to the combined date and time in the UTC format, as specified in the /// ISO 8601 standard. + /// This field is required for a valid SPDX annotation; + /// will report an error if the value is absent or not a valid ISO 8601 date-time. /// public string Date { get; set; } = ""; /// - /// Annotation Type Field (optional) + /// Annotation Type Field /// + /// + /// Indicates the category of the annotation. Valid values are + /// and . + /// This field is required for a valid SPDX annotation; + /// will report an error if the value is . + /// public SpdxAnnotationType Type { get; set; } = SpdxAnnotationType.Missing; /// - /// Annotation Comment field (optional) + /// Annotation Comment field /// + /// + /// Free-text content of the annotation describing the finding or note left by + /// the annotator. This field is required for a valid SPDX annotation; + /// will report an error if the value is absent or empty. + /// public string Comment { get; set; } = ""; /// /// Make a deep-copy of this object /// + /// + /// Returns an independent copy with no shared mutable references — mutating the copy + /// does not affect the original and vice versa. Used by + /// when a new annotation is appended from the other array. + /// /// Deep copy of this object public SpdxAnnotation DeepCopy() { @@ -85,6 +109,12 @@ public SpdxAnnotation DeepCopy() /// /// Enhance missing fields in the annotation /// + /// + /// This operation is additive-only: it never overwrites a non-empty field on + /// this instance. Fields are only populated when they are currently empty/missing. + /// The sentinel is used to detect an absent + /// field. + /// /// Other annotation to enhance with public void Enhance(SpdxAnnotation other) { @@ -110,6 +140,12 @@ public void Enhance(SpdxAnnotation other) /// /// Enhance missing annotations in array /// + /// + /// Matches annotations using (annotator + date + type + comment). + /// For each entry in : if a matching annotation already exists + /// in it is enhanced in place; otherwise a deep copy is appended. + /// The returned array may be larger than the input array. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -142,8 +178,16 @@ public static SpdxAnnotation[] Enhance(SpdxAnnotation[] array, SpdxAnnotation[] /// /// Perform validation of information /// - /// Associated parent node - /// List to populate with issues + /// + /// Issues are appended to rather than thrown as exceptions, + /// allowing all validation errors across the document to be collected in a single pass. + /// The caller is responsible for checking whether any issues were added after this call. + /// + /// + /// Identifier of the parent element (e.g. package or file SPDX-ID) used as + /// a prefix in issue messages so callers can locate the problematic annotation. + /// + /// List to populate with validation issues public void Validate(string parent, List issues) { // Validate Annotator Field @@ -174,9 +218,23 @@ public void Validate(string parent, List issues) /// /// Equality Comparer to test for the same annotation /// + /// + /// Implements identity semantics used by + /// to detect duplicate annotations before merging. Two annotations are considered the + /// same when their , , + /// , and fields are all equal, + /// regardless of their . + /// private sealed class SpdxAnnotationSame : IEqualityComparer { /// + /// + /// Compares the four identity fields: , + /// , , and + /// . The field + /// is intentionally excluded so that an annotation with or without an assigned + /// SPDX-ID is still recognized as the same logical entry. + /// public bool Equals(SpdxAnnotation? a1, SpdxAnnotation? a2) { if (ReferenceEquals(a1, a2)) @@ -196,6 +254,12 @@ public bool Equals(SpdxAnnotation? a1, SpdxAnnotation? a2) } /// + /// + /// Computes the hash from the same four identity fields as : + /// , , + /// , and . + /// Uses HashCode.Combine for a well-distributed composite hash. + /// public int GetHashCode(SpdxAnnotation obj) { return HashCode.Combine( diff --git a/src/DemaConsulting.SpdxModel/SpdxAnnotationType.cs b/src/DemaConsulting.SpdxModel/SpdxAnnotationType.cs index 2a2f4dc..7078fbb 100644 --- a/src/DemaConsulting.SpdxModel/SpdxAnnotationType.cs +++ b/src/DemaConsulting.SpdxModel/SpdxAnnotationType.cs @@ -23,35 +23,64 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Annotation Type enumeration /// +/// +/// The sentinel value (-1) is used internally to represent +/// an absent or uninitialized annotation type and must not be serialized to JSON. +/// will throw if called with +/// . +/// public enum SpdxAnnotationType { /// /// Missing annotation type /// + /// + /// Sentinel value (-1) representing an absent or uninitialized annotation type. + /// will throw + /// if called with this value, and it must never + /// be written to a serialized SPDX document. + /// Missing = -1, /// /// Annotation created during review /// + /// + /// Canonical SPDX 2.x text form: "REVIEW". + /// Review, /// /// Annotation created for other reasons /// + /// + /// Canonical SPDX 2.x text form: "OTHER". + /// Other } /// /// SPDX Annotation Type Extensions /// +/// +/// Provides string ↔ enum conversion for SPDX 2.x JSON serialization. Both directions +/// are consumed by and +/// . +/// public static class SpdxAnnotationTypeExtensions { /// /// Convert text to SpdxAnnotationType /// + /// + /// Matching is case-insensitive: "review", "REVIEW", and "Review" + /// all map to . An empty string maps to + /// . Any other value throws + /// . + /// /// Annotation Type text /// SpdxAnnotationType - /// on error + /// Thrown when is not a recognized SPDX annotation type string. public static SpdxAnnotationType FromText(string annotationType) { return annotationType.ToUpperInvariant() switch @@ -66,9 +95,15 @@ public static SpdxAnnotationType FromText(string annotationType) /// /// Convert SpdxAnnotationType to text /// + /// + /// Returns the uppercase SPDX 2.x text representation of the enum value + /// (e.g., "REVIEW"). + /// Throws for + /// and for any unrecognized numeric value to prevent silent serialization of invalid data. + /// /// SpdxAnnotationType /// Annotation Type text - /// on error + /// Thrown when the enum value is or is not a recognized value. public static string ToText(this SpdxAnnotationType annotationType) { return annotationType switch diff --git a/src/DemaConsulting.SpdxModel/SpdxChecksum.cs b/src/DemaConsulting.SpdxModel/SpdxChecksum.cs index 7f59fa6..bf49502 100644 --- a/src/DemaConsulting.SpdxModel/SpdxChecksum.cs +++ b/src/DemaConsulting.SpdxModel/SpdxChecksum.cs @@ -60,6 +60,11 @@ public sealed class SpdxChecksum /// /// Make a deep-copy of this object /// + /// + /// Deep copy is required to prevent aliasing when checksums are shared between + /// documents during merge operations. Mutating a checksum on one document must not + /// affect the corresponding checksum on another. + /// /// Deep copy of this object public SpdxChecksum DeepCopy() { @@ -73,7 +78,12 @@ public SpdxChecksum DeepCopy() /// /// Enhance missing fields in the checksum /// - /// Other checksum to enhance with + /// + /// This method is called during document merge operations to fill in any + /// fields that are absent in this instance. Fields that are already populated + /// are preserved; only missing (default/empty) values are replaced. + /// + /// Other checksum to enhance with. Must not be null. public void Enhance(SpdxChecksum other) { // Populate the algorithm if missing @@ -89,8 +99,14 @@ public void Enhance(SpdxChecksum other) /// /// Enhance missing checksums in array /// - /// Array to enhance - /// Other array to enhance with + /// + /// Matching uses the comparer (algorithm + value equality). Entries + /// in that match an existing entry are merged in place via the + /// instance method. Entries with no match are + /// deep-copied before being appended to preserve independence from the source array. + /// + /// Array to enhance. Must not be null. + /// Other array to enhance with. Must not be null. /// Updated array public static SpdxChecksum[] Enhance(SpdxChecksum[] array, SpdxChecksum[] others) { @@ -121,8 +137,13 @@ public static SpdxChecksum[] Enhance(SpdxChecksum[] array, SpdxChecksum[] others /// /// Perform validation of information /// + /// + /// Called by containing element validators (e.g., SpdxPackage, SpdxFile, SpdxExternalDocumentReference) + /// to verify algorithm and value fields are populated and consistent. The + /// string is prepended to each issue message to provide context in diagnostic output. + /// /// Associated parent node - /// List to populate with issues + /// List to populate with issues. Must not be null. public void Validate(string parent, List issues) { // Validate Algorithm Field @@ -130,6 +151,10 @@ public void Validate(string parent, List issues) { issues.Add($"{parent} Invalid Checksum Algorithm Field - Missing"); } + else if (!Enum.IsDefined(Algorithm)) + { + issues.Add($"{parent} Invalid Checksum Algorithm Field - Unknown"); + } // Validate Checksum Value Field if (Value.Length == 0) @@ -141,6 +166,11 @@ public void Validate(string parent, List issues) /// /// Equality Comparer to test for the same relationship /// + /// + /// Two checksums are considered the same when both their + /// and fields are equal. This semantics is used to + /// deduplicate and merge checksum arrays during document merge operations. + /// private sealed class SpdxChecksumSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxChecksumAlgorithm.cs b/src/DemaConsulting.SpdxModel/SpdxChecksumAlgorithm.cs index 1276940..2dedca0 100644 --- a/src/DemaConsulting.SpdxModel/SpdxChecksumAlgorithm.cs +++ b/src/DemaConsulting.SpdxModel/SpdxChecksumAlgorithm.cs @@ -23,110 +23,153 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Checksum Algorithm enumeration /// +/// +/// The sentinel value (-1) indicates that no algorithm has been +/// assigned. It is used as a default/uninitialised state and is not a valid SPDX +/// algorithm name. All other members correspond to named SPDX algorithm identifiers. +/// public enum SpdxChecksumAlgorithm { /// /// Missing checksum algorithm /// + /// + /// Sentinel value indicating that no algorithm has been assigned. This value is not + /// a valid SPDX checksum algorithm and must not be serialized to SPDX text form. + /// It is used as the default state before an algorithm is explicitly set. + /// Missing = -1, /// /// SHA-1 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA1. Sha1, /// /// SHA-224 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA224. Sha224, /// /// SHA-256 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA256. Sha256, /// /// SHA-384 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA384. Sha384, /// /// SHA-512 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA512. Sha512, /// /// MD2 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier MD2. Md2, /// /// MD4 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier MD4. Md4, /// /// MD5 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier MD5. Md5, /// /// MD6 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier MD6. Md6, /// /// SHA3-256 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA3-256. Sha3256, /// /// SHA3-384 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA3-384. Sha3384, /// /// SHA3-512 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier SHA3-512. Sha3512, /// /// BLAKE2b-256 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier BLAKE2b-256. Blake2B256, /// /// BLAKE2b-384 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier BLAKE2b-384. Blake2B384, /// /// BLAKE2b-512 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier BLAKE2b-512. Blake2B512, /// /// BLAKE3 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier BLAKE3. Blake3, /// /// ADLER32 checksum algorithm /// + /// Corresponds to the SPDX algorithm identifier ADLER32. Adler32 } /// /// SPDX Checksum Algorithm Extensions /// +/// +/// Provides the factory method and the +/// extension method to convert between SPDX algorithm text strings and the +/// enumeration. Use when +/// parsing SPDX JSON/tag-value input and when serializing back +/// to SPDX text form. +/// public static class SpdxChecksumAlgorithmExtensions { /// /// Convert text to SpdxChecksumAlgorithm /// - /// Checksum algorithm text + /// + /// The input string is converted to upper-case via ToUpperInvariant() before + /// comparison, so the lookup is case-insensitive and locale-independent. An empty + /// string maps to rather than throwing; + /// any other unrecognized value throws . + /// + /// Checksum algorithm text. Must not be null. /// SpdxChecksumAlgorithm - /// on error + /// + /// Thrown when is a non-empty string that does not match any + /// known SPDX checksum algorithm name (case-insensitive comparison is used before the exception is raised). + /// public static SpdxChecksumAlgorithm FromText(string checksumAlgorithm) { return checksumAlgorithm.ToUpperInvariant() switch @@ -156,9 +199,18 @@ public static SpdxChecksumAlgorithm FromText(string checksumAlgorithm) /// /// Convert SpdxChecksumAlgorithm to text /// + /// + /// is intentionally rejected rather than + /// round-tripping to an empty string: serializing a checksum without an algorithm + /// would produce invalid SPDX output, so the caller must ensure only valid, named + /// algorithms are passed to this method. + /// /// SpdxChecksumAlgorithm /// Checksum Algorithm text - /// on error + /// + /// Thrown when is or + /// is a numeric value that is not a named member of the enumeration. + /// public static string ToText(this SpdxChecksumAlgorithm checksumAlgorithm) { return checksumAlgorithm switch diff --git a/src/DemaConsulting.SpdxModel/SpdxCreationInformation.cs b/src/DemaConsulting.SpdxModel/SpdxCreationInformation.cs index 9139043..c808261 100644 --- a/src/DemaConsulting.SpdxModel/SpdxCreationInformation.cs +++ b/src/DemaConsulting.SpdxModel/SpdxCreationInformation.cs @@ -35,6 +35,11 @@ public sealed class SpdxCreationInformation /// /// Regular expression for checking license list versions /// + /// + /// The pattern ^\d+\.\d+$ matches the SPDX major.minor version format (e.g., + /// 3.9 or 3.21). The 100 ms timeout passed to the + /// constructor guards against ReDoS on untrusted or malformed input. + /// private static readonly Regex LicenseListVersionRegex = new( @"^\d+\.\d+$", RegexOptions.None, @@ -68,6 +73,11 @@ public sealed class SpdxCreationInformation /// /// Creator Comment Field (optional) /// + /// + /// An optional free-text comment from the document creators providing supplemental context + /// about the creation process. This field is preserved during deep-copy and propagated + /// during enhance if absent from this instance. + /// public string? Comment { get; set; } /// @@ -82,6 +92,11 @@ public sealed class SpdxCreationInformation /// /// Make a deep-copy of this object /// + /// + /// Returns a structurally independent copy with no shared mutable references. The + /// array is cloned so that mutations to one instance do not + /// affect the other. + /// /// Deep copy of this object public SpdxCreationInformation DeepCopy() { @@ -97,6 +112,11 @@ public SpdxCreationInformation DeepCopy() /// /// Enhance missing fields in the creation information /// + /// + /// Merges into this instance using a union-and-deduplicate strategy + /// for and a fill-if-absent strategy for scalar fields. This method + /// is used during document merge operations to consolidate creation metadata. + /// /// Other creation information to enhance with public void Enhance(SpdxCreationInformation other) { @@ -116,6 +136,13 @@ public void Enhance(SpdxCreationInformation other) /// /// Perform validation of information /// + /// + /// Checks four rules: (1) must be non-empty; (2) each creator entry + /// must start with Person:, Organization:, or Tool:; (3) + /// must be a valid SPDX date-time string when non-empty (empty is permitted for + /// partially-constructed documents); (4) , when present, + /// must match the pattern \d+\.\d+. + /// /// List to populate with issues public void Validate(List issues) { diff --git a/src/DemaConsulting.SpdxModel/SpdxDocument.cs b/src/DemaConsulting.SpdxModel/SpdxDocument.cs index 870e4e4..37eff8d 100644 --- a/src/DemaConsulting.SpdxModel/SpdxDocument.cs +++ b/src/DemaConsulting.SpdxModel/SpdxDocument.cs @@ -25,11 +25,24 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Document class /// +/// +/// is the root container of the SPDX in-memory object model. +/// It aggregates all packages, files, snippets, relationships, and annotations that +/// together form a complete SPDX Software Bill of Materials. The class is +/// to prevent inheritance. All mutable collection and scalar +/// properties are not thread-safe; external synchronization is required when instances +/// are shared across threads. +/// public sealed class SpdxDocument : SpdxElement { /// /// Regular expression for checking SPDX version fields /// + /// + /// The pattern ^SPDX-\d+\.\d+$ matches valid SPDX version strings such as + /// SPDX-2.3. The 100 ms timeout passed to the constructor + /// guards against ReDoS on untrusted or malformed input. + /// private static readonly Regex VersionRegex = new( @"^SPDX-\d+\.\d+$", RegexOptions.None, @@ -48,6 +61,10 @@ public sealed class SpdxDocument : SpdxElement /// /// Document Name Field /// + /// + /// Human-readable name for this SPDX document as defined in SPDX 2.x §2.4. + /// Must be non-empty for a valid document. + /// public string Name { get; set; } = string.Empty; /// @@ -78,16 +95,30 @@ public sealed class SpdxDocument : SpdxElement /// /// SPDX Document Namespace Field /// + /// + /// A unique URI that globally identifies this SPDX document as defined in SPDX 2.x §2.5. + /// Used to qualify element IDs when cross-referencing elements across documents. + /// Must be non-empty for a valid document. + /// public string DocumentNamespace { get; set; } = string.Empty; /// /// Document Comment Field (optional) /// + /// + /// An optional free-text comment about this document. When , + /// the field is absent from serialized output. The value is preserved during + /// deep-copy and propagated by enhance if absent from this instance. + /// public string? Comment { get; set; } /// /// Creation Information /// + /// + /// Mandatory metadata describing who created this document and when as defined in SPDX 2.x §2.7–2.9. + /// One instance is required per document. + /// public SpdxCreationInformation CreationInformation { get; set; } = new(); /// @@ -103,17 +134,28 @@ public sealed class SpdxDocument : SpdxElement /// /// Extracted Licensing Information /// + /// + /// Non-standard license texts extracted from software described by this document, as defined in + /// SPDX 2.x §10. Each entry provides a locally unique license identifier and the full text. + /// public SpdxExtractedLicensingInfo[] ExtractedLicensingInfo { get; set; } = []; /// /// Annotations /// + /// + /// Document-level reviewer or review annotations as defined in SPDX 2.x §12. + /// These annotations apply to the document itself rather than to individual elements. + /// public SpdxAnnotation[] Annotations { get; set; } = []; /// /// Files /// + /// + /// All file elements described in this SPDX document as defined in SPDX 2.x §4. + /// public SpdxFile[] Files { get; set; } = []; /// @@ -127,11 +169,19 @@ public sealed class SpdxDocument : SpdxElement /// /// Snippets /// + /// + /// All snippet elements described in this SPDX document as defined in SPDX 2.x §5. + /// public SpdxSnippet[] Snippets { get; set; } = []; /// /// Relationships /// + /// + /// Relationship elements are intentionally excluded from + /// to avoid them appearing as duplicate elements alongside the source/target elements + /// they connect. They are validated separately via the method. + /// public SpdxRelationship[] Relationships { get; set; } = []; /// @@ -145,6 +195,11 @@ public sealed class SpdxDocument : SpdxElement /// /// Make a deep-copy of this object /// + /// + /// Produces a fully independent object graph — every nested array and object is + /// recursively deep-copied so that mutations to the returned instance have no effect + /// on this instance, and vice versa. This method is stateless and does not throw. + /// /// Deep copy of this object public SpdxDocument DeepCopy() { @@ -171,6 +226,12 @@ public SpdxDocument DeepCopy() /// /// Perform validation of information /// + /// + /// Issues are appended to rather than thrown as exceptions, + /// enabling a complete diagnostic pass in a single call. The method is not thread-safe + /// if the document is mutated concurrently. When is + /// , additional NTIA minimum-element checks are performed. + /// /// List to populate with issues /// Perform NTIA validation public void Validate(List issues, bool ntia = false) @@ -267,6 +328,13 @@ public void Validate(List issues, bool ntia = false) /// /// Get the root packages this document claims to describe /// + /// + /// A package qualifies as a root package if its ID appears in the + /// array, is the target of a DESCRIBES relationship + /// from the document element, or is the source of a DESCRIBED_BY relationship + /// pointing at the document element. All three mechanisms are checked and the results + /// are unioned. + /// /// Array of packages described by this document public SpdxPackage[] GetRootPackages() { @@ -291,6 +359,13 @@ public SpdxPackage[] GetRootPackages() /// /// Get all SPDX elements in the document /// + /// + /// elements are deliberately excluded from the + /// returned sequence. Relationships are not SPDX elements in the same sense as + /// packages, files, and snippets, and including them would cause them to appear + /// alongside the elements they connect during ID-uniqueness checks and other + /// traversals. + /// /// Enumerable of all elements public IEnumerable GetAllElements() { @@ -309,6 +384,12 @@ public IEnumerable GetAllElements() /// /// Get an SPDX element by ID /// + /// + /// Returns the first element whose matches + /// , or if no matching element exists. + /// The search includes the document itself, all files, packages, snippets, and their + /// annotations. Relationships are not searched. + /// /// Element ID /// SPDX element or null public SpdxElement? GetElement(string id) @@ -319,6 +400,12 @@ public IEnumerable GetAllElements() /// /// Get an SPDX element of a specific type /// + /// + /// Delegates to and casts the result to + /// . Returns if no element with + /// exists, or if the element exists but is not of type + /// . + /// /// SPDX element type /// Element ID /// SPDX element or null @@ -328,8 +415,14 @@ public IEnumerable GetAllElements() } /// - /// Equality Comparer to test for the same relationship + /// Equality Comparer to test for the same document /// + /// + /// Two documents are considered the same when their + /// fields are equal and their root-package collections (as returned by + /// ) contain the same packages in any order, + /// compared using . + /// private sealed class SpdxDocumentSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxElement.cs b/src/DemaConsulting.SpdxModel/SpdxElement.cs index d451649..673b3a1 100644 --- a/src/DemaConsulting.SpdxModel/SpdxElement.cs +++ b/src/DemaConsulting.SpdxModel/SpdxElement.cs @@ -25,34 +25,58 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Element base class /// +/// +/// Acts as the abstract base for all identifiable SPDX model objects (documents, packages, +/// files, snippets, relationships, and annotations). Centralizing the identity property here +/// ensures that element lookup, traversal, and duplicate-detection logic works uniformly +/// across the entire object model. +/// public abstract class SpdxElement { /// - /// No Assertion value + /// Sentinel value indicating that a field was intentionally omitted or its value is not known. /// + /// + /// Used by optional fields throughout the SPDX model (e.g., package supplier, originator, and + /// download location) to distinguish an explicit "no assertion" from an absent value. + /// public const string NoAssertion = "NOASSERTION"; /// /// Regular expression for checking element IDs of the form "SPDXRef-name" /// + /// + /// Matches the full pattern ^SPDXRef-[a-zA-Z0-9.-]+$, which allows letters, + /// digits, hyphens, and dots after the mandatory SPDXRef- prefix. + /// Declared as static readonly so a single compiled instance is shared safely + /// across all concurrent callers. The 100 ms timeout is a ReDoS protection measure + /// against pathological input strings from untrusted SPDX sources. + /// protected static readonly Regex SpdxRefRegex = new( "^SPDXRef-[a-zA-Z0-9.-]+$", RegexOptions.None, - TimeSpan.FromMilliseconds(100)); + TimeSpan.FromMilliseconds(100)); // 100 ms timeout guards against ReDoS on untrusted SPDX identifier strings /// /// Gets or sets the Element ID /// /// - /// Uniquely identify any element in an SPDX document which may be - /// referenced by other elements. + /// Uniquely identifies any element in an SPDX document. The value must follow the + /// SPDXRef-<name> format (validated by ) and must + /// be unique within the document. Elements with duplicate or malformed identifiers are + /// reported as validation issues by the concrete subclass Validate methods. /// public string Id { get; set; } = ""; /// /// Enhance missing fields in the element /// - /// Other element to enhance with + /// + /// Called by subclass Enhance methods to propagate the element identity from + /// when the current is empty. This is a no-op if + /// 's is also empty. + /// + /// Source element whose supplies the fallback value. Must not be null. protected void EnhanceElement(SpdxElement other) { // Populate the ID if missing diff --git a/src/DemaConsulting.SpdxModel/SpdxExternalDocumentReference.cs b/src/DemaConsulting.SpdxModel/SpdxExternalDocumentReference.cs index 69a6e7e..82a2c82 100644 --- a/src/DemaConsulting.SpdxModel/SpdxExternalDocumentReference.cs +++ b/src/DemaConsulting.SpdxModel/SpdxExternalDocumentReference.cs @@ -50,16 +50,31 @@ public sealed class SpdxExternalDocumentReference /// /// External Document Checksum Field /// + /// + /// A cryptographic checksum of the referenced SPDX document used for integrity verification. + /// This allows consumers to confirm that the referenced document has not been modified since + /// the reference was created. + /// public SpdxChecksum Checksum { get; set; } = new(); /// /// SPDX Document URI Field /// + /// + /// The URI of the referenced external SPDX document. Must be a valid absolute URI that + /// uniquely identifies the external document's namespace. Used together with + /// to qualify cross-document element references. + /// public string Document { get; set; } = ""; /// /// Make a deep-copy of this object /// + /// + /// Produces an independent copy with no shared mutable references. The nested + /// is deep-copied so that mutations to the returned instance + /// do not affect this instance, and vice versa. + /// /// Deep copy of this object public SpdxExternalDocumentReference DeepCopy() { @@ -72,9 +87,14 @@ public SpdxExternalDocumentReference DeepCopy() } /// - /// Enhance missing fields in the checksum + /// Enhance missing fields in the external document reference /// - /// Other checksum to enhance with + /// + /// Applies a fill-if-absent strategy: each field is updated only when its current + /// value is empty or default. The nested is enhanced in place + /// using the same additive semantics. + /// + /// Other external document reference to enhance with public void Enhance(SpdxExternalDocumentReference other) { // Populate the external document ID if missing @@ -90,6 +110,13 @@ public void Enhance(SpdxExternalDocumentReference other) /// /// Enhance missing external document references in array /// + /// + /// Matching uses the comparer (Document URI equality). Entries in + /// that match an existing entry are merged in place via the + /// instance method. Entries with + /// no match are deep-copied before being appended to preserve independence from the + /// source array. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -123,6 +150,11 @@ public static SpdxExternalDocumentReference[] Enhance(SpdxExternalDocumentRefere /// /// Perform validation of information /// + /// + /// Issues are appended to rather than thrown as exceptions, + /// enabling a complete diagnostic pass across all references in a single call. The + /// nested is validated via . + /// /// List to populate with issues public void Validate(List issues) { @@ -145,6 +177,13 @@ public void Validate(List issues) /// /// Equality Comparer to test for the same external document reference /// + /// + /// Two external document references are considered the same when their + /// URI fields are equal. + /// The field is + /// intentionally excluded from the comparison because the same external document + /// may be referenced under different local aliases in different documents. + /// private sealed class SpdxExternalDocumentReferenceSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxExternalReference.cs b/src/DemaConsulting.SpdxModel/SpdxExternalReference.cs index eee7245..cf38fe4 100644 --- a/src/DemaConsulting.SpdxModel/SpdxExternalReference.cs +++ b/src/DemaConsulting.SpdxModel/SpdxExternalReference.cs @@ -70,11 +70,18 @@ public sealed class SpdxExternalReference /// /// External Reference Comment Field (optional) /// + /// + /// Null when no comment is present. An empty string is not used. + /// public string? Comment { get; set; } /// /// Make a deep-copy of this object /// + /// + /// Used by the static Enhance merge to add new entries without aliasing the source array; also used by callers + /// that need an independent snapshot. + /// /// Deep copy of this object public SpdxExternalReference DeepCopy() { @@ -90,6 +97,12 @@ public SpdxExternalReference DeepCopy() /// /// Enhance missing fields in the external reference /// + /// + /// Mutates this instance in place. is only overwritten when it + /// equals ; all other fields use fitness-based + /// selection (see ). Not thread-safe for concurrent + /// mutation of the same instance. + /// /// Other external reference to enhance with public void Enhance(SpdxExternalReference other) { @@ -112,6 +125,12 @@ public void Enhance(SpdxExternalReference other) /// /// Enhance missing external references in array /// + /// + /// Neither input array is modified; a new array is always returned. Matching uses the + /// comparer (category + type + locator). Entries in + /// that have no match in are appended as independent deep copies so + /// that mutations to the returned array do not affect the source. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -144,6 +163,12 @@ public static SpdxExternalReference[] Enhance(SpdxExternalReference[] array, Spd /// /// Perform validation of information /// + /// + /// Issues are collected non-throwingly into the caller-supplied list. + /// The following fields are validated: (must not be + /// ), (must be non-empty), and + /// (must be non-empty). + /// /// Package name /// List to populate with issues public void Validate(string package, List issues) @@ -170,6 +195,13 @@ public void Validate(string package, List issues) /// /// Equality Comparer to test for the same external reference /// + /// + /// Equality is based on , + /// , and + /// only. is intentionally excluded so that two + /// references pointing to the same resource but carrying different annotations are still + /// recognized as the same entry during merge operations. + /// private sealed class SpdxExternalReferenceSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxExtractedLicensingInfo.cs b/src/DemaConsulting.SpdxModel/SpdxExtractedLicensingInfo.cs index b3b7875..3c47130 100644 --- a/src/DemaConsulting.SpdxModel/SpdxExtractedLicensingInfo.cs +++ b/src/DemaConsulting.SpdxModel/SpdxExtractedLicensingInfo.cs @@ -34,8 +34,7 @@ public sealed class SpdxExtractedLicensingInfo /// Equality comparer for the same extracted licensing info /// /// - /// This considers packages as being the same if they have the same - /// extracted text. + /// This considers extracted licensing infos as being the same if they have the same extracted text. /// public static readonly IEqualityComparer Same = new SpdxExtractedLicensingInfoSame(); @@ -60,21 +59,34 @@ public sealed class SpdxExtractedLicensingInfo /// /// License Name Field /// + /// + /// Null when no name is present. + /// public string? Name { get; set; } /// /// License Cross-Reference Field (optional) /// + /// + /// An empty array when no cross-references are present. + /// public string[] CrossReferences { get; set; } = []; /// /// License Comment Field (optional) /// + /// + /// Null when no comment is present. An empty string is not used. + /// public string? Comment { get; set; } /// /// Make a deep-copy of this object /// + /// + /// Used by the static Enhance merge to add new entries without aliasing the source array; also used by callers + /// that need an independent snapshot. + /// /// Deep copy of this object public SpdxExtractedLicensingInfo DeepCopy() { @@ -91,6 +103,10 @@ public SpdxExtractedLicensingInfo DeepCopy() /// /// Enhance missing fields in the extracted licensing info /// + /// + /// Populates LicenseId, ExtractedText, Name, and Comment using fitness-based selection. + /// CrossReferences are merged by concatenation and deduplication. + /// /// Other extracted licensing info to enhance with public void Enhance(SpdxExtractedLicensingInfo other) { @@ -113,6 +129,10 @@ public void Enhance(SpdxExtractedLicensingInfo other) /// /// Enhance missing extracted licensing info in array /// + /// + /// Matches existing entries by ExtractedText (via the Same comparer) and enhances them; + /// entries with no match are appended as deep copies. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -146,10 +166,14 @@ public static SpdxExtractedLicensingInfo[] Enhance(SpdxExtractedLicensingInfo[] /// /// Perform validation of information /// + /// + /// Validates that LicenseId is non-empty and ExtractedText is non-empty. + /// Issues are appended to ; no exceptions are thrown. + /// /// List to populate with issues public void Validate(List issues) { - // Validate Extracted License ID ID Field + // Validate Extracted License ID Field if (LicenseId.Length == 0) { issues.Add("Extracted License Information Invalid License ID Field - Empty"); @@ -165,9 +189,22 @@ public void Validate(List issues) /// /// Equality Comparer to test for the same extracted licensing info /// + /// + /// Instantiated once and held in the field. Comparison is solely by + /// ; other fields such as + /// and + /// are intentionally excluded so that + /// two entries carrying the same license text but different metadata are still recognized + /// as the same entry during merge operations. + /// private sealed class SpdxExtractedLicensingInfoSame : IEqualityComparer { /// + /// + /// Evaluation order: reference equality is checked first (returns true + /// immediately), then null-safety (either null returns false), then + /// string equality. + /// public bool Equals(SpdxExtractedLicensingInfo? l1, SpdxExtractedLicensingInfo? l2) { if (ReferenceEquals(l1, l2)) @@ -184,6 +221,9 @@ public bool Equals(SpdxExtractedLicensingInfo? l1, SpdxExtractedLicensingInfo? l } /// + /// + /// The hash code is derived solely from . + /// public int GetHashCode(SpdxExtractedLicensingInfo obj) { return obj.ExtractedText.GetHashCode(); diff --git a/src/DemaConsulting.SpdxModel/SpdxFile.cs b/src/DemaConsulting.SpdxModel/SpdxFile.cs index a674407..be090b6 100644 --- a/src/DemaConsulting.SpdxModel/SpdxFile.cs +++ b/src/DemaConsulting.SpdxModel/SpdxFile.cs @@ -23,6 +23,13 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX File Information Element /// +/// +/// Represents the SPDX File element, capturing per-file license, checksum, and contributor +/// information as defined by the SPDX specification. The class is sealed because the +/// SPDX data model does not define any further subtypes of a file element, and sealing +/// prevents unintended subclassing that could introduce inconsistent behavior. Not thread-safe +/// for concurrent mutation of the same instance. +/// public sealed class SpdxFile : SpdxLicenseElement { /// @@ -73,6 +80,9 @@ public sealed class SpdxFile : SpdxLicenseElement /// /// File Comment Field (optional) /// + /// + /// Null when no comment is present. An empty string is not used. + /// public string? Comment { get; set; } /// @@ -99,6 +109,13 @@ public sealed class SpdxFile : SpdxLicenseElement /// /// Make a deep-copy of this object /// + /// + /// All arrays (, , + /// , , + /// , and + /// ) are independently deep-copied; the + /// returned instance shares no mutable state with the original. + /// /// Deep copy of this object public SpdxFile DeepCopy() { @@ -123,6 +140,14 @@ public SpdxFile DeepCopy() /// /// Enhance missing fields in the file /// + /// + /// Non-destructive merge: existing non-empty fields are preserved. String fields use + /// fitness-based selection (concrete value > NOASSERTION > empty > null). + /// , , and + /// are merged by concatenation and deduplication. + /// are merged by identity-match and enhance via + /// . + /// /// Other file to enhance with public void Enhance(SpdxFile other) { @@ -154,6 +179,12 @@ public void Enhance(SpdxFile other) /// /// Enhance missing files in array /// + /// + /// Matching uses the comparer, which is keyed on + /// with a SHA1 checksum tiebreaker: two entries with the same + /// file name but differing SHA1 digests are treated as distinct. Entries in + /// that have no match are appended as independent deep copies. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -186,6 +217,11 @@ public static SpdxFile[] Enhance(SpdxFile[] array, SpdxFile[] others) /// /// Perform validation of information /// + /// + /// Issues are appended to rather than thrown. Nested + /// and are also + /// validated by delegating to their respective Validate methods. + /// /// List to populate with issues public void Validate(List issues) { @@ -222,6 +258,10 @@ public void Validate(List issues) /// /// Equality Comparer to test for the same file /// + /// + /// Equality is based on with a SHA1 checksum tiebreaker: + /// two entries with the same file name but differing SHA1 digests are treated as distinct. + /// private sealed class SpdxFileSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxFileType.cs b/src/DemaConsulting.SpdxModel/SpdxFileType.cs index 3fd4436..faf67c0 100644 --- a/src/DemaConsulting.SpdxModel/SpdxFileType.cs +++ b/src/DemaConsulting.SpdxModel/SpdxFileType.cs @@ -23,75 +23,121 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX File Type enumeration /// +/// +/// Enumerates the file types defined by the SPDX specification. Each value corresponds to +/// a canonical uppercase SPDX text constant used during serialization and deserialization. +/// public enum SpdxFileType { /// /// Human-readable source code /// + /// + /// Corresponds to the SPDX specification text constant SOURCE. + /// Source, /// /// Compiled object, target image or binary executable /// + /// + /// Corresponds to the SPDX specification text constant BINARY. + /// Binary, /// /// File represents an archive /// + /// + /// Corresponds to the SPDX specification text constant ARCHIVE. + /// Archive, /// /// Application file /// + /// + /// Corresponds to the SPDX specification text constant APPLICATION. + /// Application, /// /// Audio file /// + /// + /// Corresponds to the SPDX specification text constant AUDIO. + /// Audio, /// /// Image file /// + /// + /// Corresponds to the SPDX specification text constant IMAGE. + /// Image, /// /// Human-readable text file /// + /// + /// Corresponds to the SPDX specification text constant TEXT. + /// Text, /// /// Video file /// + /// + /// Corresponds to the SPDX specification text constant VIDEO. + /// Video, /// /// Documentation file /// + /// + /// Corresponds to the SPDX specification text constant DOCUMENTATION. + /// Documentation, /// /// SPDX document /// + /// + /// Corresponds to the SPDX specification text constant SPDX. + /// Spdx, /// /// Other type of document not matching standard categories /// + /// + /// Corresponds to the SPDX specification text constant OTHER. + /// Other } /// /// SPDX File Type Extensions /// +/// +/// Static extension companion to that provides serialization +/// helpers for mapping between the enum and the canonical uppercase SPDX document text +/// values. +/// public static class SpdxFileTypeExtensions { /// /// Convert text to SpdxFileType /// + /// + /// Matching is case-insensitive, implemented via ToUpperInvariant before the + /// switch expression. + /// /// File Type text /// SpdxFileType - /// on error + /// Thrown when does not match any known SPDX file type string. public static SpdxFileType FromText(string fileType) { return fileType.ToUpperInvariant() switch @@ -114,9 +160,13 @@ public static SpdxFileType FromText(string fileType) /// /// Convert SpdxFileType to text /// + /// + /// Returned strings are canonical uppercase SPDX representations per the SPDX + /// specification (e.g., SOURCE, BINARY). + /// /// SpdxFileType - /// Annotation Type text - /// on error + /// File Type text + /// Thrown when is not a supported enum value. public static string ToText(this SpdxFileType fileType) { return fileType switch diff --git a/src/DemaConsulting.SpdxModel/SpdxHelpers.cs b/src/DemaConsulting.SpdxModel/SpdxHelpers.cs index 9d5d9cf..8c55e92 100644 --- a/src/DemaConsulting.SpdxModel/SpdxHelpers.cs +++ b/src/DemaConsulting.SpdxModel/SpdxHelpers.cs @@ -28,18 +28,31 @@ internal static partial class SpdxHelpers /// /// Regular expression for checking date/time formats (source-generated for .NET 7+) /// + /// + /// The source-generated variant is used on .NET 7+ because it is AOT-safe: the compiler + /// emits a fully static, allocation-free regex with no runtime compilation step, which is + /// required for Native AOT deployments. Stateless and thread-safe. + /// [GeneratedRegex(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", RegexOptions.None, 100)] private static partial Regex DateTimeRegex(); #else /// /// Cached regular expression instance for checking date/time formats (pre-.NET 7 fallback) /// + /// + /// The instance is initialized once at type load and shared across all calls. Thread-safe + /// because instances are immutable after construction. + /// private static readonly Regex DateTimeRegexInstance = new Regex(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", RegexOptions.None, TimeSpan.FromMilliseconds(100)); /// /// Regular expression for checking date/time formats /// + /// + /// Pre-.NET 7 wrapper that returns the cached to + /// provide the same call site as the source-generated variant above. + /// /// Compiled instance private static Regex DateTimeRegex() => DateTimeRegexInstance; #endif @@ -47,18 +60,37 @@ internal static partial class SpdxHelpers /// /// Test if a string is a valid SPDX date/time field (which include null/empty) /// - /// String value - /// True if valid + /// + /// Null and empty strings are treated as valid because SPDX date/time fields are optional; + /// a null or empty value means "not set", which is permitted by the specification. Non-empty + /// values are validated against the ISO 8601 UTC format (yyyy-MM-ddTHH:mm:ssZ) using + /// a regular expression. Stateless and thread-safe. + /// + /// The timestamp string to validate. Null and empty strings are treated as valid (not-set). + /// + /// Returns true if matches the ISO 8601 UTC format, or if + /// is null or empty (both treated as not-set and therefore valid); + /// false otherwise. + /// internal static bool IsValidSpdxDateTime(string? value) { return string.IsNullOrEmpty(value) || DateTimeRegex().IsMatch(value); } /// - /// This method picks the best string. + /// Returns the highest-fitness string from the supplied candidates. /// + /// + /// Fitness ranking: null=0, empty string=1, NOASSERTION=2, any other concrete value=3. + /// Returns the candidate with the highest fitness. When all candidates are null (or the + /// array is empty), returns null. + /// /// String values to pick from - /// Best string + /// + /// The highest-fitness candidate, selected by: concrete value (non-empty, non-NOASSERTION) + /// > NOASSERTION > empty string > null. Returns null when + /// all candidates are null or the array is empty. + /// internal static string? EnhanceString(params string?[] values) { // Return the value with the highest fitness diff --git a/src/DemaConsulting.SpdxModel/SpdxLicenseElement.cs b/src/DemaConsulting.SpdxModel/SpdxLicenseElement.cs index 51c5ffa..8cd55b2 100644 --- a/src/DemaConsulting.SpdxModel/SpdxLicenseElement.cs +++ b/src/DemaConsulting.SpdxModel/SpdxLicenseElement.cs @@ -23,6 +23,12 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Element with License /// +/// +/// Abstract intermediate base class that centralizes license-related fields (concluded +/// license, copyright text, license comments, attribution notices, and annotations) to +/// remove duplication across , , and +/// . +/// public abstract class SpdxLicenseElement : SpdxElement { /// @@ -75,6 +81,13 @@ public abstract class SpdxLicenseElement : SpdxElement /// /// Enhance missing fields in the license element /// + /// + /// String fields (ConcludedLicense, LicenseComments, CopyrightText) are selected by + /// fitness ranking: concrete value (rank 3) > NOASSERTION (rank 2) > empty string (rank 1) + /// > null (rank 0). AttributionText is merged by concatenation and deduplication. + /// Annotations are merged by identity-match (enhance existing) and append (add new). + /// Also calls EnhanceElement(other) to populate the inherited Id field if absent. + /// /// Other license element to enhance with protected void EnhanceLicenseElement(SpdxLicenseElement other) { diff --git a/src/DemaConsulting.SpdxModel/SpdxPackage.cs b/src/DemaConsulting.SpdxModel/SpdxPackage.cs index d34ed32..1649f84 100644 --- a/src/DemaConsulting.SpdxModel/SpdxPackage.cs +++ b/src/DemaConsulting.SpdxModel/SpdxPackage.cs @@ -21,8 +21,13 @@ namespace DemaConsulting.SpdxModel; /// -/// SPDX Package class +/// Represents an SPDX package — the primary unit of a Software Bill of Materials, capturing identity, provenance, +/// licensing, and verification metadata for a software component. /// +/// +/// An is mutable. Instances are not thread-safe. The comparer matches +/// by and ; it does not consider all fields. +/// public sealed class SpdxPackage : SpdxLicenseElement { /// @@ -127,11 +132,18 @@ public sealed class SpdxPackage : SpdxLicenseElement /// /// Package Checksum Field (optional) /// + /// + /// Multiple algorithms may be present simultaneously (e.g., both SHA-1 and SHA-256) to allow consumers to verify + /// with their preferred algorithm. + /// public SpdxChecksum[] Checksums { get; set; } = []; /// /// Package Home Page Field (optional) /// + /// + /// Should be a valid URI. No URI format validation is performed during . + /// public string? HomePage { get; set; } /// @@ -184,6 +196,9 @@ public sealed class SpdxPackage : SpdxLicenseElement /// /// Package Comment Field (optional) /// + /// + /// Human-readable annotation for this package record. Not interpreted by the library. + /// public string? Comment { get; set; } /// @@ -236,6 +251,11 @@ public sealed class SpdxPackage : SpdxLicenseElement /// /// Make a deep-copy of this object /// + /// + /// All nested objects and arrays (including , , + /// , ) are deep-copied, so the caller is + /// free to mutate the result without affecting the original. + /// /// Deep copy of this object public SpdxPackage DeepCopy() { @@ -275,6 +295,14 @@ public SpdxPackage DeepCopy() /// /// Enhance missing fields in the package /// + /// + /// Field fitness ranking used when choosing which value wins: null < "" < + /// "NOASSERTION" < any concrete value. Array fields such as , + /// , and are merged by deduplication. + /// The nullable field is populated from when null. + /// The array is intentionally not merged: it contains SPDX element IDs that are + /// document-scoped and may not be valid across documents. + /// /// Other package to enhance with public void Enhance(SpdxPackage other) { @@ -299,6 +327,9 @@ public void Enhance(SpdxPackage other) // Populate the download-location field if missing DownloadLocation = SpdxHelpers.EnhanceString(DownloadLocation, other.DownloadLocation) ?? ""; + // Populate the files-analyzed field if missing + FilesAnalyzed ??= other.FilesAnalyzed; + // Enhance or populate the verification code if (VerificationCode != null && other.VerificationCode != null) { @@ -352,6 +383,11 @@ public void Enhance(SpdxPackage other) /// /// Enhance missing packages in array /// + /// + /// Packages are matched using the comparer (by and + /// ). Matching packages are enhanced in place; non-matching packages from + /// are deep-copied and appended. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -384,6 +420,12 @@ public static SpdxPackage[] Enhance(SpdxPackage[] array, SpdxPackage[] others) /// /// Perform validation of information /// + /// + /// When is non-null, each entry in is verified to exist as a file + /// ID in doc.Files. When is true, additionally calls + /// to enforce the NTIA minimum-elements requirements for and + /// . + /// /// List to populate with issues /// Optional document for checking file-references /// Perform NTIA validation @@ -480,6 +522,10 @@ public void Validate(List issues, SpdxDocument? doc, bool ntia = false) /// /// Perform NTIA validation of information /// + /// + /// This is a separate validation path because NTIA minimum-elements compliance is an opt-in check + /// (ntia flag) — not all SPDX documents need to comply with the NTIA minimum element requirements. + /// /// List to populate with issues private void ValidateNtia(List issues) { @@ -499,6 +545,13 @@ private void ValidateNtia(List issues) /// /// Equality Comparer to test for the same package /// + /// + /// Two packages are considered the same when they share the same and + /// . A dedicated nested class is used rather than an ad-hoc lambda so the + /// comparer instance can be stored in the field and passed to LINQ operations. + /// The match key intentionally excludes because different SPDX documents may + /// assign different IDs to the same logical package. + /// private sealed class SpdxPackageSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxPackageVerificationCode.cs b/src/DemaConsulting.SpdxModel/SpdxPackageVerificationCode.cs index 87b19fc..933908f 100644 --- a/src/DemaConsulting.SpdxModel/SpdxPackageVerificationCode.cs +++ b/src/DemaConsulting.SpdxModel/SpdxPackageVerificationCode.cs @@ -18,6 +18,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.Text.RegularExpressions; + namespace DemaConsulting.SpdxModel; /// @@ -32,6 +34,17 @@ namespace DemaConsulting.SpdxModel; /// public sealed class SpdxPackageVerificationCode { + /// + /// Regex for validating SHA-1 hex strings (40 lowercase or uppercase hex digits). + /// + /// + /// The 100 ms timeout guards against ReDoS on untrusted or malformed input. + /// + private static readonly Regex Sha1HexRegex = new( + "^[0-9a-fA-F]{40}$", + RegexOptions.None, + TimeSpan.FromMilliseconds(100)); + /// /// Equality comparer for the same package verification code /// @@ -45,7 +58,7 @@ public sealed class SpdxPackageVerificationCode /// Excluded Files Field /// /// - /// Files that was excluded when calculating the package verification code. + /// Files that were excluded when calculating the package verification code. /// This is usually a file containing SPDX data regarding the package. /// If a package contains more than one SPDX file all SPDX files must be /// excluded from the package verification code. If this is not done it @@ -65,6 +78,10 @@ public sealed class SpdxPackageVerificationCode /// /// Make a deep-copy of this object /// + /// + /// Both and are fully copied, so the caller is free to mutate + /// the result without affecting the original. + /// /// Deep copy of this object public SpdxPackageVerificationCode DeepCopy() { @@ -78,6 +95,10 @@ public SpdxPackageVerificationCode DeepCopy() /// /// Enhance missing fields in the verification code /// + /// + /// entries from are merged by deduplication. + /// is updated only if the current value is null or empty. + /// /// Other verification code to enhance with public void Enhance(SpdxPackageVerificationCode other) { @@ -91,20 +112,26 @@ public void Enhance(SpdxPackageVerificationCode other) /// /// Perform validation of information /// + /// + /// Validation checks that is exactly 40 hex characters (a SHA1 hex digest). No format check + /// is performed on entries. + /// /// Associated package /// List to populate with issues public void Validate(string package, List issues) { // Validate Package Verification Code Value Field - if (Value.Length != 40) + if (Value.Length != 40 || !Sha1HexRegex.IsMatch(Value)) { issues.Add($"Package '{package}' Invalid Package Verification Code Value '{Value}'"); } } - /// - /// Equality Comparer to test for the same package verification code - /// + /// Equality comparer that considers two package verification codes the same when their Value fields are identical. + /// + /// A dedicated nested class is used rather than an ad-hoc lambda so the comparer instance can be stored in the + /// field and passed to LINQ operations without boxing or allocation. + /// private sealed class SpdxPackageVerificationCodeSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/SpdxReferenceCategory.cs b/src/DemaConsulting.SpdxModel/SpdxReferenceCategory.cs index d030d18..fcafa91 100644 --- a/src/DemaConsulting.SpdxModel/SpdxReferenceCategory.cs +++ b/src/DemaConsulting.SpdxModel/SpdxReferenceCategory.cs @@ -23,45 +23,83 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Reference Category enumeration /// +/// +/// Enumerates the broad reference categories defined by the SPDX specification. +/// The sentinel (value -1) is used internally to represent an +/// unset category and must never be serialized to an SPDX document. +/// public enum SpdxReferenceCategory { /// /// Missing reference category /// + /// + /// Sentinel value indicating that no category has been assigned. This value must never + /// be serialized to an SPDX document; see + /// which throws when called with + /// this value. + /// Missing = -1, /// /// Reference for security-related information /// + /// + /// Corresponds to the SPDX specification text constant SECURITY. + /// Security, /// /// Reference for package management information /// + /// + /// Corresponds to the SPDX specification text constant PACKAGE-MANAGER. + /// PackageManager, /// /// Reference for software heritage archive persistent identifier /// + /// + /// Corresponds to the SPDX specification text constant PERSISTENT-ID. + /// PersistentId, /// /// Reference for other reasons /// + /// + /// Corresponds to the SPDX specification text constant OTHER. + /// Other } /// /// SPDX Reference Category Extensions /// +/// +/// Static extension companion to that provides +/// serialization helpers for mapping between the enum and the SPDX document text values +/// defined in the SPDX specification. +/// public static class SpdxReferenceCategoryExtensions { /// /// Convert text to SpdxReferenceCategory /// + /// + /// Matching is case-insensitive (implemented via ToUpperInvariant). Both + /// PACKAGE-MANAGER and PACKAGE_MANAGER are accepted for backward + /// compatibility with documents that use the underscore variant. + /// /// Reference Category text - /// SpdxReferenceCategory - /// on error + /// + /// Returns SpdxReferenceCategory.Missing when category is an empty string; otherwise returns the matching enum + /// value. + /// + /// + /// Thrown when is not a recognized SPDX reference category string. + /// public static SpdxReferenceCategory FromText(string category) { return category.ToUpperInvariant() switch @@ -79,9 +117,17 @@ public static SpdxReferenceCategory FromText(string category) /// /// Convert SpdxReferenceCategory to text /// + /// + /// The output is always the canonical SPDX specification string (e.g., PACKAGE-MANAGER + /// not PACKAGE_MANAGER). Calling this method with + /// always throws; callers must check for the + /// sentinel before serializing. + /// /// SpdxReferenceCategory /// Reference Category text - /// on error + /// + /// Thrown when is SpdxReferenceCategory.Missing or an unsupported enum value. + /// public static string ToText(this SpdxReferenceCategory category) { return category switch diff --git a/src/DemaConsulting.SpdxModel/SpdxRelationship.cs b/src/DemaConsulting.SpdxModel/SpdxRelationship.cs index 2a321f2..6c23f2c 100644 --- a/src/DemaConsulting.SpdxModel/SpdxRelationship.cs +++ b/src/DemaConsulting.SpdxModel/SpdxRelationship.cs @@ -21,10 +21,14 @@ namespace DemaConsulting.SpdxModel; /// -/// SPDX Relationship class +/// Represents a directed relationship between two SPDX elements in an SPDX document. /// /// -/// Relationships referenced in the SPDX document. +/// Relationships define the dependency graph, containment hierarchy, and other associations +/// between packages, files, and snippets. This class provides equality comparison via the +/// and comparers to support array merging and +/// deduplication. Instances are mutable; see for producing independent +/// copies. Not thread-safe for concurrent mutation. /// public sealed class SpdxRelationship : SpdxElement { @@ -67,11 +71,18 @@ public sealed class SpdxRelationship : SpdxElement /// /// Relationship Comment Field /// + /// + /// Optional free-text comment providing human-readable context for the relationship. + /// public string? Comment { get; set; } /// /// Make a deep-copy of this object /// + /// + /// All scalar fields are copied by value. The returned instance is fully independent + /// of the original and may be mutated without side effects. + /// /// Deep copy of this object public SpdxRelationship DeepCopy() { @@ -87,6 +98,10 @@ public SpdxRelationship DeepCopy() /// /// Enhance missing fields in the relationship /// + /// + /// Each field in this instance is replaced only if its current value is null, empty, or the + /// sentinel. Existing non-empty values are never overwritten. + /// /// Other relationship to enhance with public void Enhance(SpdxRelationship other) { @@ -109,6 +124,11 @@ public void Enhance(SpdxRelationship other) /// /// Enhance missing relationships in array /// + /// + /// Relationships are matched using the comparer (by , + /// , and ). Matching relationships are enhanced + /// in place; non-matching relationships from are deep-copied and appended. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -141,6 +161,12 @@ public static SpdxRelationship[] Enhance(SpdxRelationship[] array, SpdxRelations /// /// Perform validation of information /// + /// + /// Validates the SPDX element ID, the related element ID, and the relationship type. When + /// is provided, element IDs are also checked for existence within that document. + /// External references (prefixed with DocumentRef-) and NOASSERTION are accepted without + /// document lookup. + /// /// List to populate with issues /// Optional document for checking references public void Validate(List issues, SpdxDocument? doc) @@ -176,9 +202,22 @@ public void Validate(List issues, SpdxDocument? doc) /// /// Equality Comparer to test for the same relationship /// + /// + /// Two relationships are considered the same when they share the same , + /// , and . + /// A dedicated nested class is used rather than an ad-hoc lambda so the comparer instance can be stored in the + /// field and passed to LINQ operations without boxing or allocation. + /// private sealed class SpdxRelationshipSame : IEqualityComparer { - /// + /// + /// Determines whether two instances are considered the same. + /// + /// + /// Two relationships are considered equal when they share the same , + /// , and . + /// Fields such as are ignored during comparison. + /// public bool Equals(SpdxRelationship? r1, SpdxRelationship? r2) { if (ReferenceEquals(r1, r2)) @@ -196,7 +235,14 @@ public bool Equals(SpdxRelationship? r1, SpdxRelationship? r2) r1.RelatedSpdxElement == r2.RelatedSpdxElement; } - /// + /// + /// Returns a hash code for the specified instance. + /// + /// + /// The hash is derived from the , , + /// and fields, consistent with the equality comparison + /// performed by . + /// public int GetHashCode(SpdxRelationship obj) { return HashCode.Combine( @@ -209,9 +255,22 @@ public int GetHashCode(SpdxRelationship obj) /// /// Equality Comparer to test for the same elements /// + /// + /// Two relationships are considered to share the same elements when they have the same + /// and , + /// regardless of . Used when deduplicating by + /// element endpoints regardless of relationship kind. Backed by the field. + /// private sealed class SpdxRelationshipSameElements : IEqualityComparer { - /// + /// + /// Determines whether two instances share the same elements. + /// + /// + /// Two relationships are considered equal when they share the same and + /// , regardless of + /// . Used when deduplicating by element endpoints only. + /// public bool Equals(SpdxRelationship? r1, SpdxRelationship? r2) { if (ReferenceEquals(r1, r2)) @@ -228,7 +287,14 @@ public bool Equals(SpdxRelationship? r1, SpdxRelationship? r2) r1.RelatedSpdxElement == r2.RelatedSpdxElement; } - /// + /// + /// Returns a hash code for the specified instance. + /// + /// + /// The hash is derived from the and + /// fields, consistent with the equality comparison + /// performed by . + /// public int GetHashCode(SpdxRelationship obj) { return HashCode.Combine(obj.Id, obj.RelatedSpdxElement); diff --git a/src/DemaConsulting.SpdxModel/SpdxRelationshipType.cs b/src/DemaConsulting.SpdxModel/SpdxRelationshipType.cs index 3e2797f..0c568af 100644 --- a/src/DemaConsulting.SpdxModel/SpdxRelationshipType.cs +++ b/src/DemaConsulting.SpdxModel/SpdxRelationshipType.cs @@ -23,253 +23,408 @@ namespace DemaConsulting.SpdxModel; /// /// SPDX Relationship Type enumeration /// +/// +/// The sentinel value (value -1) represents an unknown or uninitialized +/// relationship type during partial deserialization. It must not be serialized to SPDX output; +/// throws +/// for . +/// public enum SpdxRelationshipType { /// /// Missing relationship type /// + /// + /// Sentinel value used internally to represent an absent or uninitialized relationship type. + /// Never serialized to SPDX output. + /// Missing = -1, /// /// Element describes the related element /// + /// + /// Maps to the SPDX relationship type token DESCRIBES. + /// Describes, /// /// Element is described by the related element /// + /// + /// Maps to the SPDX relationship type token DESCRIBED_BY. + /// DescribedBy, /// /// Element contains the related element /// + /// + /// Maps to the SPDX relationship type token CONTAINS. + /// Contains, /// /// Element is contained by the related element /// + /// + /// Maps to the SPDX relationship type token CONTAINED_BY. + /// ContainedBy, /// /// Element depends on the related element /// + /// + /// Maps to the SPDX relationship type token DEPENDS_ON. + /// DependsOn, /// /// Element is a dependency of the related element /// + /// + /// Maps to the SPDX relationship type token DEPENDENCY_OF. + /// DependencyOf, /// /// Element is a manifest file that lists a set of dependencies for the related element /// + /// + /// Maps to the SPDX relationship type token DEPENDENCY_MANIFEST_OF. + /// DependencyManifestOf, /// /// Element is a build dependency of the related element /// + /// + /// Maps to the SPDX relationship type token BUILD_DEPENDENCY_OF. + /// BuildDependencyOf, /// /// Element is a development dependency of the related element /// + /// + /// Maps to the SPDX relationship type token DEV_DEPENDENCY_OF. + /// DevDependencyOf, /// /// Element is an optional dependency of the related element /// + /// + /// Maps to the SPDX relationship type token OPTIONAL_DEPENDENCY_OF. + /// OptionalDependencyOf, /// /// Element is a to-be-provided dependency of the related element /// + /// + /// Maps to the SPDX relationship type token PROVIDED_DEPENDENCY_OF. + /// ProvidedDependencyOf, /// /// Element is a test dependency of the related element /// + /// + /// Maps to the SPDX relationship type token TEST_DEPENDENCY_OF. + /// TestDependencyOf, /// /// Element is a dependency required for the execution of the related element /// + /// + /// Maps to the SPDX relationship type token RUNTIME_DEPENDENCY_OF. + /// RuntimeDependencyOf, /// /// Element is an example of the related element /// + /// + /// Maps to the SPDX relationship type token EXAMPLE_OF. + /// ExampleOf, /// /// Element generates the related element /// + /// + /// Maps to the SPDX relationship type token GENERATES. + /// Generates, /// /// Element was generated from the related element /// + /// + /// Maps to the SPDX relationship type token GENERATED_FROM. + /// GeneratedFrom, /// /// Element is an ancestor (same lineage but pre-dated) the related element /// + /// + /// Maps to the SPDX relationship type token ANCESTOR_OF. + /// AncestorOf, /// /// Element is a descendant of (same lineage but post-dates) the related element /// + /// + /// Maps to the SPDX relationship type token DESCENDANT_OF. + /// DescendantOf, /// /// Element is a variant of (same lineage but not clear which came first) the related element /// + /// + /// Maps to the SPDX relationship type token VARIANT_OF. + /// VariantOf, /// /// Element is a distribution artifact of the related element /// + /// + /// Maps to the SPDX relationship type token DISTRIBUTION_ARTIFACT. + /// DistributionArtifact, /// /// Element is a patch file for (to be applied to) the related element /// + /// + /// Maps to the SPDX relationship type token PATCH_FOR. + /// PatchFor, /// /// Element is a patch file that has been applied to the related element /// + /// + /// Maps to the SPDX relationship type token PATCH_APPLIED. + /// PatchApplied, /// /// Element is an exact copy of the related element /// + /// + /// Maps to the SPDX relationship type token COPY_OF. + /// CopyOf, /// /// Element is a file that was added to the related element /// + /// + /// Maps to the SPDX relationship type token FILE_ADDED. + /// FileAdded, /// /// Element is a file that was deleted from the related element /// + /// + /// Maps to the SPDX relationship type token FILE_DELETED. + /// FileDeleted, /// /// Element is a file that was modified from the related element /// + /// + /// Maps to the SPDX relationship type token FILE_MODIFIED. + /// FileModified, /// /// Element has been expanded from an archive file /// + /// + /// Maps to the SPDX relationship type token EXPANDED_FROM_ARCHIVE. + /// ExpandedFromArchive, /// /// Element dynamically links to the related element /// + /// + /// Maps to the SPDX relationship type token DYNAMIC_LINK. + /// DynamicLink, /// /// Element statically links to the related element /// + /// + /// Maps to the SPDX relationship type token STATIC_LINK. + /// StaticLink, /// /// Element is a data file used by the related element /// + /// + /// Maps to the SPDX relationship type token DATA_FILE_OF. + /// DataFileOf, /// - /// Element is a test cased used in testing the related element + /// Element is a test case used in testing the related element /// + /// + /// Maps to the SPDX relationship type token TEST_CASE_OF. + /// TestCaseOf, /// /// Element is used to build the related element /// + /// + /// Maps to the SPDX relationship type token BUILD_TOOL_OF. + /// BuildToolOf, /// /// Element is used as a development tool for the related element /// + /// + /// Maps to the SPDX relationship type token DEV_TOOL_OF. + /// DevToolOf, /// /// Element is used for testing the related element /// + /// + /// Maps to the SPDX relationship type token TEST_OF. + /// TestOf, /// /// Element is used as a test tool for the related element /// + /// + /// Maps to the SPDX relationship type token TEST_TOOL_OF. + /// TestToolOf, /// /// Element provides documentation of the related element /// + /// + /// Maps to the SPDX relationship type token DOCUMENTATION_OF. + /// DocumentationOf, /// /// Element is an optional component of the related element /// + /// + /// Maps to the SPDX relationship type token OPTIONAL_COMPONENT_OF. + /// OptionalComponentOf, /// /// Element is a metafile of the related element /// + /// + /// Maps to the SPDX relationship type token METAFILE_OF. + /// MetafileOf, /// /// Element is a package as part of the related element /// + /// + /// Maps to the SPDX relationship type token PACKAGE_OF. + /// PackageOf, /// /// Element is an SPDX document amending the SPDX information in the related element /// + /// + /// Maps to the SPDX relationship type token AMENDS. + /// Amends, /// /// Element is a prerequisite for the related element /// + /// + /// Maps to the SPDX relationship type token PREREQUISITE_FOR. + /// PrerequisiteFor, /// /// Element has a prerequisite of the related element /// + /// + /// Maps to the SPDX relationship type token HAS_PREREQUISITE. + /// HasPrerequisite, /// /// Element describes, illustrates, or specifies a requirement statement for the related element /// + /// + /// Maps to the SPDX relationship type token REQUIREMENT_DESCRIPTION_FOR. + /// RequirementDescriptionFor, /// /// Element describes, illustrates, or defines a design specification for the related element /// + /// + /// Maps to the SPDX relationship type token SPECIFICATION_FOR. + /// SpecificationFor, /// /// Element has a relationship with the related element described in the comment field /// + /// + /// Maps to the SPDX relationship type token OTHER. + /// Other } /// /// SPDX Relationship Type Extensions /// +/// +/// This class is stateless and thread-safe. Note the asymmetry: accepts an empty string and +/// returns , but throws for +/// . This design prevents silent serialization of sentinel values while +/// permitting partial deserialization. +/// public static class SpdxRelationshipTypeExtensions { /// /// Convert text to SpdxRelationshipType /// + /// + /// Comparison is case-insensitive. An empty or null input maps to . + /// All 45 SPDX 2.x relationship type tokens are recognized. + /// /// Relationship Type text /// SpdxRelationshipType - /// on error + /// Thrown when is not a recognized SPDX relationship type string. public static SpdxRelationshipType FromText(string relationshipType) { - return relationshipType.ToUpperInvariant() switch + return (relationshipType ?? string.Empty).ToUpperInvariant() switch { "" => SpdxRelationshipType.Missing, "DESCRIBES" => SpdxRelationshipType.Describes, @@ -324,9 +479,14 @@ public static SpdxRelationshipType FromText(string relationshipType) /// /// Convert SpdxRelationshipType to text /// + /// + /// This is an extension method so it can be called directly on a value. + /// Throws for the sentinel to prevent accidental serialization + /// of incomplete relationship data. + /// /// SpdxRelationshipType /// Relationship Type text - /// on error + /// Thrown when is or an unrecognized enum value. public static string ToText(this SpdxRelationshipType relationshipType) { return relationshipType switch diff --git a/src/DemaConsulting.SpdxModel/SpdxSnippet.cs b/src/DemaConsulting.SpdxModel/SpdxSnippet.cs index cb24a01..7ad4b3b 100644 --- a/src/DemaConsulting.SpdxModel/SpdxSnippet.cs +++ b/src/DemaConsulting.SpdxModel/SpdxSnippet.cs @@ -21,10 +21,13 @@ namespace DemaConsulting.SpdxModel; /// -/// SPDX Snippet Class +/// Represents a portion of a file in an SPDX document with distinct licensing or provenance. /// /// -/// Snippets referenced in the SPDX document +/// Snippets are used when a specific byte or line range within a file has different licensing +/// or provenance from the rest of the file, enabling granular compliance tracking for reused +/// code segments. This class is not thread-safe; callers must synchronize access when sharing +/// instances across threads. /// public sealed class SpdxSnippet : SpdxLicenseElement { @@ -49,21 +52,34 @@ public sealed class SpdxSnippet : SpdxLicenseElement /// /// Snippet Byte Range Start Field /// + /// + /// Must be ≥ 1. Validated by . Used as part of the snippet identity key for array merging. + /// public int SnippetByteStart { get; set; } /// /// Snippet Byte Range End Field /// + /// + /// Must be ≥ . Validated by . Used as part of the snippet + /// identity key for array merging. + /// public int SnippetByteEnd { get; set; } /// /// Snippet Line Range Start Field /// + /// + /// 0 signifies that the line range start is not specified. Not validated by . + /// public int SnippetLineStart { get; set; } /// /// Snippet Line Range End Field /// + /// + /// 0 signifies that the line range end is not specified. Not validated by . + /// public int SnippetLineEnd { get; set; } /// @@ -80,19 +96,27 @@ public sealed class SpdxSnippet : SpdxLicenseElement /// /// Snippet Comment Field (optional) /// + /// + /// Optional free-text comment providing human-readable context for the snippet. + /// public string? Comment { get; set; } /// /// Snippet Name Field (optional) /// /// - /// Identify name of this snippet. + /// Optional human-readable name for this snippet. /// public string? Name { get; set; } /// /// Make a deep-copy of this object /// + /// + /// All nested arrays (including , , + /// ) are deep-copied, so the caller is free to mutate the result without + /// affecting the original. + /// /// Deep copy of this object public SpdxSnippet DeepCopy() { @@ -118,6 +142,10 @@ public SpdxSnippet DeepCopy() /// /// Enhance missing fields in the snippet /// + /// + /// Field fitness ranking: 0 or empty values are replaced by non-zero / non-empty values from + /// . Array fields such as are merged by deduplication. + /// /// Other snippet to enhance with public void Enhance(SpdxSnippet other) { @@ -164,6 +192,11 @@ public void Enhance(SpdxSnippet other) /// /// Enhance missing snippets in array /// + /// + /// Snippets are matched using the comparer (by , byte start, and + /// byte end). Matching snippets are enhanced in place; non-matching snippets from are + /// deep-copied and appended. + /// /// Array to enhance /// Other array to enhance with /// Updated array @@ -196,6 +229,12 @@ public static SpdxSnippet[] Enhance(SpdxSnippet[] array, SpdxSnippet[] others) /// /// Perform validation of information /// + /// + /// Validates snippet ID, , byte range ( ≥ 1, + /// ), , + /// , and annotations. Missing required fields, invalid byte ranges, + /// malformed IDs, and invalid annotations are recorded in . + /// /// List to populate with issues public void Validate(List issues) { @@ -245,6 +284,12 @@ public void Validate(List issues) /// /// Equality Comparer to test for the same snippet /// + /// + /// Two snippets are considered the same when they share the same , + /// , and . This is the backing implementation + /// for . A dedicated nested class is used rather than an ad-hoc lambda so the + /// comparer instance can be stored as a field and passed to LINQ operations without boxing or allocation. + /// private sealed class SpdxSnippetSame : IEqualityComparer { /// diff --git a/src/DemaConsulting.SpdxModel/Transform/SpdxRelationships.cs b/src/DemaConsulting.SpdxModel/Transform/SpdxRelationships.cs index 2a3d78a..0e9d185 100644 --- a/src/DemaConsulting.SpdxModel/Transform/SpdxRelationships.cs +++ b/src/DemaConsulting.SpdxModel/Transform/SpdxRelationships.cs @@ -1,18 +1,75 @@ +// Copyright(c) 2024 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. + namespace DemaConsulting.SpdxModel.Transform; /// /// Transformations for SPDX relationships. /// +/// +/// This class is a stateless utility: all methods are static and carry no instance +/// state. The same instance (or the class itself) may be called concurrently from +/// multiple threads provided each call operates on a different ; +/// concurrent calls on the same document are not safe without external synchronization. +/// All mutating methods operate directly on . +/// public static class SpdxRelationships { /// /// Add new relationships to the SPDX document. /// - /// SPDX document + /// + /// All incoming relationships are validated before any mutation is performed. + /// This ensures that a validation failure leaves the document in its original state + /// (atomic with respect to the batch). Replacement uses + /// (type-agnostic, matches by source and + /// target ID only) so that a replace-and-add can change the relationship type between + /// the same pair of elements. Individual deduplication within the add path uses + /// (type-inclusive) so that relationships of + /// different types between the same elements co-exist correctly. + /// Side-effect: mutates .. + /// + /// SPDX document to modify /// New relationships to add - /// True to replace existing relationships + /// + /// When true, existing relationships whose source and target match any incoming + /// relationship are removed before the new relationships are added. + /// + /// + /// Thrown when any relationship's source element ID is not found in the document, + /// or when any relationship's target element ID is not found in the document and is + /// neither NOASSERTION nor prefixed with DocumentRef-. + /// When this exception is thrown, the document is left unmodified. + /// public static void Add(SpdxDocument document, IEnumerable relationships, bool replace = false) { + // Materialize the enumerable so it can be iterated multiple times + var incoming = relationships.ToArray(); + + // Pre-validate all relationships before making any mutations. + // This ensures the document is left unmodified if any relationship is invalid. + foreach (var relationship in incoming) + { + ValidateRelationship(document, relationship); + } + // Handle replacing existing relationships if (replace) { @@ -20,24 +77,59 @@ public static void Add(SpdxDocument document, IEnumerable rela document.Relationships = [ ..document.Relationships - .Where(r => !relationships.Any(r2 => SpdxRelationship.SameElements.Equals(r, r2))) + .Where(r => !incoming.Any(r2 => SpdxRelationship.SameElements.Equals(r, r2))) ]; } - // Add the new relationships - foreach (var relationship in relationships) + // Add the new relationships (validation already passed, so no exceptions expected here) + foreach (var relationship in incoming) { - Add(document, relationship); + AddValidated(document, relationship); } } /// /// Add a relationship to the SPDX document. /// - /// SPDX document + /// + /// If a relationship with the same source, target, and type already exists (as determined + /// by ), the existing entry is enhanced with any + /// additional field values from . Otherwise a deep copy is + /// appended to . + /// Side-effect: mutates .. + /// + /// SPDX document to modify /// SPDX relationship to add - /// Thrown if the relationship argument is invalid + /// + /// Thrown when the relationship's source element ID () + /// is not found in the document, or when the target element ID + /// () is not found in the document and + /// is neither NOASSERTION nor prefixed with DocumentRef-. + /// public static void Add(SpdxDocument document, SpdxRelationship relationship) + { + // Validate before any mutation + ValidateRelationship(document, relationship); + + // Perform the actual add (validation already passed) + AddValidated(document, relationship); + } + + /// + /// Validates a relationship against the document without mutating it. + /// + /// + /// This method performs all ID-existence checks WITHOUT mutating the document. + /// It MUST be called before any mutation (replacement or addition) to preserve + /// the atomicity guarantee: if validation fails the document remains unchanged. + /// + /// SPDX document providing the element registry + /// Relationship to validate + /// + /// Thrown when the source element ID is not found in the document, or when the target + /// element ID is not found and is neither NOASSERTION nor DocumentRef--prefixed. + /// + private static void ValidateRelationship(SpdxDocument document, SpdxRelationship relationship) { // Ensure the relationship ID matches an element if (document.GetElement(relationship.Id) == null) @@ -53,7 +145,20 @@ public static void Add(SpdxDocument document, SpdxRelationship relationship) throw new ArgumentException($"Element {relationship.RelatedSpdxElement} not found in SPDX document", nameof(relationship)); } + } + /// + /// Adds a pre-validated relationship to the document (no validation performed). + /// + /// + /// Callers MUST invoke on + /// before calling this method. No further ID validation is performed here; passing an + /// unvalidated relationship may produce an inconsistent document. + /// + /// SPDX document to modify + /// Already-validated relationship to add + private static void AddValidated(SpdxDocument document, SpdxRelationship relationship) + { // Look for an existing relationship var existing = Array.Find(document.Relationships, r => SpdxRelationship.Same.Equals(r, relationship)); if (existing != null) diff --git a/test/DemaConsulting.SpdxModel.Tests/AssemblyInfo.cs b/test/DemaConsulting.SpdxModel.Tests/AssemblyInfo.cs index 0562131..7fd3270 100644 --- a/test/DemaConsulting.SpdxModel.Tests/AssemblyInfo.cs +++ b/test/DemaConsulting.SpdxModel.Tests/AssemblyInfo.cs @@ -18,6 +18,3 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -using Microsoft.VisualStudio.TestTools.UnitTesting; - -[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] diff --git a/test/DemaConsulting.SpdxModel.Tests/DemaConsulting.SpdxModel.Tests.csproj b/test/DemaConsulting.SpdxModel.Tests/DemaConsulting.SpdxModel.Tests.csproj index 4812d0d..16c11ec 100644 --- a/test/DemaConsulting.SpdxModel.Tests/DemaConsulting.SpdxModel.Tests.csproj +++ b/test/DemaConsulting.SpdxModel.Tests/DemaConsulting.SpdxModel.Tests.csproj @@ -6,6 +6,7 @@ latest enable enable + Exe false @@ -30,8 +31,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -68,7 +72,7 @@ - + diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize22.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize22.cs index 797329e..2d6d09c 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize22.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize22.cs @@ -25,162 +25,170 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX 2.2 JSON documents to classes. /// -[TestClass] +/// +/// Exercises end-to-end deserialization of a real SPDX 2.2 JSON example document +/// (embedded resource) using xUnit v3 as the test framework. +/// public class Spdx2JsonDeserialize22 { /// /// Test parsing SPDX 2.2 JSON document. /// - [TestMethod] - public void Spdx2JsonDeserializer_Deserialize_ValidSpdx22JsonReturnsExpectedDocument() + /// + /// Uses the canonical SPDX 2.2 JSON example bundled as an embedded resource. Verifies + /// document-level fields, external document references, extracted licensing info, + /// annotations, files, snippets, and relationships are all correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_Deserialize_ValidSpdx22Json_ReturnsExpectedDocument() { // Arrange: Load the SPDX 2.2 JSON example from embedded resources var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.2.spdx.json"); + + // Act: Deserialize the JSON document var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); - // Act: Validate the document + // Assert: Verify that the document is valid + Assert.NotNull(doc); var issues = new List(); doc.Validate(issues); - - // Assert: Verify that there are no validation issues - Assert.IsEmpty(issues); + Assert.Empty(issues); // Assert: Verify the document properties - Assert.AreEqual("SPDX-Tools-v2.0", doc.Name); - Assert.AreEqual("SPDX-2.2", doc.Version); - Assert.AreEqual("http://spdx.org/spdxdocs/spdx-example-json-2.2-444504E0-4F89-41D3-9A0C-0305E82C3301", + Assert.Equal("SPDX-Tools-v2.0", doc.Name); + Assert.Equal("SPDX-2.2", doc.Version); + Assert.Equal("http://spdx.org/spdxdocs/spdx-example-json-2.2-444504E0-4F89-41D3-9A0C-0305E82C3301", doc.DocumentNamespace); - Assert.AreEqual("This document was created using SPDX 2.0 using licenses from the web site.", doc.Comment); - Assert.HasCount(3, doc.CreationInformation.Creators); - Assert.AreEqual("Tool: LicenseFind-1.0", doc.CreationInformation.Creators[0]); - Assert.AreEqual("Organization: ExampleCodeInspect ()", doc.CreationInformation.Creators[1]); - Assert.AreEqual("Person: Jane Doe ()", doc.CreationInformation.Creators[2]); - Assert.AreEqual("2010-01-29T18:30:22Z", doc.CreationInformation.Created); + Assert.Equal("This document was created using SPDX 2.0 using licenses from the web site.", doc.Comment); + Assert.Equal(3, doc.CreationInformation.Creators.Length); + Assert.Equal("Tool: LicenseFind-1.0", doc.CreationInformation.Creators[0]); + Assert.Equal("Organization: ExampleCodeInspect ()", doc.CreationInformation.Creators[1]); + Assert.Equal("Person: Jane Doe ()", doc.CreationInformation.Creators[2]); + Assert.Equal("2010-01-29T18:30:22Z", doc.CreationInformation.Created); Assert.StartsWith("This package has been shipped in source and", doc.CreationInformation.Comment); - Assert.AreEqual("3.9", doc.CreationInformation.LicenseListVersion); + Assert.Equal("3.9", doc.CreationInformation.LicenseListVersion); // Assert: Verify external document references - Assert.HasCount(1, doc.ExternalDocumentReferences); - Assert.AreEqual("DocumentRef-spdx-tool-1.2", doc.ExternalDocumentReferences[0].ExternalDocumentId); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, doc.ExternalDocumentReferences[0].Checksum.Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2759", doc.ExternalDocumentReferences[0].Checksum.Value); - Assert.AreEqual("http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301", + Assert.Single(doc.ExternalDocumentReferences); + Assert.Equal("DocumentRef-spdx-tool-1.2", doc.ExternalDocumentReferences[0].ExternalDocumentId); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, doc.ExternalDocumentReferences[0].Checksum.Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2759", doc.ExternalDocumentReferences[0].Checksum.Value); + Assert.Equal("http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301", doc.ExternalDocumentReferences[0].Document); // Assert: Verify extracted licensing info - Assert.HasCount(5, doc.ExtractedLicensingInfo); - Assert.AreEqual("LicenseRef-Beerware-4.2", doc.ExtractedLicensingInfo[0].LicenseId); + Assert.Equal(5, doc.ExtractedLicensingInfo.Length); + Assert.Equal("LicenseRef-Beerware-4.2", doc.ExtractedLicensingInfo[0].LicenseId); Assert.StartsWith("\"THE BEER-WARE LICENSE\"", doc.ExtractedLicensingInfo[0].ExtractedText); - Assert.AreEqual("LicenseRef-4", doc.ExtractedLicensingInfo[1].LicenseId); + Assert.Equal("LicenseRef-4", doc.ExtractedLicensingInfo[1].LicenseId); Assert.StartsWith("/*\n * (c) Copyright 2009 University of Bristol", doc.ExtractedLicensingInfo[1].ExtractedText); - Assert.AreEqual("LicenseRef-3", doc.ExtractedLicensingInfo[2].LicenseId); + Assert.Equal("LicenseRef-3", doc.ExtractedLicensingInfo[2].LicenseId); Assert.StartsWith("The CyberNeko Software License", doc.ExtractedLicensingInfo[2].ExtractedText); - Assert.AreEqual("CyberNeko License", doc.ExtractedLicensingInfo[2].Name); - Assert.HasCount(2, doc.ExtractedLicensingInfo[2].CrossReferences); - Assert.AreEqual("http://people.apache.org/~andyc/neko/LICENSE", + Assert.Equal("CyberNeko License", doc.ExtractedLicensingInfo[2].Name); + Assert.Equal(2, doc.ExtractedLicensingInfo[2].CrossReferences.Length); + Assert.Equal("http://people.apache.org/~andyc/neko/LICENSE", doc.ExtractedLicensingInfo[2].CrossReferences[0]); - Assert.AreEqual("http://justasample.url.com", doc.ExtractedLicensingInfo[2].CrossReferences[1]); - Assert.AreEqual("This is the CyperNeko License", doc.ExtractedLicensingInfo[2].Comment); - Assert.AreEqual("LicenseRef-2", doc.ExtractedLicensingInfo[3].LicenseId); + Assert.Equal("http://justasample.url.com", doc.ExtractedLicensingInfo[2].CrossReferences[1]); + Assert.Equal("This is the CyperNeko License", doc.ExtractedLicensingInfo[2].Comment); + Assert.Equal("LicenseRef-2", doc.ExtractedLicensingInfo[3].LicenseId); Assert.StartsWith("This package includes the", doc.ExtractedLicensingInfo[3].ExtractedText); - Assert.AreEqual("LicenseRef-1", doc.ExtractedLicensingInfo[4].LicenseId); + Assert.Equal("LicenseRef-1", doc.ExtractedLicensingInfo[4].LicenseId); Assert.StartsWith("/*\n * (c) Copyright 2000, 2001, 2002", doc.ExtractedLicensingInfo[4].ExtractedText); // Assert: Verify annotations - Assert.HasCount(3, doc.Annotations); - Assert.AreEqual("Person: Jane Doe ()", doc.Annotations[0].Annotator); - Assert.AreEqual("2010-01-29T18:30:22Z", doc.Annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Other, doc.Annotations[0].Type); - Assert.AreEqual("Document level annotation", doc.Annotations[0].Comment); - Assert.AreEqual("Person: Suzanne Reviewer", doc.Annotations[1].Annotator); - Assert.AreEqual("2011-03-13T00:00:00Z", doc.Annotations[1].Date); - Assert.AreEqual(SpdxAnnotationType.Review, doc.Annotations[1].Type); - Assert.AreEqual("Another example reviewer.", doc.Annotations[1].Comment); - Assert.AreEqual("Person: Joe Reviewer", doc.Annotations[2].Annotator); - Assert.AreEqual("2010-02-10T00:00:00Z", doc.Annotations[2].Date); - Assert.AreEqual(SpdxAnnotationType.Review, doc.Annotations[2].Type); + Assert.Equal(3, doc.Annotations.Length); + Assert.Equal("Person: Jane Doe ()", doc.Annotations[0].Annotator); + Assert.Equal("2010-01-29T18:30:22Z", doc.Annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Other, doc.Annotations[0].Type); + Assert.Equal("Document level annotation", doc.Annotations[0].Comment); + Assert.Equal("Person: Suzanne Reviewer", doc.Annotations[1].Annotator); + Assert.Equal("2011-03-13T00:00:00Z", doc.Annotations[1].Date); + Assert.Equal(SpdxAnnotationType.Review, doc.Annotations[1].Type); + Assert.Equal("Another example reviewer.", doc.Annotations[1].Comment); + Assert.Equal("Person: Joe Reviewer", doc.Annotations[2].Annotator); + Assert.Equal("2010-02-10T00:00:00Z", doc.Annotations[2].Date); + Assert.Equal(SpdxAnnotationType.Review, doc.Annotations[2].Type); Assert.StartsWith("This is just an example", doc.Annotations[2].Comment); // Assert: Verify files - Assert.HasCount(4, doc.Files); - Assert.AreEqual("SPDXRef-DoapSource", doc.Files[0].Id); - Assert.AreEqual("./src/org/spdx/parser/DOAPProject.java", doc.Files[0].FileName); - Assert.HasCount(1, doc.Files[0].FileTypes); - Assert.IsTrue(doc.Files[0].FileTypes.Contains(SpdxFileType.Source)); - Assert.HasCount(1, doc.Files[0].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, doc.Files[0].Checksums[0].Algorithm); - Assert.AreEqual("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", doc.Files[0].Checksums[0].Value); - Assert.AreEqual("Apache-2.0", doc.Files[0].ConcludedLicense); - Assert.HasCount(1, doc.Files[0].LicenseInfoInFiles); - Assert.AreEqual("Apache-2.0", doc.Files[0].LicenseInfoInFiles[0]); - Assert.AreEqual("Copyright 2010, 2011 Source Auditor Inc.", doc.Files[0].CopyrightText); - Assert.HasCount(5, doc.Files[0].Contributors); - Assert.AreEqual("Protecode Inc.", doc.Files[0].Contributors[0]); - Assert.AreEqual("SPDX Technical Team Members", doc.Files[0].Contributors[1]); - Assert.AreEqual("Open Logic Inc.", doc.Files[0].Contributors[2]); - Assert.AreEqual("Source Auditor Inc.", doc.Files[0].Contributors[3]); - Assert.AreEqual("Black Duck Software Inc.", doc.Files[0].Contributors[4]); - Assert.AreEqual("This file is used by Jena", doc.Files[1].Comment); + Assert.Equal(4, doc.Files.Length); + Assert.Equal("SPDXRef-DoapSource", doc.Files[0].Id); + Assert.Equal("./src/org/spdx/parser/DOAPProject.java", doc.Files[0].FileName); + Assert.Single(doc.Files[0].FileTypes); + Assert.Contains(SpdxFileType.Source, doc.Files[0].FileTypes); + Assert.Single(doc.Files[0].Checksums); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, doc.Files[0].Checksums[0].Algorithm); + Assert.Equal("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", doc.Files[0].Checksums[0].Value); + Assert.Equal("Apache-2.0", doc.Files[0].ConcludedLicense); + Assert.Single(doc.Files[0].LicenseInfoInFiles); + Assert.Equal("Apache-2.0", doc.Files[0].LicenseInfoInFiles[0]); + Assert.Equal("Copyright 2010, 2011 Source Auditor Inc.", doc.Files[0].CopyrightText); + Assert.Equal(5, doc.Files[0].Contributors.Length); + Assert.Equal("Protecode Inc.", doc.Files[0].Contributors[0]); + Assert.Equal("SPDX Technical Team Members", doc.Files[0].Contributors[1]); + Assert.Equal("Open Logic Inc.", doc.Files[0].Contributors[2]); + Assert.Equal("Source Auditor Inc.", doc.Files[0].Contributors[3]); + Assert.Equal("Black Duck Software Inc.", doc.Files[0].Contributors[4]); + Assert.Equal("This file is used by Jena", doc.Files[1].Comment); Assert.StartsWith("Apache Commons Lang\nCopyright 2001-2011", doc.Files[1].Notice); - Assert.AreEqual("This license is used by Jena", doc.Files[2].LicenseComments); - Assert.HasCount(1, doc.Files[3].Annotations); - Assert.AreEqual("Person: File Commenter", doc.Files[3].Annotations[0].Annotator); - Assert.AreEqual("2011-01-29T18:30:22Z", doc.Files[3].Annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Other, doc.Files[3].Annotations[0].Type); - Assert.AreEqual("File level annotation", doc.Files[3].Annotations[0].Comment); - Assert.HasCount(2, doc.Files[3].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, doc.Files[3].Checksums[0].Algorithm); - Assert.AreEqual("624c1abb3664f4b35547e7c73864ad24", doc.Files[3].Checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, doc.Files[3].Checksums[1].Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2758", doc.Files[3].Checksums[1].Value); + Assert.Equal("This license is used by Jena", doc.Files[2].LicenseComments); + Assert.Single(doc.Files[3].Annotations); + Assert.Equal("Person: File Commenter", doc.Files[3].Annotations[0].Annotator); + Assert.Equal("2011-01-29T18:30:22Z", doc.Files[3].Annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Other, doc.Files[3].Annotations[0].Type); + Assert.Equal("File level annotation", doc.Files[3].Annotations[0].Comment); + Assert.Equal(2, doc.Files[3].Checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Md5, doc.Files[3].Checksums[0].Algorithm); + Assert.Equal("624c1abb3664f4b35547e7c73864ad24", doc.Files[3].Checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, doc.Files[3].Checksums[1].Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2758", doc.Files[3].Checksums[1].Value); // Assert: Verify snippets - Assert.HasCount(1, doc.Snippets); - Assert.AreEqual("SPDXRef-Snippet", doc.Snippets[0].Id); - Assert.AreEqual("SPDXRef-DoapSource", doc.Snippets[0].SnippetFromFile); - Assert.AreEqual(310, doc.Snippets[0].SnippetByteStart); - Assert.AreEqual(420, doc.Snippets[0].SnippetByteEnd); - Assert.AreEqual(5, doc.Snippets[0].SnippetLineStart); - Assert.AreEqual(23, doc.Snippets[0].SnippetLineEnd); - Assert.AreEqual("GPL-2.0-only", doc.Snippets[0].ConcludedLicense); - Assert.HasCount(1, doc.Snippets[0].LicenseInfoInSnippet); - Assert.AreEqual("GPL-2.0-only", doc.Snippets[0].LicenseInfoInSnippet[0]); + Assert.Single(doc.Snippets); + Assert.Equal("SPDXRef-Snippet", doc.Snippets[0].Id); + Assert.Equal("SPDXRef-DoapSource", doc.Snippets[0].SnippetFromFile); + Assert.Equal(310, doc.Snippets[0].SnippetByteStart); + Assert.Equal(420, doc.Snippets[0].SnippetByteEnd); + Assert.Equal(5, doc.Snippets[0].SnippetLineStart); + Assert.Equal(23, doc.Snippets[0].SnippetLineEnd); + Assert.Equal("GPL-2.0-only", doc.Snippets[0].ConcludedLicense); + Assert.Single(doc.Snippets[0].LicenseInfoInSnippet); + Assert.Equal("GPL-2.0-only", doc.Snippets[0].LicenseInfoInSnippet[0]); Assert.StartsWith("The concluded license was taken", doc.Snippets[0].LicenseComments); - Assert.AreEqual("Copyright 2008-2010 John Smith", doc.Snippets[0].CopyrightText); + Assert.Equal("Copyright 2008-2010 John Smith", doc.Snippets[0].CopyrightText); Assert.StartsWith("This snippet was identified as significant", doc.Snippets[0].Comment); - Assert.AreEqual("from linux kernel", doc.Snippets[0].Name); + Assert.Equal("from linux kernel", doc.Snippets[0].Name); // Assert: Verify relationships - Assert.HasCount(9, doc.Relationships); - Assert.AreEqual("SPDXRef-DOCUMENT", doc.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Contains, doc.Relationships[0].RelationshipType); - Assert.AreEqual("SPDXRef-DOCUMENT", doc.Relationships[1].Id); - Assert.AreEqual("SPDXRef-File", doc.Relationships[1].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Describes, doc.Relationships[1].RelationshipType); - Assert.AreEqual("SPDXRef-DOCUMENT", doc.Relationships[2].Id); - Assert.AreEqual("DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement", doc.Relationships[2].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.CopyOf, doc.Relationships[2].RelationshipType); - Assert.AreEqual("SPDXRef-DOCUMENT", doc.Relationships[3].Id); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[3].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Describes, doc.Relationships[3].RelationshipType); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[4].Id); - Assert.AreEqual("SPDXRef-Saxon", doc.Relationships[4].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DynamicLink, doc.Relationships[4].RelationshipType); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[5].Id); - Assert.AreEqual("SPDXRef-JenaLib", doc.Relationships[5].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Contains, doc.Relationships[5].RelationshipType); - Assert.AreEqual("SPDXRef-CommonsLangSrc", doc.Relationships[6].Id); - Assert.AreEqual("NOASSERTION", doc.Relationships[6].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.GeneratedFrom, doc.Relationships[6].RelationshipType); - Assert.AreEqual("SPDXRef-JenaLib", doc.Relationships[7].Id); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[7].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Contains, doc.Relationships[7].RelationshipType); - Assert.AreEqual("SPDXRef-File", doc.Relationships[8].Id); - Assert.AreEqual("SPDXRef-fromDoap-0", doc.Relationships[8].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.GeneratedFrom, doc.Relationships[8].RelationshipType); + Assert.Equal(9, doc.Relationships.Length); + Assert.Equal("SPDXRef-DOCUMENT", doc.Relationships[0].Id); + Assert.Equal("SPDXRef-Package", doc.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Contains, doc.Relationships[0].RelationshipType); + Assert.Equal("SPDXRef-DOCUMENT", doc.Relationships[1].Id); + Assert.Equal("SPDXRef-File", doc.Relationships[1].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Describes, doc.Relationships[1].RelationshipType); + Assert.Equal("SPDXRef-DOCUMENT", doc.Relationships[2].Id); + Assert.Equal("DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement", doc.Relationships[2].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.CopyOf, doc.Relationships[2].RelationshipType); + Assert.Equal("SPDXRef-DOCUMENT", doc.Relationships[3].Id); + Assert.Equal("SPDXRef-Package", doc.Relationships[3].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Describes, doc.Relationships[3].RelationshipType); + Assert.Equal("SPDXRef-Package", doc.Relationships[4].Id); + Assert.Equal("SPDXRef-Saxon", doc.Relationships[4].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DynamicLink, doc.Relationships[4].RelationshipType); + Assert.Equal("SPDXRef-Package", doc.Relationships[5].Id); + Assert.Equal("SPDXRef-JenaLib", doc.Relationships[5].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Contains, doc.Relationships[5].RelationshipType); + Assert.Equal("SPDXRef-CommonsLangSrc", doc.Relationships[6].Id); + Assert.Equal("NOASSERTION", doc.Relationships[6].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.GeneratedFrom, doc.Relationships[6].RelationshipType); + Assert.Equal("SPDXRef-JenaLib", doc.Relationships[7].Id); + Assert.Equal("SPDXRef-Package", doc.Relationships[7].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Contains, doc.Relationships[7].RelationshipType); + Assert.Equal("SPDXRef-File", doc.Relationships[8].Id); + Assert.Equal("SPDXRef-fromDoap-0", doc.Relationships[8].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.GeneratedFrom, doc.Relationships[8].RelationshipType); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize23.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize23.cs index 80cde95..bfe48fa 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize23.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserialize23.cs @@ -25,157 +25,165 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX 2.3 JSON documents to classes. /// -[TestClass] +/// +/// Exercises end-to-end deserialization of a real SPDX 2.3 JSON example document +/// (embedded resource) using xUnit v3 as the test framework. +/// public class Spdx2JsonDeserialize23 { /// /// Test parsing SPDX 2.3 JSON document. /// - [TestMethod] - public void Spdx2JsonDeserializer_Deserialize_ValidSpdx23JsonReturnsExpectedDocument() + /// + /// Uses the canonical SPDX 2.3 JSON example bundled as an embedded resource. Verifies + /// document-level fields, external document references, extracted licensing info, + /// annotations, files, snippets, and relationships are all correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_Deserialize_ValidSpdx23Json_ReturnsExpectedDocument() { // Arrange: Get the SPDX 2.3 JSON example from embedded resources - var json22Example = SpdxTestHelpers.GetEmbeddedResource( + var json23Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); - var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); - // Act: Validate the document + // Act: Deserialize the JSON document + var doc = Spdx2JsonDeserializer.Deserialize(json23Example); + + // Assert: Verify that the document is valid + Assert.NotNull(doc); var issues = new List(); doc.Validate(issues); - - // Assert: Verify that there are no validation issues - Assert.IsEmpty(issues); + Assert.Empty(issues); // Assert: Verify document - Assert.AreEqual("SPDX-Tools-v2.0", doc.Name); - Assert.AreEqual("SPDX-2.3", doc.Version); - Assert.AreEqual("http://spdx.org/spdxdocs/spdx-example-json-2.3-444504E0-4F89-41D3-9A0C-0305E82C3301", + Assert.Equal("SPDX-Tools-v2.0", doc.Name); + Assert.Equal("SPDX-2.3", doc.Version); + Assert.Equal("http://spdx.org/spdxdocs/spdx-example-json-2.3-444504E0-4F89-41D3-9A0C-0305E82C3301", doc.DocumentNamespace); - Assert.AreEqual("This document was created using SPDX 2.0 using licenses from the web site.", doc.Comment); - Assert.HasCount(3, doc.CreationInformation.Creators); - Assert.AreEqual("Tool: LicenseFind-1.0", doc.CreationInformation.Creators[0]); - Assert.AreEqual("Organization: ExampleCodeInspect ()", doc.CreationInformation.Creators[1]); - Assert.AreEqual("Person: Jane Doe ()", doc.CreationInformation.Creators[2]); - Assert.AreEqual("2010-01-29T18:30:22Z", doc.CreationInformation.Created); + Assert.Equal("This document was created using SPDX 2.0 using licenses from the web site.", doc.Comment); + Assert.Equal(3, doc.CreationInformation.Creators.Length); + Assert.Equal("Tool: LicenseFind-1.0", doc.CreationInformation.Creators[0]); + Assert.Equal("Organization: ExampleCodeInspect ()", doc.CreationInformation.Creators[1]); + Assert.Equal("Person: Jane Doe ()", doc.CreationInformation.Creators[2]); + Assert.Equal("2010-01-29T18:30:22Z", doc.CreationInformation.Created); Assert.StartsWith("This package has been shipped in source and", doc.CreationInformation.Comment); - Assert.AreEqual("3.17", doc.CreationInformation.LicenseListVersion); + Assert.Equal("3.17", doc.CreationInformation.LicenseListVersion); // Assert: Verify external document references - Assert.HasCount(1, doc.ExternalDocumentReferences); - Assert.AreEqual("DocumentRef-spdx-tool-1.2", doc.ExternalDocumentReferences[0].ExternalDocumentId); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, doc.ExternalDocumentReferences[0].Checksum.Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2759", doc.ExternalDocumentReferences[0].Checksum.Value); - Assert.AreEqual("http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301", + Assert.Single(doc.ExternalDocumentReferences); + Assert.Equal("DocumentRef-spdx-tool-1.2", doc.ExternalDocumentReferences[0].ExternalDocumentId); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, doc.ExternalDocumentReferences[0].Checksum.Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2759", doc.ExternalDocumentReferences[0].Checksum.Value); + Assert.Equal("http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301", doc.ExternalDocumentReferences[0].Document); // Assert: Verify extracted licensing info - Assert.HasCount(5, doc.ExtractedLicensingInfo); - Assert.AreEqual("LicenseRef-1", doc.ExtractedLicensingInfo[0].LicenseId); + Assert.Equal(5, doc.ExtractedLicensingInfo.Length); + Assert.Equal("LicenseRef-1", doc.ExtractedLicensingInfo[0].LicenseId); Assert.StartsWith("/*\n * (c) Copyright 2000, 2001, 2002, 2003", doc.ExtractedLicensingInfo[0].ExtractedText); - Assert.AreEqual("LicenseRef-2", doc.ExtractedLicensingInfo[1].LicenseId); + Assert.Equal("LicenseRef-2", doc.ExtractedLicensingInfo[1].LicenseId); Assert.StartsWith("This package includes the", doc.ExtractedLicensingInfo[1].ExtractedText); - Assert.AreEqual("LicenseRef-4", doc.ExtractedLicensingInfo[2].LicenseId); + Assert.Equal("LicenseRef-4", doc.ExtractedLicensingInfo[2].LicenseId); Assert.StartsWith("/*\n * (c) Copyright 2009 University of Bristol", doc.ExtractedLicensingInfo[2].ExtractedText); - Assert.AreEqual("LicenseRef-Beerware-4.2", doc.ExtractedLicensingInfo[3].LicenseId); + Assert.Equal("LicenseRef-Beerware-4.2", doc.ExtractedLicensingInfo[3].LicenseId); Assert.StartsWith( "\"THE BEER-WARE LICENSE\" (Revision 42)", doc.ExtractedLicensingInfo[3].ExtractedText); - Assert.AreEqual("Beer-Ware License (Version 42)", doc.ExtractedLicensingInfo[3].Name); - Assert.HasCount(1, doc.ExtractedLicensingInfo[3].CrossReferences); - Assert.AreEqual("http://people.freebsd.org/~phk/", doc.ExtractedLicensingInfo[3].CrossReferences[0]); + Assert.Equal("Beer-Ware License (Version 42)", doc.ExtractedLicensingInfo[3].Name); + Assert.Single(doc.ExtractedLicensingInfo[3].CrossReferences); + Assert.Equal("http://people.freebsd.org/~phk/", doc.ExtractedLicensingInfo[3].CrossReferences[0]); Assert.StartsWith("The beerware license has", doc.ExtractedLicensingInfo[3].Comment); - Assert.AreEqual("LicenseRef-3", doc.ExtractedLicensingInfo[4].LicenseId); + Assert.Equal("LicenseRef-3", doc.ExtractedLicensingInfo[4].LicenseId); Assert.StartsWith("The CyberNeko Software License", doc.ExtractedLicensingInfo[4].ExtractedText); - Assert.AreEqual("CyberNeko License", doc.ExtractedLicensingInfo[4].Name); - Assert.HasCount(2, doc.ExtractedLicensingInfo[4].CrossReferences); - Assert.AreEqual("http://people.apache.org/~andyc/neko/LICENSE", + Assert.Equal("CyberNeko License", doc.ExtractedLicensingInfo[4].Name); + Assert.Equal(2, doc.ExtractedLicensingInfo[4].CrossReferences.Length); + Assert.Equal("http://people.apache.org/~andyc/neko/LICENSE", doc.ExtractedLicensingInfo[4].CrossReferences[0]); - Assert.AreEqual("http://justasample.url.com", doc.ExtractedLicensingInfo[4].CrossReferences[1]); + Assert.Equal("http://justasample.url.com", doc.ExtractedLicensingInfo[4].CrossReferences[1]); Assert.StartsWith("This is the CyperNeko License", doc.ExtractedLicensingInfo[4].Comment); // Assert: Verify annotations - Assert.HasCount(3, doc.Annotations); - Assert.AreEqual("Person: Jane Doe ()", doc.Annotations[0].Annotator); - Assert.AreEqual("2010-01-29T18:30:22Z", doc.Annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Other, doc.Annotations[0].Type); + Assert.Equal(3, doc.Annotations.Length); + Assert.Equal("Person: Jane Doe ()", doc.Annotations[0].Annotator); + Assert.Equal("2010-01-29T18:30:22Z", doc.Annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Other, doc.Annotations[0].Type); Assert.StartsWith("Document level annotation", doc.Annotations[0].Comment); - Assert.AreEqual("Person: Joe Reviewer", doc.Annotations[1].Annotator); - Assert.AreEqual("2010-02-10T00:00:00Z", doc.Annotations[1].Date); - Assert.AreEqual(SpdxAnnotationType.Review, doc.Annotations[1].Type); + Assert.Equal("Person: Joe Reviewer", doc.Annotations[1].Annotator); + Assert.Equal("2010-02-10T00:00:00Z", doc.Annotations[1].Date); + Assert.Equal(SpdxAnnotationType.Review, doc.Annotations[1].Type); Assert.StartsWith("This is just an example", doc.Annotations[1].Comment); - Assert.AreEqual("Person: Suzanne Reviewer", doc.Annotations[2].Annotator); - Assert.AreEqual("2011-03-13T00:00:00Z", doc.Annotations[2].Date); - Assert.AreEqual(SpdxAnnotationType.Review, doc.Annotations[2].Type); + Assert.Equal("Person: Suzanne Reviewer", doc.Annotations[2].Annotator); + Assert.Equal("2011-03-13T00:00:00Z", doc.Annotations[2].Date); + Assert.Equal(SpdxAnnotationType.Review, doc.Annotations[2].Type); Assert.StartsWith("Another example reviewer.", doc.Annotations[2].Comment); // Assert: Verify files - Assert.HasCount(5, doc.Files); - Assert.AreEqual("SPDXRef-DoapSource", doc.Files[0].Id); - Assert.AreEqual("./src/org/spdx/parser/DOAPProject.java", doc.Files[0].FileName); - Assert.HasCount(1, doc.Files[0].FileTypes); - Assert.IsTrue(doc.Files[0].FileTypes.Contains(SpdxFileType.Source)); - Assert.HasCount(1, doc.Files[0].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, doc.Files[0].Checksums[0].Algorithm); - Assert.AreEqual("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", doc.Files[0].Checksums[0].Value); - Assert.AreEqual("Apache-2.0", doc.Files[0].ConcludedLicense); - Assert.HasCount(1, doc.Files[0].LicenseInfoInFiles); - Assert.AreEqual("Apache-2.0", doc.Files[0].LicenseInfoInFiles[0]); - Assert.AreEqual("Copyright 2010, 2011 Source Auditor Inc.", doc.Files[0].CopyrightText); - Assert.HasCount(5, doc.Files[0].Contributors); - Assert.AreEqual("Protecode Inc.", doc.Files[0].Contributors[0]); - Assert.AreEqual("SPDX Technical Team Members", doc.Files[0].Contributors[1]); - Assert.AreEqual("Open Logic Inc.", doc.Files[0].Contributors[2]); - Assert.AreEqual("Source Auditor Inc.", doc.Files[0].Contributors[3]); - Assert.AreEqual("Black Duck Software Inc.", doc.Files[0].Contributors[4]); - Assert.AreEqual("This file is used by Jena", doc.Files[1].Comment); + Assert.Equal(5, doc.Files.Length); + Assert.Equal("SPDXRef-DoapSource", doc.Files[0].Id); + Assert.Equal("./src/org/spdx/parser/DOAPProject.java", doc.Files[0].FileName); + Assert.Single(doc.Files[0].FileTypes); + Assert.Contains(SpdxFileType.Source, doc.Files[0].FileTypes); + Assert.Single(doc.Files[0].Checksums); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, doc.Files[0].Checksums[0].Algorithm); + Assert.Equal("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", doc.Files[0].Checksums[0].Value); + Assert.Equal("Apache-2.0", doc.Files[0].ConcludedLicense); + Assert.Single(doc.Files[0].LicenseInfoInFiles); + Assert.Equal("Apache-2.0", doc.Files[0].LicenseInfoInFiles[0]); + Assert.Equal("Copyright 2010, 2011 Source Auditor Inc.", doc.Files[0].CopyrightText); + Assert.Equal(5, doc.Files[0].Contributors.Length); + Assert.Equal("Protecode Inc.", doc.Files[0].Contributors[0]); + Assert.Equal("SPDX Technical Team Members", doc.Files[0].Contributors[1]); + Assert.Equal("Open Logic Inc.", doc.Files[0].Contributors[2]); + Assert.Equal("Source Auditor Inc.", doc.Files[0].Contributors[3]); + Assert.Equal("Black Duck Software Inc.", doc.Files[0].Contributors[4]); + Assert.Equal("This file is used by Jena", doc.Files[1].Comment); Assert.StartsWith("Apache Commons Lang\nCopyright 2001-2011", doc.Files[1].Notice); - Assert.AreEqual("This license is used by Jena", doc.Files[2].LicenseComments); - Assert.HasCount(2, doc.Files[4].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, doc.Files[4].Checksums[0].Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2758", doc.Files[4].Checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, doc.Files[4].Checksums[1].Algorithm); - Assert.AreEqual("624c1abb3664f4b35547e7c73864ad24", doc.Files[4].Checksums[1].Value); + Assert.Equal("This license is used by Jena", doc.Files[2].LicenseComments); + Assert.Equal(2, doc.Files[4].Checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, doc.Files[4].Checksums[0].Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2758", doc.Files[4].Checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Md5, doc.Files[4].Checksums[1].Algorithm); + Assert.Equal("624c1abb3664f4b35547e7c73864ad24", doc.Files[4].Checksums[1].Value); // Assert: Verify snippets - Assert.HasCount(1, doc.Snippets); - Assert.AreEqual("SPDXRef-Snippet", doc.Snippets[0].Id); - Assert.AreEqual("SPDXRef-DoapSource", doc.Snippets[0].SnippetFromFile); - Assert.AreEqual(310, doc.Snippets[0].SnippetByteStart); - Assert.AreEqual(420, doc.Snippets[0].SnippetByteEnd); - Assert.AreEqual(5, doc.Snippets[0].SnippetLineStart); - Assert.AreEqual(23, doc.Snippets[0].SnippetLineEnd); - Assert.AreEqual("GPL-2.0-only", doc.Snippets[0].ConcludedLicense); - Assert.HasCount(1, doc.Snippets[0].LicenseInfoInSnippet); - Assert.AreEqual("GPL-2.0-only", doc.Snippets[0].LicenseInfoInSnippet[0]); + Assert.Single(doc.Snippets); + Assert.Equal("SPDXRef-Snippet", doc.Snippets[0].Id); + Assert.Equal("SPDXRef-DoapSource", doc.Snippets[0].SnippetFromFile); + Assert.Equal(310, doc.Snippets[0].SnippetByteStart); + Assert.Equal(420, doc.Snippets[0].SnippetByteEnd); + Assert.Equal(5, doc.Snippets[0].SnippetLineStart); + Assert.Equal(23, doc.Snippets[0].SnippetLineEnd); + Assert.Equal("GPL-2.0-only", doc.Snippets[0].ConcludedLicense); + Assert.Single(doc.Snippets[0].LicenseInfoInSnippet); + Assert.Equal("GPL-2.0-only", doc.Snippets[0].LicenseInfoInSnippet[0]); Assert.StartsWith("The concluded license was taken", doc.Snippets[0].LicenseComments); - Assert.AreEqual("Copyright 2008-2010 John Smith", doc.Snippets[0].CopyrightText); + Assert.Equal("Copyright 2008-2010 John Smith", doc.Snippets[0].CopyrightText); Assert.StartsWith("This snippet was identified as significant", doc.Snippets[0].Comment); - Assert.AreEqual("from linux kernel", doc.Snippets[0].Name); + Assert.Equal("from linux kernel", doc.Snippets[0].Name); // Assert: Verify relationships - Assert.HasCount(7, doc.Relationships); - Assert.AreEqual("SPDXRef-DOCUMENT", doc.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Contains, doc.Relationships[0].RelationshipType); - Assert.AreEqual("SPDXRef-DOCUMENT", doc.Relationships[1].Id); - Assert.AreEqual("DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement", doc.Relationships[1].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.CopyOf, doc.Relationships[1].RelationshipType); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[2].Id); - Assert.AreEqual("SPDXRef-Saxon", doc.Relationships[2].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DynamicLink, doc.Relationships[2].RelationshipType); - Assert.AreEqual("SPDXRef-CommonsLangSrc", doc.Relationships[3].Id); - Assert.AreEqual("NOASSERTION", doc.Relationships[3].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.GeneratedFrom, doc.Relationships[3].RelationshipType); - Assert.AreEqual("SPDXRef-JenaLib", doc.Relationships[4].Id); - Assert.AreEqual("SPDXRef-Package", doc.Relationships[4].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Contains, doc.Relationships[4].RelationshipType); - Assert.AreEqual("SPDXRef-Specification", doc.Relationships[5].Id); - Assert.AreEqual("SPDXRef-fromDoap-0", doc.Relationships[5].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.SpecificationFor, doc.Relationships[5].RelationshipType); - Assert.AreEqual("SPDXRef-File", doc.Relationships[6].Id); - Assert.AreEqual("SPDXRef-fromDoap-0", doc.Relationships[6].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.GeneratedFrom, doc.Relationships[6].RelationshipType); + Assert.Equal(7, doc.Relationships.Length); + Assert.Equal("SPDXRef-DOCUMENT", doc.Relationships[0].Id); + Assert.Equal("SPDXRef-Package", doc.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Contains, doc.Relationships[0].RelationshipType); + Assert.Equal("SPDXRef-DOCUMENT", doc.Relationships[1].Id); + Assert.Equal("DocumentRef-spdx-tool-1.2:SPDXRef-ToolsElement", doc.Relationships[1].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.CopyOf, doc.Relationships[1].RelationshipType); + Assert.Equal("SPDXRef-Package", doc.Relationships[2].Id); + Assert.Equal("SPDXRef-Saxon", doc.Relationships[2].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DynamicLink, doc.Relationships[2].RelationshipType); + Assert.Equal("SPDXRef-CommonsLangSrc", doc.Relationships[3].Id); + Assert.Equal("NOASSERTION", doc.Relationships[3].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.GeneratedFrom, doc.Relationships[3].RelationshipType); + Assert.Equal("SPDXRef-JenaLib", doc.Relationships[4].Id); + Assert.Equal("SPDXRef-Package", doc.Relationships[4].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Contains, doc.Relationships[4].RelationshipType); + Assert.Equal("SPDXRef-Specification", doc.Relationships[5].Id); + Assert.Equal("SPDXRef-fromDoap-0", doc.Relationships[5].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.SpecificationFor, doc.Relationships[5].RelationshipType); + Assert.Equal("SPDXRef-File", doc.Relationships[6].Id); + Assert.Equal("SPDXRef-fromDoap-0", doc.Relationships[6].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.GeneratedFrom, doc.Relationships[6].RelationshipType); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeAnnotation.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeAnnotation.cs index 2304e2e..40f4420 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeAnnotation.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeAnnotation.cs @@ -24,16 +24,25 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// -/// Tests for deserializing SPDX annotations to classes. +/// Tests for deserializing SPDX annotations to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX annotation elements using xUnit v3 as the test +/// framework. Each test constructs inline JSON and verifies +/// the resulting fields. +/// public class Spdx2JsonDeserializeAnnotation { /// /// Tests deserializing an annotation. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeAnnotation_CorrectResults() + /// + /// Verifies that all four annotation fields (annotationDate, annotationType, annotator, + /// comment) are mapped to the correct properties when a + /// single JSON object is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeAnnotation_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing an annotation var json = new JsonObject @@ -48,17 +57,21 @@ public void Spdx2JsonDeserializer_DeserializeAnnotation_CorrectResults() var annotation = Spdx2JsonDeserializer.DeserializeAnnotation(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("2010-01-29T18:30:22Z", annotation.Date); - Assert.AreEqual(SpdxAnnotationType.Other, annotation.Type); - Assert.AreEqual("Person: Jane Doe ()", annotation.Annotator); - Assert.AreEqual("Document level annotation", annotation.Comment); + Assert.Equal("2010-01-29T18:30:22Z", annotation.Date); + Assert.Equal(SpdxAnnotationType.Other, annotation.Type); + Assert.Equal("Person: Jane Doe ()", annotation.Annotator); + Assert.Equal("Document level annotation", annotation.Comment); } /// /// Tests deserializing multiple annotations. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeAnnotations_CorrectResults() + /// + /// Verifies that a JSON array of two annotation objects is deserialized to an array + /// of two instances with fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeAnnotations_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple annotations var json = new JsonArray @@ -84,15 +97,15 @@ public void Spdx2JsonDeserializer_DeserializeAnnotations_CorrectResults() var annotations = Spdx2JsonDeserializer.DeserializeAnnotations(json); // Assert: Verify the deserialized array has the expected number of annotations and their properties - Assert.HasCount(2, annotations); - Assert.AreEqual("2010-01-29T18:30:22Z", annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Other, annotations[0].Type); - Assert.AreEqual("Person: Jane Doe ()", annotations[0].Annotator); - Assert.AreEqual("Document level annotation", annotations[0].Comment); - Assert.AreEqual("2010-02-10T00:00:00Z", annotations[1].Date); - Assert.AreEqual(SpdxAnnotationType.Review, annotations[1].Type); - Assert.AreEqual("Person: Joe Reviewer", annotations[1].Annotator); - Assert.AreEqual( + Assert.Equal(2, annotations.Length); + Assert.Equal("2010-01-29T18:30:22Z", annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Other, annotations[0].Type); + Assert.Equal("Person: Jane Doe ()", annotations[0].Annotator); + Assert.Equal("Document level annotation", annotations[0].Comment); + Assert.Equal("2010-02-10T00:00:00Z", annotations[1].Date); + Assert.Equal(SpdxAnnotationType.Review, annotations[1].Type); + Assert.Equal("Person: Joe Reviewer", annotations[1].Annotator); + Assert.Equal( "This is just an example. Some of the non-standard licenses look like they are actually BSD 3 clause licenses", annotations[1].Comment); } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeChecksum.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeChecksum.cs index 651a675..467f24f 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeChecksum.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeChecksum.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX checksums to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX checksum elements using xUnit v3 as the test +/// framework. Each test constructs inline JSON and verifies +/// the resulting fields. +/// public class Spdx2JsonDeserializeChecksum { /// /// Tests deserializing a checksum. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeChecksum_CorrectResults() + /// + /// Verifies that the algorithm and checksumValue JSON fields are mapped to the + /// corresponding properties when a single checksum object + /// is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeChecksum_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a checksum var json = new JsonObject @@ -46,15 +55,20 @@ public void Spdx2JsonDeserializer_DeserializeChecksum_CorrectResults() var checksum = Spdx2JsonDeserializer.DeserializeChecksum(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, checksum.Algorithm); - Assert.AreEqual("2fd4e1c67a2d28f123849ee1bb76e7391b93eb12", checksum.Value); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, checksum.Algorithm); + Assert.Equal("2fd4e1c67a2d28f123849ee1bb76e7391b93eb12", checksum.Value); } /// /// Tests deserializing multiple checksums. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeChecksums_CorrectResults() + /// + /// Verifies that a JSON array of two checksum objects (SHA1 and MD5) is deserialized + /// to an array of two instances with correct algorithm and + /// value mappings. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeChecksums_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple checksums var json = new JsonArray @@ -75,10 +89,10 @@ public void Spdx2JsonDeserializer_DeserializeChecksums_CorrectResults() var checksums = Spdx2JsonDeserializer.DeserializeChecksums(json); // Assert: Verify the deserialized array has the expected properties - Assert.HasCount(2, checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, checksums[0].Algorithm); - Assert.AreEqual("2fd4e1c67a2d28f123849ee1bb76e7391b93eb12", checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, checksums[1].Algorithm); - Assert.AreEqual("d41d8cd98f00b204e9800998ecf8427e", checksums[1].Value); + Assert.Equal(2, checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, checksums[0].Algorithm); + Assert.Equal("2fd4e1c67a2d28f123849ee1bb76e7391b93eb12", checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Md5, checksums[1].Algorithm); + Assert.Equal("d41d8cd98f00b204e9800998ecf8427e", checksums[1].Value); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeCreationInformation.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeCreationInformation.cs index efc774b..23c6444 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeCreationInformation.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeCreationInformation.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX creation information to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX creation information using xUnit v3 as the test +/// framework. Constructs inline JSON and verifies the resulting +/// fields. +/// public class Spdx2JsonDeserializeCreationInformation { /// /// Tests deserializing creation information. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeCreationInformation_CorrectResults() + /// + /// Verifies that all creation information fields (comment, created, creators array, + /// licenseListVersion) are correctly mapped to the + /// properties. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeCreationInformation_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing creation information var json = new JsonObject @@ -54,14 +63,14 @@ public void Spdx2JsonDeserializer_DeserializeCreationInformation_CorrectResults( var creationInformation = Spdx2JsonDeserializer.DeserializeCreationInformation(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual( + Assert.Equal( "This package has been shipped in source and binary form.\nThe binaries were created with gcc 4.5.1 and expect to link to\ncompatible system run time libraries.", creationInformation.Comment); - Assert.AreEqual("2010-01-29T18:30:22Z", creationInformation.Created); - Assert.HasCount(3, creationInformation.Creators); - Assert.AreEqual("Tool: LicenseFind-1.0", creationInformation.Creators[0]); - Assert.AreEqual("Organization: ExampleCodeInspect ()", creationInformation.Creators[1]); - Assert.AreEqual("Person: Jane Doe ()", creationInformation.Creators[2]); - Assert.AreEqual("3.17", creationInformation.LicenseListVersion); + Assert.Equal("2010-01-29T18:30:22Z", creationInformation.Created); + Assert.Equal(3, creationInformation.Creators.Length); + Assert.Equal("Tool: LicenseFind-1.0", creationInformation.Creators[0]); + Assert.Equal("Organization: ExampleCodeInspect ()", creationInformation.Creators[1]); + Assert.Equal("Person: Jane Doe ()", creationInformation.Creators[2]); + Assert.Equal("3.17", creationInformation.LicenseListVersion); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeDocument.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeDocument.cs index c8c6634..9b0b4e9 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeDocument.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeDocument.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX documents to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX document-level elements using xUnit v3 as the +/// test framework. Constructs inline JSON and verifies +/// the resulting fields. +/// public class Spdx2JsonDeserializeDocument { /// /// Tests deserializing a document. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeDocument_CorrectResults() + /// + /// Verifies that all top-level document fields (SPDXID, spdxVersion, name, dataLicense, + /// comment, documentNamespace, documentDescribes) and empty collection fields are + /// correctly mapped when a full document JSON object is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeDocument_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a document var json = new JsonObject @@ -76,22 +85,22 @@ public void Spdx2JsonDeserializer_DeserializeDocument_CorrectResults() var document = Spdx2JsonDeserializer.DeserializeDocument(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("SPDXRef-DOCUMENT", document.Id); - Assert.AreEqual("SPDX-2.3", document.Version); - Assert.AreEqual("SPDX-Tools-v2.0", document.Name); - Assert.AreEqual("CC0-1.0", document.DataLicense); - Assert.AreEqual("This document was created using SPDX 2.0 using licenses from the web site.", document.Comment); - Assert.AreEqual("http://spdx.org/spdxdocs/spdx-example-json-2.3-444504E0-4F89-41D3-9A0C-0305E82C3301", + Assert.Equal("SPDXRef-DOCUMENT", document.Id); + Assert.Equal("SPDX-2.3", document.Version); + Assert.Equal("SPDX-Tools-v2.0", document.Name); + Assert.Equal("CC0-1.0", document.DataLicense); + Assert.Equal("This document was created using SPDX 2.0 using licenses from the web site.", document.Comment); + Assert.Equal("http://spdx.org/spdxdocs/spdx-example-json-2.3-444504E0-4F89-41D3-9A0C-0305E82C3301", document.DocumentNamespace); - Assert.HasCount(3, document.Describes); - Assert.AreEqual("SPDXRef-File", document.Describes[0]); - Assert.AreEqual("SPDXRef-File", document.Describes[1]); - Assert.AreEqual("SPDXRef-Package", document.Describes[2]); - Assert.IsEmpty(document.ExternalDocumentReferences); - Assert.IsEmpty(document.ExtractedLicensingInfo); - Assert.IsEmpty(document.Packages); - Assert.IsEmpty(document.Files); - Assert.IsEmpty(document.Snippets); - Assert.IsEmpty(document.Relationships); + Assert.Equal(3, document.Describes.Length); + Assert.Equal("SPDXRef-File", document.Describes[0]); + Assert.Equal("SPDXRef-File", document.Describes[1]); + Assert.Equal("SPDXRef-Package", document.Describes[2]); + Assert.Empty(document.ExternalDocumentReferences); + Assert.Empty(document.ExtractedLicensingInfo); + Assert.Empty(document.Packages); + Assert.Empty(document.Files); + Assert.Empty(document.Snippets); + Assert.Empty(document.Relationships); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalDocumentReference.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalDocumentReference.cs index d3bdfd9..f94bc11 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalDocumentReference.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalDocumentReference.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX external document references to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX external document reference elements using xUnit v3 +/// as the test framework. Each test constructs inline JSON +/// and verifies the resulting fields. +/// public class Spdx2JsonDeserializeExternalDocumentReference { /// /// Tests deserializing an external document reference. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeExternalDocumentReference_CorrectResults() + /// + /// Verifies that externalDocumentId, checksum (algorithm and value), and spdxDocument + /// JSON fields are correctly mapped to the + /// properties when a single object is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeExternalDocumentReference_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing an external document reference var json = new JsonObject @@ -52,17 +61,21 @@ public void Spdx2JsonDeserializer_DeserializeExternalDocumentReference_CorrectRe var externalDocumentReference = Spdx2JsonDeserializer.DeserializeExternalDocumentReference(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("DocumentRef-1", externalDocumentReference.ExternalDocumentId); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, externalDocumentReference.Checksum.Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2759", externalDocumentReference.Checksum.Value); - Assert.AreEqual("SPDXRef-Document", externalDocumentReference.Document); + Assert.Equal("DocumentRef-1", externalDocumentReference.ExternalDocumentId); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, externalDocumentReference.Checksum.Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2759", externalDocumentReference.Checksum.Value); + Assert.Equal("SPDXRef-Document", externalDocumentReference.Document); } /// /// Tests deserializing multiple external document references. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_CorrectResults() + /// + /// Verifies that a JSON array containing one external document reference object is + /// deserialized to a single-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple external document references var json = new JsonArray @@ -84,10 +97,10 @@ public void Spdx2JsonDeserializer_DeserializeExternalDocumentReferences_CorrectR var externalDocumentReferences = Spdx2JsonDeserializer.DeserializeExternalDocumentReferences(json); // Assert: Verify the deserialized array has the expected number of references and their properties - Assert.HasCount(1, externalDocumentReferences); - Assert.AreEqual("DocumentRef-1", externalDocumentReferences[0].ExternalDocumentId); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, externalDocumentReferences[0].Checksum.Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2759", externalDocumentReferences[0].Checksum.Value); - Assert.AreEqual("SPDXRef-Document", externalDocumentReferences[0].Document); + Assert.Single(externalDocumentReferences); + Assert.Equal("DocumentRef-1", externalDocumentReferences[0].ExternalDocumentId); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, externalDocumentReferences[0].Checksum.Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2759", externalDocumentReferences[0].Checksum.Value); + Assert.Equal("SPDXRef-Document", externalDocumentReferences[0].Document); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalReference.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalReference.cs index 9904eb2..9ca5375 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalReference.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExternalReference.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX external references to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX external reference elements using xUnit v3 as the +/// test framework. Each test constructs inline JSON and +/// verifies the resulting fields. +/// public class Spdx2JsonDeserializeExternalReference { /// /// Tests deserializing an external reference. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeExternalReference_CorrectResults() + /// + /// Verifies that comment, referenceLocator, referenceType, and referenceCategory JSON + /// fields are correctly mapped to the properties + /// when a single SECURITY-category reference is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeExternalReference_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing an external reference var json = new JsonObject @@ -48,17 +57,21 @@ public void Spdx2JsonDeserializer_DeserializeExternalReference_CorrectResults() var reference = Spdx2JsonDeserializer.DeserializeExternalReference(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("This is just an example", reference.Comment); - Assert.AreEqual("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", reference.Locator); - Assert.AreEqual("cpe23Type", reference.Type); - Assert.AreEqual(SpdxReferenceCategory.Security, reference.Category); + Assert.Equal("This is just an example", reference.Comment); + Assert.Equal("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", reference.Locator); + Assert.Equal("cpe23Type", reference.Type); + Assert.Equal(SpdxReferenceCategory.Security, reference.Category); } /// /// Tests deserializing multiple external references. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeExternalReferences_CorrectResults() + /// + /// Verifies that a JSON array of two external reference objects (one SECURITY, one OTHER + /// category) is deserialized to a two-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeExternalReferences_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple external references var json = new JsonArray @@ -84,16 +97,16 @@ public void Spdx2JsonDeserializer_DeserializeExternalReferences_CorrectResults() var references = Spdx2JsonDeserializer.DeserializeExternalReferences(json); // Assert: Verify the deserialized array has the expected number of references and their properties - Assert.HasCount(2, references); - Assert.AreEqual("This is just an example", references[0].Comment); - Assert.AreEqual("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", references[0].Locator); - Assert.AreEqual("cpe23Type", references[0].Type); - Assert.AreEqual(SpdxReferenceCategory.Security, references[0].Category); - Assert.AreEqual("This is the external ref for Acme", references[1].Comment); - Assert.AreEqual("acmecorp/acmenator/4.1.3-alpha", references[1].Locator); - Assert.AreEqual( + Assert.Equal(2, references.Length); + Assert.Equal("This is just an example", references[0].Comment); + Assert.Equal("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", references[0].Locator); + Assert.Equal("cpe23Type", references[0].Type); + Assert.Equal(SpdxReferenceCategory.Security, references[0].Category); + Assert.Equal("This is the external ref for Acme", references[1].Comment); + Assert.Equal("acmecorp/acmenator/4.1.3-alpha", references[1].Locator); + Assert.Equal( "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge", references[1].Type); - Assert.AreEqual(SpdxReferenceCategory.Other, references[1].Category); + Assert.Equal(SpdxReferenceCategory.Other, references[1].Category); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExtractedLicensingInfo.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExtractedLicensingInfo.cs index 97a38ed..2d95447 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExtractedLicensingInfo.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeExtractedLicensingInfo.cs @@ -26,14 +26,24 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX extracted licensing information to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX extracted licensing information elements using +/// xUnit v3 as the test framework. Each test constructs +/// inline JSON and verifies the resulting fields. +/// public class Spdx2JsonDeserializeExtractedLicensingInfo { /// /// Tests deserializing an extracted licensing information. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_CorrectResults() + /// + /// Verifies that licenseId, extractedText, name, seeAlsos (cross-references), and + /// comment JSON fields are correctly mapped to the + /// properties when a single object is + /// deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing extracted licensing information var json = new JsonObject @@ -49,19 +59,23 @@ public void Spdx2JsonDeserializer_DeserializeExtractedLicensingInfo_CorrectResul var extractedLicensingInfo = Spdx2JsonDeserializer.DeserializeExtractedLicensingInfo(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("MIT", extractedLicensingInfo.LicenseId); - Assert.AreEqual("This is the MIT license", extractedLicensingInfo.ExtractedText); - Assert.AreEqual("MIT License", extractedLicensingInfo.Name); - Assert.HasCount(1, extractedLicensingInfo.CrossReferences); - Assert.AreEqual("https://opensource.org/licenses/MIT", extractedLicensingInfo.CrossReferences[0]); - Assert.AreEqual("This is a comment", extractedLicensingInfo.Comment); + Assert.Equal("MIT", extractedLicensingInfo.LicenseId); + Assert.Equal("This is the MIT license", extractedLicensingInfo.ExtractedText); + Assert.Equal("MIT License", extractedLicensingInfo.Name); + Assert.Single(extractedLicensingInfo.CrossReferences); + Assert.Equal("https://opensource.org/licenses/MIT", extractedLicensingInfo.CrossReferences[0]); + Assert.Equal("This is a comment", extractedLicensingInfo.Comment); } /// /// Tests deserializing multiple extracted licensing information. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_CorrectResults() + /// + /// Verifies that a JSON array containing one extracted licensing info object is + /// deserialized to a single-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple extracted licensing information var json = new JsonArray @@ -80,12 +94,12 @@ public void Spdx2JsonDeserializer_DeserializeExtractedLicensingInfos_CorrectResu var extractedLicensingInfos = Spdx2JsonDeserializer.DeserializeExtractedLicensingInfos(json); // Assert: Verify the deserialized array has the expected properties - Assert.HasCount(1, extractedLicensingInfos); - Assert.AreEqual("MIT", extractedLicensingInfos[0].LicenseId); - Assert.AreEqual("This is the MIT license", extractedLicensingInfos[0].ExtractedText); - Assert.AreEqual("MIT License", extractedLicensingInfos[0].Name); - Assert.HasCount(1, extractedLicensingInfos[0].CrossReferences); - Assert.AreEqual("https://opensource.org/licenses/MIT", extractedLicensingInfos[0].CrossReferences[0]); - Assert.AreEqual("This is a comment", extractedLicensingInfos[0].Comment); + Assert.Single(extractedLicensingInfos); + Assert.Equal("MIT", extractedLicensingInfos[0].LicenseId); + Assert.Equal("This is the MIT license", extractedLicensingInfos[0].ExtractedText); + Assert.Equal("MIT License", extractedLicensingInfos[0].Name); + Assert.Single(extractedLicensingInfos[0].CrossReferences); + Assert.Equal("https://opensource.org/licenses/MIT", extractedLicensingInfos[0].CrossReferences[0]); + Assert.Equal("This is a comment", extractedLicensingInfos[0].Comment); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeFile.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeFile.cs index 6998912..7909af2 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeFile.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeFile.cs @@ -26,14 +26,24 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX files to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX file elements using xUnit v3 as the test +/// framework. Each test constructs inline JSON and verifies the +/// resulting fields. +/// public class Spdx2JsonDeserializeFile { /// /// Tests deserializing a file. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeFile_CorrectResults() + /// + /// Verifies that all standard file fields (SPDXID, fileName, fileTypes, checksums, + /// licenseConcluded, licenseInfoInFiles, licenseComments, comment, noticeText) are + /// correctly mapped to properties when a single file JSON + /// object is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeFile_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a file var json = new JsonObject @@ -60,26 +70,30 @@ public void Spdx2JsonDeserializer_DeserializeFile_CorrectResults() var file = Spdx2JsonDeserializer.DeserializeFile(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("SPDXRef-File", file.Id); - Assert.AreEqual("src/DemaConsulting.SpdxModel/SpdxFile.cs", file.FileName); - Assert.HasCount(1, file.FileTypes); - Assert.AreEqual(SpdxFileType.Source, file.FileTypes[0]); - Assert.HasCount(1, file.Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, file.Checksums[0].Algorithm); - Assert.AreEqual("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", file.Checksums[0].Value); - Assert.AreEqual("MIT", file.ConcludedLicense); - Assert.HasCount(1, file.LicenseInfoInFiles); - Assert.AreEqual("MIT", file.LicenseInfoInFiles[0]); - Assert.AreEqual("This is the MIT license", file.LicenseComments); - Assert.AreEqual("This is a comment", file.Comment); - Assert.AreEqual("This is a notice", file.Notice); + Assert.Equal("SPDXRef-File", file.Id); + Assert.Equal("src/DemaConsulting.SpdxModel/SpdxFile.cs", file.FileName); + Assert.Single(file.FileTypes); + Assert.Equal(SpdxFileType.Source, file.FileTypes[0]); + Assert.Single(file.Checksums); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, file.Checksums[0].Algorithm); + Assert.Equal("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", file.Checksums[0].Value); + Assert.Equal("MIT", file.ConcludedLicense); + Assert.Single(file.LicenseInfoInFiles); + Assert.Equal("MIT", file.LicenseInfoInFiles[0]); + Assert.Equal("This is the MIT license", file.LicenseComments); + Assert.Equal("This is a comment", file.Comment); + Assert.Equal("This is a notice", file.Notice); } /// /// Tests deserializing multiple files. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeFiles_CorrectResults() + /// + /// Verifies that a JSON array containing one file object is deserialized to a + /// single-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeFiles_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple files var json = new JsonArray @@ -109,19 +123,19 @@ public void Spdx2JsonDeserializer_DeserializeFiles_CorrectResults() var files = Spdx2JsonDeserializer.DeserializeFiles(json); // Assert: Verify the deserialized array has the expected properties - Assert.HasCount(1, files); - Assert.AreEqual("SPDXRef-File", files[0].Id); - Assert.AreEqual("src/DemaConsulting.SpdxModel/SpdxFile.cs", files[0].FileName); - Assert.HasCount(1, files[0].FileTypes); - Assert.AreEqual(SpdxFileType.Source, files[0].FileTypes[0]); - Assert.HasCount(1, files[0].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, files[0].Checksums[0].Algorithm); - Assert.AreEqual("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", files[0].Checksums[0].Value); - Assert.AreEqual("MIT", files[0].ConcludedLicense); - Assert.HasCount(1, files[0].LicenseInfoInFiles); - Assert.AreEqual("MIT", files[0].LicenseInfoInFiles[0]); - Assert.AreEqual("This is the MIT license", files[0].LicenseComments); - Assert.AreEqual("This is a comment", files[0].Comment); - Assert.AreEqual("This is a notice", files[0].Notice); + Assert.Single(files); + Assert.Equal("SPDXRef-File", files[0].Id); + Assert.Equal("src/DemaConsulting.SpdxModel/SpdxFile.cs", files[0].FileName); + Assert.Single(files[0].FileTypes); + Assert.Equal(SpdxFileType.Source, files[0].FileTypes[0]); + Assert.Single(files[0].Checksums); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, files[0].Checksums[0].Algorithm); + Assert.Equal("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12", files[0].Checksums[0].Value); + Assert.Equal("MIT", files[0].ConcludedLicense); + Assert.Single(files[0].LicenseInfoInFiles); + Assert.Equal("MIT", files[0].LicenseInfoInFiles[0]); + Assert.Equal("This is the MIT license", files[0].LicenseComments); + Assert.Equal("This is a comment", files[0].Comment); + Assert.Equal("This is a notice", files[0].Notice); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackage.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackage.cs index 410f0a9..9f7d526 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackage.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackage.cs @@ -26,14 +26,24 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX packages to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX package elements using xUnit v3 as the test +/// framework. Each test constructs inline JSON and verifies +/// the resulting fields. +/// public class Spdx2JsonDeserializePackage { /// /// Tests deserializing a package. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializePackage_CorrectResults() + /// + /// Verifies that all standard package fields (SPDXID, annotations, attributionTexts, + /// builtDate, checksums, copyrightText, description, downloadLocation, externalRefs) + /// are correctly mapped to properties when a single package + /// JSON object is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializePackage_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a package var json = new JsonObject @@ -91,41 +101,45 @@ public void Spdx2JsonDeserializer_DeserializePackage_CorrectResults() var package = Spdx2JsonDeserializer.DeserializePackage(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("SPDXRef-Package", package.Id); - Assert.HasCount(1, package.Annotations); - Assert.AreEqual("2011-01-29T18:30:22Z", package.Annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Other, package.Annotations[0].Type); - Assert.AreEqual("Person: Package Commenter", package.Annotations[0].Annotator); - Assert.AreEqual("Package level annotation", package.Annotations[0].Comment); - Assert.HasCount(1, package.AttributionText); - Assert.AreEqual( + Assert.Equal("SPDXRef-Package", package.Id); + Assert.Single(package.Annotations); + Assert.Equal("2011-01-29T18:30:22Z", package.Annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Other, package.Annotations[0].Type); + Assert.Equal("Person: Package Commenter", package.Annotations[0].Annotator); + Assert.Equal("Package level annotation", package.Annotations[0].Comment); + Assert.Single(package.AttributionText); + Assert.Equal( "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.", package.AttributionText[0]); - Assert.AreEqual("2011-01-29T18:30:22Z", package.BuiltDate); - Assert.HasCount(3, package.Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, package.Checksums[0].Algorithm); - Assert.AreEqual("624c1abb3664f4b35547e7c73864ad24", package.Checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, package.Checksums[1].Algorithm); - Assert.AreEqual("85ed0817af83a24ad8da68c2b5094de69833983c", package.Checksums[1].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha256, package.Checksums[2].Algorithm); - Assert.AreEqual("11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd", package.Checksums[2].Value); - Assert.AreEqual("Copyright 2008-2010 John Smith", package.CopyrightText); - Assert.AreEqual( + Assert.Equal("2011-01-29T18:30:22Z", package.BuiltDate); + Assert.Equal(3, package.Checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Md5, package.Checksums[0].Algorithm); + Assert.Equal("624c1abb3664f4b35547e7c73864ad24", package.Checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, package.Checksums[1].Algorithm); + Assert.Equal("85ed0817af83a24ad8da68c2b5094de69833983c", package.Checksums[1].Value); + Assert.Equal(SpdxChecksumAlgorithm.Sha256, package.Checksums[2].Algorithm); + Assert.Equal("11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd", package.Checksums[2].Value); + Assert.Equal("Copyright 2008-2010 John Smith", package.CopyrightText); + Assert.Equal( "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.", package.Description); - Assert.AreEqual("http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz", package.DownloadLocation); - Assert.HasCount(1, package.ExternalReferences); - Assert.AreEqual(SpdxReferenceCategory.Security, package.ExternalReferences[0].Category); - Assert.AreEqual("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", + Assert.Equal("http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz", package.DownloadLocation); + Assert.Single(package.ExternalReferences); + Assert.Equal(SpdxReferenceCategory.Security, package.ExternalReferences[0].Category); + Assert.Equal("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", package.ExternalReferences[0].Locator); - Assert.AreEqual("cpe23Type", package.ExternalReferences[0].Type); + Assert.Equal("cpe23Type", package.ExternalReferences[0].Type); } /// /// Tests deserializing multiple packages. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializePackages_CorrectResults() + /// + /// Verifies that a JSON array containing one package object is deserialized to a + /// single-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializePackages_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple packages var json = new JsonArray @@ -186,35 +200,35 @@ public void Spdx2JsonDeserializer_DeserializePackages_CorrectResults() var packages = Spdx2JsonDeserializer.DeserializePackages(json); // Assert: Verify the deserialized array has the expected number of packages and their properties - Assert.HasCount(1, packages); - Assert.AreEqual("SPDXRef-Package", packages[0].Id); - Assert.HasCount(1, packages[0].Annotations); - Assert.AreEqual("2011-01-29T18:30:22Z", packages[0].Annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Other, packages[0].Annotations[0].Type); - Assert.AreEqual("Person: Package Commenter", packages[0].Annotations[0].Annotator); - Assert.AreEqual("Package level annotation", packages[0].Annotations[0].Comment); - Assert.HasCount(1, packages[0].AttributionText); - Assert.AreEqual( + Assert.Single(packages); + Assert.Equal("SPDXRef-Package", packages[0].Id); + Assert.Single(packages[0].Annotations); + Assert.Equal("2011-01-29T18:30:22Z", packages[0].Annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Other, packages[0].Annotations[0].Type); + Assert.Equal("Person: Package Commenter", packages[0].Annotations[0].Annotator); + Assert.Equal("Package level annotation", packages[0].Annotations[0].Comment); + Assert.Single(packages[0].AttributionText); + Assert.Equal( "The GNU C Library is free software. See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed. License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually.", packages[0].AttributionText[0]); - Assert.AreEqual("2011-01-29T18:30:22Z", packages[0].BuiltDate); - Assert.HasCount(3, packages[0].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, packages[0].Checksums[0].Algorithm); - Assert.AreEqual("624c1abb3664f4b35547e7c73864ad24", packages[0].Checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, packages[0].Checksums[1].Algorithm); - Assert.AreEqual("85ed0817af83a24ad8da68c2b5094de69833983c", packages[0].Checksums[1].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha256, packages[0].Checksums[2].Algorithm); - Assert.AreEqual("11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd", + Assert.Equal("2011-01-29T18:30:22Z", packages[0].BuiltDate); + Assert.Equal(3, packages[0].Checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Md5, packages[0].Checksums[0].Algorithm); + Assert.Equal("624c1abb3664f4b35547e7c73864ad24", packages[0].Checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, packages[0].Checksums[1].Algorithm); + Assert.Equal("85ed0817af83a24ad8da68c2b5094de69833983c", packages[0].Checksums[1].Value); + Assert.Equal(SpdxChecksumAlgorithm.Sha256, packages[0].Checksums[2].Algorithm); + Assert.Equal("11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd", packages[0].Checksums[2].Value); - Assert.AreEqual("Copyright 2008-2010 John Smith", packages[0].CopyrightText); - Assert.AreEqual( + Assert.Equal("Copyright 2008-2010 John Smith", packages[0].CopyrightText); + Assert.Equal( "The GNU C Library defines functions that are specified by the ISO C standard, as well as additional features specific to POSIX and other derivatives of the Unix operating system, and extensions specific to GNU systems.", packages[0].Description); - Assert.AreEqual("http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz", packages[0].DownloadLocation); - Assert.HasCount(1, packages[0].ExternalReferences); - Assert.AreEqual(SpdxReferenceCategory.Security, packages[0].ExternalReferences[0].Category); - Assert.AreEqual("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", + Assert.Equal("http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz", packages[0].DownloadLocation); + Assert.Single(packages[0].ExternalReferences); + Assert.Equal(SpdxReferenceCategory.Security, packages[0].ExternalReferences[0].Category); + Assert.Equal("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", packages[0].ExternalReferences[0].Locator); - Assert.AreEqual("cpe23Type", packages[0].ExternalReferences[0].Type); + Assert.Equal("cpe23Type", packages[0].ExternalReferences[0].Type); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackageVerificationCode.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackageVerificationCode.cs index 03e64bf..0e0227d 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackageVerificationCode.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializePackageVerificationCode.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX package verification codes to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX package verification code elements using xUnit v3 +/// as the test framework. Constructs inline JSON and +/// verifies the resulting fields. +/// public class Spdx2JsonDeserializePackageVerificationCode { /// /// Tests deserializing a package verification code. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializePackageVerificationCode_CorrectResults() + /// + /// Verifies that packageVerificationCodeValue and packageVerificationCodeExcludedFiles + /// JSON fields are correctly mapped to the + /// properties and that the result is non-null. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializePackageVerificationCode_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a package verification code var json = new JsonObject @@ -48,12 +57,12 @@ public void Spdx2JsonDeserializer_DeserializePackageVerificationCode_CorrectResu // Act: Deserialize the JSON object to an SpdxPackageVerificationCode object var packageVerificationCode = Spdx2JsonDeserializer.DeserializeVerificationCode(json); - Assert.IsNotNull(packageVerificationCode); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("d3b07384d113edec49eaa6238ad5ff00", packageVerificationCode.Value); - Assert.HasCount(2, packageVerificationCode.ExcludedFiles); - Assert.AreEqual("file1.txt", packageVerificationCode.ExcludedFiles[0]); - Assert.AreEqual("file2.txt", packageVerificationCode.ExcludedFiles[1]); + Assert.NotNull(packageVerificationCode); + Assert.Equal("d3b07384d113edec49eaa6238ad5ff00", packageVerificationCode.Value); + Assert.Equal(2, packageVerificationCode.ExcludedFiles.Length); + Assert.Equal("file1.txt", packageVerificationCode.ExcludedFiles[0]); + Assert.Equal("file2.txt", packageVerificationCode.ExcludedFiles[1]); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeRelationship.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeRelationship.cs index 5c50f28..de6daa6 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeRelationship.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeRelationship.cs @@ -26,14 +26,23 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX relationships to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX relationship elements using xUnit v3 as the test +/// framework. Each test constructs inline JSON and verifies +/// the resulting fields. +/// public class Spdx2JsonDeserializeRelationship { /// /// Tests deserializing a relationship. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeRelationship_CorrectResults() + /// + /// Verifies that spdxElementId, relatedSpdxElement, relationshipType, and comment JSON + /// fields are correctly mapped to the properties when a + /// single relationship object is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeRelationship_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a relationship var json = new JsonObject @@ -48,17 +57,21 @@ public void Spdx2JsonDeserializer_DeserializeRelationship_CorrectResults() var relationship = Spdx2JsonDeserializer.DeserializeRelationship(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("SPDXRef-DOCUMENT", relationship.Id); - Assert.AreEqual("SPDXRef-Package", relationship.RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Describes, relationship.RelationshipType); - Assert.AreEqual("This is just an example", relationship.Comment); + Assert.Equal("SPDXRef-DOCUMENT", relationship.Id); + Assert.Equal("SPDXRef-Package", relationship.RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Describes, relationship.RelationshipType); + Assert.Equal("This is just an example", relationship.Comment); } /// /// Tests deserializing multiple relationships. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeRelationships_CorrectResults() + /// + /// Verifies that a JSON array of two relationship objects (DESCRIBES and DESCRIBED_BY) + /// is deserialized to a two-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeRelationships_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple relationships var json = new JsonArray @@ -83,14 +96,14 @@ public void Spdx2JsonDeserializer_DeserializeRelationships_CorrectResults() var relationships = Spdx2JsonDeserializer.DeserializeRelationships(json); // Assert: Verify the deserialized objects have the expected properties - Assert.HasCount(2, relationships); - Assert.AreEqual("SPDXRef-DOCUMENT", relationships[0].Id); - Assert.AreEqual("SPDXRef-Package", relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.Describes, relationships[0].RelationshipType); - Assert.AreEqual("This is just an example", relationships[0].Comment); - Assert.AreEqual("SPDXRef-Package", relationships[1].Id); - Assert.AreEqual("SPDXRef-DOCUMENT", relationships[1].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DescribedBy, relationships[1].RelationshipType); - Assert.AreEqual("This is just an example", relationships[1].Comment); + Assert.Equal(2, relationships.Length); + Assert.Equal("SPDXRef-DOCUMENT", relationships[0].Id); + Assert.Equal("SPDXRef-Package", relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.Describes, relationships[0].RelationshipType); + Assert.Equal("This is just an example", relationships[0].Comment); + Assert.Equal("SPDXRef-Package", relationships[1].Id); + Assert.Equal("SPDXRef-DOCUMENT", relationships[1].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DescribedBy, relationships[1].RelationshipType); + Assert.Equal("This is just an example", relationships[1].Comment); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeSnippet.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeSnippet.cs index 76e941d..085d347 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeSnippet.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializeSnippet.cs @@ -26,14 +26,24 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for deserializing SPDX snippets to classes. /// -[TestClass] +/// +/// Exercises deserialization of SPDX snippet elements using xUnit v3 as the test +/// framework. Each test constructs inline JSON and verifies +/// the resulting fields. +/// public class Spdx2JsonDeserializeSnippet { /// /// Tests deserializing a snippet. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeSnippet_CorrectResults() + /// + /// Verifies that all snippet fields (SPDXID, comment, copyrightText, licenseComments, + /// licenseConcluded, licenseInfoInSnippets, name, byte ranges, line ranges, and + /// snippetFromFile) are correctly mapped to properties when + /// a single snippet JSON object with both byte and line ranges is deserialized. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeSnippet_ValidInput_CorrectResults() { // Arrange: Create a JSON object representing a snippet var json = new JsonObject @@ -86,23 +96,23 @@ public void Spdx2JsonDeserializer_DeserializeSnippet_CorrectResults() var snippet = Spdx2JsonDeserializer.DeserializeSnippet(json); // Assert: Verify the deserialized object has the expected properties - Assert.AreEqual("SPDXRef-Snippet", snippet.Id); - Assert.AreEqual( + Assert.Equal("SPDXRef-Snippet", snippet.Id); + Assert.Equal( "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.", snippet.Comment); - Assert.AreEqual("Copyright 2008-2010 John Smith", snippet.CopyrightText); - Assert.AreEqual( + Assert.Equal("Copyright 2008-2010 John Smith", snippet.CopyrightText); + Assert.Equal( "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.", snippet.LicenseComments); - Assert.AreEqual("GPL-2.0-only", snippet.ConcludedLicense); - Assert.HasCount(1, snippet.LicenseInfoInSnippet); - Assert.AreEqual("GPL-2.0-only", snippet.LicenseInfoInSnippet[0]); - Assert.AreEqual("from linux kernel", snippet.Name); - Assert.AreEqual(420, snippet.SnippetByteEnd); - Assert.AreEqual(310, snippet.SnippetByteStart); - Assert.AreEqual(23, snippet.SnippetLineEnd); - Assert.AreEqual(5, snippet.SnippetLineStart); - Assert.AreEqual("SPDXRef-DoapSource", snippet.SnippetFromFile); + Assert.Equal("GPL-2.0-only", snippet.ConcludedLicense); + Assert.Single(snippet.LicenseInfoInSnippet); + Assert.Equal("GPL-2.0-only", snippet.LicenseInfoInSnippet[0]); + Assert.Equal("from linux kernel", snippet.Name); + Assert.Equal(420, snippet.SnippetByteEnd); + Assert.Equal(310, snippet.SnippetByteStart); + Assert.Equal(23, snippet.SnippetLineEnd); + Assert.Equal(5, snippet.SnippetLineStart); + Assert.Equal("SPDXRef-DoapSource", snippet.SnippetFromFile); } /// @@ -110,7 +120,12 @@ public void Spdx2JsonDeserializer_DeserializeSnippet_CorrectResults() /// This is a regression test for a thrown when line-number /// range fields were absent and the old code used Convert.ToInt32(""). /// - [TestMethod] + /// + /// Boundary condition: when only a byte-range entry exists in the ranges array and no + /// lineNumber pointers are present, and + /// must default to zero rather than throwing. + /// + [Fact] public void Spdx2JsonDeserializer_DeserializeSnippet_WithoutLineRanges_DefaultsToZero() { // Arrange: Create a JSON snippet with only byte ranges (no lineNumber entries) @@ -143,17 +158,21 @@ public void Spdx2JsonDeserializer_DeserializeSnippet_WithoutLineRanges_DefaultsT var snippet = Spdx2JsonDeserializer.DeserializeSnippet(json); // Assert: Byte ranges are correct and absent line ranges default to 0 - Assert.AreEqual(310, snippet.SnippetByteStart); - Assert.AreEqual(420, snippet.SnippetByteEnd); - Assert.AreEqual(0, snippet.SnippetLineStart); - Assert.AreEqual(0, snippet.SnippetLineEnd); + Assert.Equal(310, snippet.SnippetByteStart); + Assert.Equal(420, snippet.SnippetByteEnd); + Assert.Equal(0, snippet.SnippetLineStart); + Assert.Equal(0, snippet.SnippetLineEnd); } /// /// Tests deserializing multiple snippets. /// - [TestMethod] - public void Spdx2JsonDeserializer_DeserializeSnippets_CorrectResults() + /// + /// Verifies that a JSON array containing one snippet object with both byte and line + /// ranges is deserialized to a single-element array with all fields correctly populated. + /// + [Fact] + public void Spdx2JsonDeserializer_DeserializeSnippets_ValidInput_CorrectResults() { // Arrange: Create a JSON array representing multiple snippets var json = new JsonArray @@ -209,23 +228,23 @@ public void Spdx2JsonDeserializer_DeserializeSnippets_CorrectResults() var snippets = Spdx2JsonDeserializer.DeserializeSnippets(json); // Assert: Verify the deserialized array has the expected properties - Assert.HasCount(1, snippets); - Assert.AreEqual("SPDXRef-Snippet", snippets[0].Id); - Assert.AreEqual( + Assert.Single(snippets); + Assert.Equal("SPDXRef-Snippet", snippets[0].Id); + Assert.Equal( "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0.", snippets[0].Comment); - Assert.AreEqual("Copyright 2008-2010 John Smith", snippets[0].CopyrightText); - Assert.AreEqual( + Assert.Equal("Copyright 2008-2010 John Smith", snippets[0].CopyrightText); + Assert.Equal( "The concluded license was taken from package xyz, from which the snippet was copied into the current file. The concluded license information was found in the COPYING.txt file in package xyz.", snippets[0].LicenseComments); - Assert.AreEqual("GPL-2.0-only", snippets[0].ConcludedLicense); - Assert.HasCount(1, snippets[0].LicenseInfoInSnippet); - Assert.AreEqual("GPL-2.0-only", snippets[0].LicenseInfoInSnippet[0]); - Assert.AreEqual("from linux kernel", snippets[0].Name); - Assert.AreEqual(420, snippets[0].SnippetByteEnd); - Assert.AreEqual(310, snippets[0].SnippetByteStart); - Assert.AreEqual(23, snippets[0].SnippetLineEnd); - Assert.AreEqual(5, snippets[0].SnippetLineStart); - Assert.AreEqual("SPDXRef-DoapSource", snippets[0].SnippetFromFile); + Assert.Equal("GPL-2.0-only", snippets[0].ConcludedLicense); + Assert.Single(snippets[0].LicenseInfoInSnippet); + Assert.Equal("GPL-2.0-only", snippets[0].LicenseInfoInSnippet[0]); + Assert.Equal("from linux kernel", snippets[0].Name); + Assert.Equal(420, snippets[0].SnippetByteEnd); + Assert.Equal(310, snippets[0].SnippetByteStart); + Assert.Equal(23, snippets[0].SnippetLineEnd); + Assert.Equal(5, snippets[0].SnippetLineStart); + Assert.Equal("SPDXRef-DoapSource", snippets[0].SnippetFromFile); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializerTests.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializerTests.cs new file mode 100644 index 0000000..30dcc2e --- /dev/null +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonDeserializerTests.cs @@ -0,0 +1,51 @@ +// Copyright(c) 2024 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 System.Text.Json; +using DemaConsulting.SpdxModel.IO; + +namespace DemaConsulting.SpdxModel.Tests.IO; + +/// +/// Error-path tests for . +/// +/// +/// Covers the error-handling paths of : invalid JSON +/// input that should throw rather than return a partially-populated document. +/// +public class Spdx2JsonDeserializerTests +{ + /// + /// Tests that deserializing malformed JSON throws a . + /// + /// + /// Confirms that syntactically broken JSON (missing closing brace) causes a + /// rather than a silent failure. + /// + [Fact] + public void Spdx2JsonDeserializer_Deserialize_MalformedJson_ThrowsJsonException() + { + // Arrange: + const string malformedJson = "{ not valid json"; + + // Act / Assert: + Assert.ThrowsAny(() => Spdx2JsonDeserializer.Deserialize(malformedJson)); + } +} diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeAnnotation.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeAnnotation.cs index b828c44..d236ebd 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeAnnotation.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeAnnotation.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeAnnotation { /// /// Tests serializing an annotation. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeAnnotation_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeAnnotation_ValidInput_CorrectResults() { // Arrange: Create a sample annotation var annotation = new SpdxAnnotation @@ -58,8 +57,8 @@ public void Spdx2JsonSerializer_SerializeAnnotation_CorrectResults() /// /// Tests serializing multiple annotations. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeAnnotations_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeAnnotations_ValidInput_CorrectResults() { // Arrange: Create a sample list of annotations var annotations = new[] @@ -86,8 +85,8 @@ public void Spdx2JsonSerializer_SerializeAnnotations_CorrectResults() var json = Spdx2JsonSerializer.SerializeAnnotations(annotations); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(2, json.Count); + Assert.NotNull(json); + Assert.Equal(2, json.Count); SpdxJsonHelpers.AssertEqual("SPDXRef-Annotation1", json[0]?["SPDXID"]); SpdxJsonHelpers.AssertEqual("John Doe", json[0]?["annotator"]); SpdxJsonHelpers.AssertEqual("2021-09-01T12:00:00Z", json[0]?["annotationDate"]); @@ -99,4 +98,31 @@ public void Spdx2JsonSerializer_SerializeAnnotations_CorrectResults() SpdxJsonHelpers.AssertEqual("OTHER", json[1]?["annotationType"]); SpdxJsonHelpers.AssertEqual("This is another comment", json[1]?["comment"]); } + + /// + /// Tests that an annotation with no ID omits the SPDXID field from the serialized JSON. + /// + [Fact] + public void Spdx2JsonSerializer_SerializeAnnotation_NoId_OmitsSpdxId() + { + // Arrange: Create an annotation with no ID (empty string) + var annotation = new SpdxAnnotation + { + Id = string.Empty, + Annotator = "John Doe", + Date = "2021-09-01T12:00:00Z", + Type = SpdxAnnotationType.Review, + Comment = "This is a comment" + }; + + // Act: Serialize the annotation to JSON + var json = Spdx2JsonSerializer.SerializeAnnotation(annotation); + + // Assert: SPDXID is absent and other fields are present + Assert.Null(json["SPDXID"]); + SpdxJsonHelpers.AssertEqual("John Doe", json["annotator"]); + SpdxJsonHelpers.AssertEqual("2021-09-01T12:00:00Z", json["annotationDate"]); + SpdxJsonHelpers.AssertEqual("REVIEW", json["annotationType"]); + SpdxJsonHelpers.AssertEqual("This is a comment", json["comment"]); + } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeChecksum.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeChecksum.cs index 15bd478..f1fc68c 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeChecksum.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeChecksum.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeChecksum { /// /// Tests serializing a checksum. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeChecksum_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeChecksum_ValidInput_CorrectResults() { // Arrange: Create a sample checksum var checksum = new SpdxChecksum @@ -45,7 +44,7 @@ public void Spdx2JsonSerializer_SerializeChecksum_CorrectResults() var json = Spdx2JsonSerializer.SerializeChecksum(checksum); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("SHA1", json["algorithm"]); SpdxJsonHelpers.AssertEqual("2fd4e1c67a2d28f123849ee1bb76e7391b93eb12", json["checksumValue"]); } @@ -53,8 +52,8 @@ public void Spdx2JsonSerializer_SerializeChecksum_CorrectResults() /// /// Tests serializing multiple checksums. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeChecksums_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeChecksums_ValidInput_CorrectResults() { // Arrange: Create sample checksums var checksums = new[] @@ -75,8 +74,8 @@ public void Spdx2JsonSerializer_SerializeChecksums_CorrectResults() var json = Spdx2JsonSerializer.SerializeChecksums(checksums); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(2, json.Count); + Assert.NotNull(json); + Assert.Equal(2, json.Count); SpdxJsonHelpers.AssertEqual("SHA1", json[0]?["algorithm"]); SpdxJsonHelpers.AssertEqual("2fd4e1c67a2d28f123849ee1bb76e7391b93eb12", json[0]?["checksumValue"]); SpdxJsonHelpers.AssertEqual("MD5", json[1]?["algorithm"]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeCreationInformation.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeCreationInformation.cs index 812c719..c58c705 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeCreationInformation.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeCreationInformation.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeCreationInformation { /// /// Tests serializing creation information. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeCreationInformation_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeCreationInformation_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxCreationInformation object var creationInformation = new SpdxCreationInformation diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeDocument.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeDocument.cs index ceb7a82..625b77d 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeDocument.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeDocument.cs @@ -26,14 +26,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeDocument { /// /// Tests serializing a document to JSON. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeDocument_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeDocument_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxDocument object var document = new SpdxDocument @@ -87,13 +86,20 @@ public void Spdx2JsonSerializer_SerializeDocument_CorrectResults() SpdxJsonHelpers.AssertEqual( "http://spdx.org/spdxdocs/spdx-example-json-2.3-444504E0-4F89-41D3-9A0C-0305E82C3301", json["documentNamespace"]); + + // Assert: Verify creationInfo fields + SpdxJsonHelpers.AssertEqual("2010-01-29T18:30:22Z", json["creationInfo"]?["created"]); + SpdxJsonHelpers.AssertEqual("Tool: LicenseFind-1.0", json["creationInfo"]?["creators"]?[0]); + SpdxJsonHelpers.AssertEqual("Organization: ExampleCodeInspect ()", json["creationInfo"]?["creators"]?[1]); + SpdxJsonHelpers.AssertEqual("Person: Jane Doe ()", json["creationInfo"]?["creators"]?[2]); + SpdxJsonHelpers.AssertEqual("3.17", json["creationInfo"]?["licenseListVersion"]); } /// /// Tests serializing a document to text /// - [TestMethod] - public void Spdx2JsonSerializer_Serialize_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_Serialize_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxDocument object var document = new SpdxDocument @@ -136,7 +142,7 @@ public void Spdx2JsonSerializer_Serialize_CorrectResults() var json = JsonNode.Parse(jsonText) as JsonObject; // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("SPDXRef-DOCUMENT", json["SPDXID"]); SpdxJsonHelpers.AssertEqual("SPDX-2.3", json["spdxVersion"]); SpdxJsonHelpers.AssertEqual("SPDX-Tools-v2.0", json["name"]); @@ -149,5 +155,12 @@ public void Spdx2JsonSerializer_Serialize_CorrectResults() SpdxJsonHelpers.AssertEqual( "http://spdx.org/spdxdocs/spdx-example-json-2.3-444504E0-4F89-41D3-9A0C-0305E82C3301", json["documentNamespace"]); + + // Assert: Verify creationInfo fields + SpdxJsonHelpers.AssertEqual("2010-01-29T18:30:22Z", json["creationInfo"]?["created"]); + SpdxJsonHelpers.AssertEqual("Tool: LicenseFind-1.0", json["creationInfo"]?["creators"]?[0]); + SpdxJsonHelpers.AssertEqual("Organization: ExampleCodeInspect ()", json["creationInfo"]?["creators"]?[1]); + SpdxJsonHelpers.AssertEqual("Person: Jane Doe ()", json["creationInfo"]?["creators"]?[2]); + SpdxJsonHelpers.AssertEqual("3.17", json["creationInfo"]?["licenseListVersion"]); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalDocumentReference.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalDocumentReference.cs index 7f37959..683eefe 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalDocumentReference.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalDocumentReference.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeExternalDocumentReference { /// /// Tests serializing an external document reference. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeExternalDocumentReference_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeExternalDocumentReference_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxExternalDocumentReference object var reference = new SpdxExternalDocumentReference @@ -60,8 +59,8 @@ public void Spdx2JsonSerializer_SerializeExternalDocumentReference_CorrectResult /// /// Tests serializing multiple external document references. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeExternalDocumentReferences_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeExternalDocumentReferences_ValidInput_CorrectResults() { // Arrange: Create a sample array of SpdxExternalDocumentReference objects var references = new[] @@ -82,8 +81,8 @@ public void Spdx2JsonSerializer_SerializeExternalDocumentReferences_CorrectResul var json = Spdx2JsonSerializer.SerializeExternalDocumentReferences(references); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(1, json.Count); + Assert.NotNull(json); + Assert.Single(json); SpdxJsonHelpers.AssertEqual("DocumentRef-spdx-tool-1.2", json[0]?["externalDocumentId"]); SpdxJsonHelpers.AssertEqual("SHA1", json[0]?["checksum"]?["algorithm"]); SpdxJsonHelpers.AssertEqual("d6a770ba38583ed4bb4525bd96e50461655d2759", json[0]?["checksum"]?["checksumValue"]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalReference.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalReference.cs index ec50aaf..98f062a 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalReference.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExternalReference.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeExternalReference { /// /// Tests serializing an external reference. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeExternalReference_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeExternalReference_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxExternalReference object var reference = new SpdxExternalReference @@ -47,7 +46,7 @@ public void Spdx2JsonSerializer_SerializeExternalReference_CorrectResults() var json = Spdx2JsonSerializer.SerializeExternalReference(reference); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("SECURITY", json["referenceCategory"]); SpdxJsonHelpers.AssertEqual("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", json["referenceLocator"]); @@ -58,8 +57,8 @@ public void Spdx2JsonSerializer_SerializeExternalReference_CorrectResults() /// /// Tests serializing multiple external references. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeExternalReferences_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeExternalReferences_ValidInput_CorrectResults() { // Arrange: Create sample SpdxExternalReference objects var references = new[] @@ -85,8 +84,8 @@ public void Spdx2JsonSerializer_SerializeExternalReferences_CorrectResults() var json = Spdx2JsonSerializer.SerializeExternalReferences(references); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(2, json.Count); + Assert.NotNull(json); + Assert.Equal(2, json.Count); SpdxJsonHelpers.AssertEqual("SECURITY", json[0]?["referenceCategory"]); SpdxJsonHelpers.AssertEqual("cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*", json[0]?["referenceLocator"]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExtractedLicensingInfo.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExtractedLicensingInfo.cs index a1be70f..2dcca1d 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExtractedLicensingInfo.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeExtractedLicensingInfo.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeExtractedLicensingInfo { /// /// Tests serializing an extracted licensing info. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeExtractedLicensingInfo_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeExtractedLicensingInfo_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxExtractedLicensingInfo object var info = new SpdxExtractedLicensingInfo @@ -58,8 +57,8 @@ public void Spdx2JsonSerializer_SerializeExtractedLicensingInfo_CorrectResults() /// /// Tests serializing multiple extracted licensing infos. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeExtractedLicensingInfos_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeExtractedLicensingInfos_ValidInput_CorrectResults() { // Arrange: Create a sample array of SpdxExtractedLicensingInfo objects var info = new[] @@ -78,8 +77,8 @@ public void Spdx2JsonSerializer_SerializeExtractedLicensingInfos_CorrectResults( var json = Spdx2JsonSerializer.SerializeExtractedLicensingInfos(info); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(1, json.Count); + Assert.NotNull(json); + Assert.Single(json); SpdxJsonHelpers.AssertEqual("MIT", json[0]?["licenseId"]); SpdxJsonHelpers.AssertEqual("This is the MIT license", json[0]?["extractedText"]); SpdxJsonHelpers.AssertEqual("MIT License", json[0]?["name"]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeFile.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeFile.cs index 752aa53..7936a88 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeFile.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeFile.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeFile { /// /// Tests serializing a file. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeFile_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeFile_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxFile object var file = new SpdxFile @@ -111,10 +110,10 @@ public void Spdx2JsonSerializer_SerializeFile_CorrectResults() /// /// Tests serializing multiple files. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeFiles_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeFiles_ValidInput_CorrectResults() { - // Arrange + // Arrange: Create a sample array containing a single SpdxFile with all fields populated var file = new[] { new SpdxFile @@ -161,12 +160,12 @@ public void Spdx2JsonSerializer_SerializeFiles_CorrectResults() } }; - // Act + // Act: Serialize the array of files to JSON var json = Spdx2JsonSerializer.SerializeFiles(file); - // Assert - Assert.IsNotNull(json); - Assert.AreEqual(1, json.Count); + // Assert: Verify the JSON output has the expected structure and values + Assert.NotNull(json); + Assert.Single(json); SpdxJsonHelpers.AssertEqual("SPDXRef-DoapSource", json[0]?["SPDXID"]); SpdxJsonHelpers.AssertEqual("./src/org/spdx/parser/DOAPProject.java", json[0]?["fileName"]); SpdxJsonHelpers.AssertEqual("SOURCE", json[0]?["fileTypes"]?[0]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackage.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackage.cs index 8d325dc..8d7db16 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackage.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackage.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializePackage { /// /// Tests serializing a package. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializePackage_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializePackage_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxPackage object var package = new SpdxPackage @@ -97,7 +96,7 @@ public void Spdx2JsonSerializer_SerializePackage_CorrectResults() var json = Spdx2JsonSerializer.SerializePackage(package); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("SPDXRef-Package", json["SPDXID"]); SpdxJsonHelpers.AssertEqual("glibc", json["name"]); SpdxJsonHelpers.AssertEqual("2.11.1", json["versionInfo"]); @@ -136,8 +135,8 @@ public void Spdx2JsonSerializer_SerializePackage_CorrectResults() /// /// Tests serializing multiple packages. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializePackages_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializePackages_ValidInput_CorrectResults() { // Arrange: Create a sample array of SpdxPackage objects var packages = new[] @@ -205,8 +204,8 @@ public void Spdx2JsonSerializer_SerializePackages_CorrectResults() var json = Spdx2JsonSerializer.SerializePackages(packages); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(1, json.Count); + Assert.NotNull(json); + Assert.Single(json); SpdxJsonHelpers.AssertEqual("SPDXRef-Package", json[0]?["SPDXID"]); SpdxJsonHelpers.AssertEqual("glibc", json[0]?["name"]); SpdxJsonHelpers.AssertEqual("2.11.1", json[0]?["versionInfo"]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackageVerificationCode.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackageVerificationCode.cs index a714a4e..7d62d32 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackageVerificationCode.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializePackageVerificationCode.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializePackageVerificationCode { /// /// Tests serializing a package verification code. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializePackageVerificationCode_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeVerificationCode_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxPackageVerificationCode object var code = new SpdxPackageVerificationCode @@ -49,7 +48,7 @@ public void Spdx2JsonSerializer_SerializePackageVerificationCode_CorrectResults( var json = Spdx2JsonSerializer.SerializeVerificationCode(code); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("d3b07384d113edec49eaa6238ad5ff00", json["packageVerificationCodeValue"]); SpdxJsonHelpers.AssertEqual("file1.txt", json["packageVerificationCodeExcludedFiles"]?[0]); SpdxJsonHelpers.AssertEqual("file2.txt", json["packageVerificationCodeExcludedFiles"]?[1]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeRelationship.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeRelationship.cs index 353c5fd..21a5e96 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeRelationship.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeRelationship.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeRelationship { /// /// Tests serializing a relationship. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeRelationship_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeRelationship_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxRelationship object var relationship = new SpdxRelationship @@ -47,7 +46,7 @@ public void Spdx2JsonSerializer_SerializeRelationship_CorrectResults() var json = Spdx2JsonSerializer.SerializeRelationship(relationship); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("SPDXRef-DOCUMENT", json["spdxElementId"]); SpdxJsonHelpers.AssertEqual("SPDXRef-Package", json["relatedSpdxElement"]); SpdxJsonHelpers.AssertEqual("DESCRIBES", json["relationshipType"]); @@ -57,8 +56,8 @@ public void Spdx2JsonSerializer_SerializeRelationship_CorrectResults() /// /// Tests serializing multiple relationships. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeRelationships_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeRelationships_ValidInput_CorrectResults() { // Arrange: Create an array of sample SpdxRelationship objects var relationships = new[] @@ -83,8 +82,8 @@ public void Spdx2JsonSerializer_SerializeRelationships_CorrectResults() var json = Spdx2JsonSerializer.SerializeRelationships(relationships); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(2, json.Count); + Assert.NotNull(json); + Assert.Equal(2, json.Count); SpdxJsonHelpers.AssertEqual("SPDXRef-DOCUMENT", json[0]?["spdxElementId"]); SpdxJsonHelpers.AssertEqual("SPDXRef-Package", json[0]?["relatedSpdxElement"]); SpdxJsonHelpers.AssertEqual("DESCRIBES", json[0]?["relationshipType"]); diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeSnippet.cs b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeSnippet.cs index 8e54276..1b77748 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeSnippet.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/Spdx2JsonSerializeSnippet.cs @@ -25,14 +25,13 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Tests for serializing to JSON. /// -[TestClass] public class Spdx2JsonSerializeSnippet { /// /// Tests serializing a snippet. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeSnippet_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeSnippet_ValidInput_CorrectResults() { // Arrange: Create a sample SpdxSnippet object var snippet = new SpdxSnippet @@ -56,7 +55,7 @@ public void Spdx2JsonSerializer_SerializeSnippet_CorrectResults() var json = Spdx2JsonSerializer.SerializeSnippet(snippet); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); + Assert.NotNull(json); SpdxJsonHelpers.AssertEqual("SPDXRef-Snippet", json["SPDXID"]); SpdxJsonHelpers.AssertEqual("SnippetFromFile", json["snippetFromFile"]); SpdxJsonHelpers.AssertEqual("Name", json["name"]); @@ -79,8 +78,8 @@ public void Spdx2JsonSerializer_SerializeSnippet_CorrectResults() /// /// Tests serializing multiple snippets. /// - [TestMethod] - public void Spdx2JsonSerializer_SerializeSnippets_CorrectResults() + [Fact] + public void Spdx2JsonSerializer_SerializeSnippets_ValidInput_CorrectResults() { // Arrange: Create a sample array of SpdxSnippet objects var snippets = new[] @@ -107,8 +106,8 @@ public void Spdx2JsonSerializer_SerializeSnippets_CorrectResults() var json = Spdx2JsonSerializer.SerializeSnippets(snippets); // Assert: Verify the JSON is not null and has the expected structure - Assert.IsNotNull(json); - Assert.AreEqual(1, json.Count); + Assert.NotNull(json); + Assert.Single(json); SpdxJsonHelpers.AssertEqual("SPDXRef-Snippet", json[0]?["SPDXID"]); SpdxJsonHelpers.AssertEqual("SnippetFromFile", json[0]?["snippetFromFile"]); SpdxJsonHelpers.AssertEqual("Name", json[0]?["name"]); @@ -127,4 +126,109 @@ public void Spdx2JsonSerializer_SerializeSnippets_CorrectResults() SpdxJsonHelpers.AssertEqual("SnippetFromFile", json[0]?["ranges"]?[1]?["startPointer"]?["reference"]); SpdxJsonHelpers.AssertEqual("3", json[0]?["ranges"]?[1]?["startPointer"]?["lineNumber"]); } + + /// + /// Tests serializing a snippet that includes an annotation, covering the annotation branch. + /// + [Fact] + public void Spdx2JsonSerializer_SerializeSnippet_WithAnnotation_IncludesAnnotation() + { + // Arrange: Create a snippet with a single annotation + var snippet = new SpdxSnippet + { + Id = "SPDXRef-SnippetAnnotated", + SnippetFromFile = "SPDXRef-SourceFile", + SnippetByteStart = 10, + SnippetByteEnd = 20, + ConcludedLicense = "MIT", + LicenseInfoInSnippet = ["MIT"], + CopyrightText = "Copyright 2024", + Annotations = + [ + new SpdxAnnotation + { + Annotator = "Tool: TestTool", + Date = "2024-01-01T00:00:00Z", + Type = SpdxAnnotationType.Review, + Comment = "Reviewed this snippet" + } + ] + }; + + // Act: Serialize the snippet to JSON + var json = Spdx2JsonSerializer.SerializeSnippet(snippet); + + // Assert: Verify the annotation is present in the serialized output + Assert.NotNull(json); + Assert.NotNull(json["annotations"]); + SpdxJsonHelpers.AssertEqual("Tool: TestTool", json["annotations"]?[0]?["annotator"]); + SpdxJsonHelpers.AssertEqual("2024-01-01T00:00:00Z", json["annotations"]?[0]?["annotationDate"]); + SpdxJsonHelpers.AssertEqual("REVIEW", json["annotations"]?[0]?["annotationType"]); + SpdxJsonHelpers.AssertEqual("Reviewed this snippet", json["annotations"]?[0]?["comment"]); + } + + /// + /// Tests that a snippet with both line values set to zero emits only the byte-range entry. + /// + [Fact] + public void Spdx2JsonSerializer_SerializeSnippet_NoLineRange_EmitsByteRangeOnly() + { + // Arrange: Create a snippet with zero line values + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "SPDXRef-SourceFile", + SnippetByteStart = 10, + SnippetByteEnd = 20, + SnippetLineStart = 0, + SnippetLineEnd = 0, + ConcludedLicense = "MIT", + LicenseInfoInSnippet = ["MIT"], + CopyrightText = "Copyright 2024" + }; + + // Act: Serialize the snippet to JSON + var json = Spdx2JsonSerializer.SerializeSnippet(snippet); + + // Assert: Only one ranges entry (byte-range) is present — no line-range + Assert.NotNull(json); + Assert.NotNull(json["ranges"]); + Assert.Single(json["ranges"]!.AsArray()); + SpdxJsonHelpers.AssertEqual("10", json["ranges"]?[0]?["startPointer"]?["offset"]); + SpdxJsonHelpers.AssertEqual("20", json["ranges"]?[0]?["endPointer"]?["offset"]); + Assert.Null(json["ranges"]?[0]?["startPointer"]?["lineNumber"]); + } + + /// + /// Tests that a snippet with only one line value non-zero emits only the byte-range entry + /// (verifies AND logic — both values must be non-zero for line-range to be emitted). + /// + [Fact] + public void Spdx2JsonSerializer_SerializeSnippet_PartialLineRange_EmitsByteRangeOnly() + { + // Arrange: Create a snippet where only one line value is non-zero + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "SPDXRef-SourceFile", + SnippetByteStart = 10, + SnippetByteEnd = 20, + SnippetLineStart = 5, + SnippetLineEnd = 0, + ConcludedLicense = "MIT", + LicenseInfoInSnippet = ["MIT"], + CopyrightText = "Copyright 2024" + }; + + // Act: Serialize the snippet to JSON + var json = Spdx2JsonSerializer.SerializeSnippet(snippet); + + // Assert: Only one ranges entry (byte-range) is present — partial line-range is not emitted + Assert.NotNull(json); + Assert.NotNull(json["ranges"]); + Assert.Single(json["ranges"]!.AsArray()); + SpdxJsonHelpers.AssertEqual("10", json["ranges"]?[0]?["startPointer"]?["offset"]); + SpdxJsonHelpers.AssertEqual("20", json["ranges"]?[0]?["endPointer"]?["offset"]); + Assert.Null(json["ranges"]?[0]?["startPointer"]?["lineNumber"]); + } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/SpdxJsonHelpers.cs b/test/DemaConsulting.SpdxModel.Tests/IO/SpdxJsonHelpers.cs index efd800a..ed8074b 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/SpdxJsonHelpers.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/SpdxJsonHelpers.cs @@ -34,7 +34,7 @@ internal static class SpdxJsonHelpers /// JSON node public static void AssertEqual(string expected, JsonNode? node) { - Assert.IsNotNull(node); - Assert.AreEqual(expected, node.ToString()); + Assert.NotNull(node); + Assert.Equal(expected, node.ToString()); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/IO/SpdxModelIOTests.cs b/test/DemaConsulting.SpdxModel.Tests/IO/SpdxModelIOTests.cs index aa37d3e..b681d39 100644 --- a/test/DemaConsulting.SpdxModel.Tests/IO/SpdxModelIOTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/IO/SpdxModelIOTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.SpdxModel.Tests.IO; /// /// Integration tests for the SpdxModel IO subsystem. /// -[TestClass] public class SpdxModelIOTests { /// /// Tests that an SPDX 2.2 document survives a JSON serialization round trip. /// - [TestMethod] + [Fact] public void SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidDocument() { // Arrange: Load the SPDX 2.2 JSON example from embedded resources @@ -44,18 +43,18 @@ public void SpdxModelIO_ReadWriteSpdxJson_Spdx22Document_RoundTripProducesValidD var roundTripped = Spdx2JsonDeserializer.Deserialize(serialized); // Assert: Verify the round-tripped document is valid and matches the original - Assert.IsNotNull(roundTripped); - Assert.AreEqual(original.Name, roundTripped.Name); - Assert.AreEqual(original.Version, roundTripped.Version); + Assert.NotNull(roundTripped); + Assert.Equal(original.Name, roundTripped.Name); + Assert.Equal(original.Version, roundTripped.Version); var issues = new List(); roundTripped.Validate(issues); - Assert.IsEmpty(issues); + Assert.Empty(issues); } /// /// Tests that an SPDX 2.3 document survives a JSON serialization round trip. /// - [TestMethod] + [Fact] public void SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidDocument() { // Arrange: Load the SPDX 2.3 JSON example from embedded resources @@ -68,11 +67,27 @@ public void SpdxModelIO_ReadWriteSpdxJson_Spdx23Document_RoundTripProducesValidD var roundTripped = Spdx2JsonDeserializer.Deserialize(serialized); // Assert: Verify the round-tripped document is valid and matches the original - Assert.IsNotNull(roundTripped); - Assert.AreEqual(original.Name, roundTripped.Name); - Assert.AreEqual(original.Version, roundTripped.Version); + Assert.NotNull(roundTripped); + Assert.Equal(original.Name, roundTripped.Name); + Assert.Equal(original.Version, roundTripped.Version); var issues = new List(); roundTripped.Validate(issues); - Assert.IsEmpty(issues); + Assert.Empty(issues); + } + + /// + /// Tests that malformed JSON throws a JsonException during deserialization. + /// + [Fact] + public void SpdxModelIO_ReadSpdxJson_InvalidJson_ThrowsJsonException() + { + // Arrange: Prepare malformed JSON text + const string malformedJson = "{ not valid json at all }"; + + // Act: Capture any exception thrown by the deserializer + var exception = Record.Exception(() => Spdx2JsonDeserializer.Deserialize(malformedJson)); + + // Assert: The exception must be a JsonException (or derived type such as JsonReaderException) + Assert.IsType(exception, exactMatch: false); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxAnnotationTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxAnnotationTests.cs index 33dd4c1..2ef119f 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxAnnotationTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxAnnotationTests.cs @@ -23,13 +23,22 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Uses xUnit v3 as the test framework. Each test method is fully isolated: +/// no shared state is maintained between tests and all test inputs are constructed +/// inline within the method body. +/// public class SpdxAnnotationTests { /// /// Tests the comparer compares annotations correctly. /// - [TestMethod] + /// + /// Verifies reflexive equality (each annotation equals itself), cross-equality (two + /// annotations with identical fields but different IDs are equal), and that hash codes + /// match for equal annotations. + /// + [Fact] public void SpdxAnnotation_SameComparer_ComparesCorrectly() { // Arrange: Create three annotations with different properties @@ -55,27 +64,32 @@ public void SpdxAnnotation_SameComparer_ComparesCorrectly() Type = SpdxAnnotationType.Other }; - // Assert: Verify annotations compare to themselves - Assert.IsTrue(SpdxAnnotation.Same.Equals(a1, a1)); - Assert.IsTrue(SpdxAnnotation.Same.Equals(a2, a2)); - Assert.IsTrue(SpdxAnnotation.Same.Equals(a3, a3)); - - // Assert: Verify annotations compare correctly - Assert.IsTrue(SpdxAnnotation.Same.Equals(a1, a2)); - Assert.IsTrue(SpdxAnnotation.Same.Equals(a2, a1)); - Assert.IsFalse(SpdxAnnotation.Same.Equals(a1, a3)); - Assert.IsFalse(SpdxAnnotation.Same.Equals(a3, a1)); - Assert.IsFalse(SpdxAnnotation.Same.Equals(a2, a3)); - Assert.IsFalse(SpdxAnnotation.Same.Equals(a3, a2)); - - // Assert: Verify same annotations have identical hashes - Assert.AreEqual(SpdxAnnotation.Same.GetHashCode(a1), SpdxAnnotation.Same.GetHashCode(a2)); + // Act / Assert: Verify annotations compare to themselves + Assert.True(SpdxAnnotation.Same.Equals(a1, a1)); + Assert.True(SpdxAnnotation.Same.Equals(a2, a2)); + Assert.True(SpdxAnnotation.Same.Equals(a3, a3)); + + // Act / Assert: Verify annotations compare correctly + Assert.True(SpdxAnnotation.Same.Equals(a1, a2)); + Assert.True(SpdxAnnotation.Same.Equals(a2, a1)); + Assert.False(SpdxAnnotation.Same.Equals(a1, a3)); + Assert.False(SpdxAnnotation.Same.Equals(a3, a1)); + Assert.False(SpdxAnnotation.Same.Equals(a2, a3)); + Assert.False(SpdxAnnotation.Same.Equals(a3, a2)); + + // Act / Assert: Verify same annotations have identical hashes + Assert.Equal(SpdxAnnotation.Same.GetHashCode(a1), SpdxAnnotation.Same.GetHashCode(a2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] + /// + /// Verifies that the deep copy is logically equal (all fields match) but is a distinct + /// object (not reference-equal), confirming no shared mutable references exist between + /// original and copy. + /// + [Fact] public void SpdxAnnotation_DeepCopy_CreatesEqualButDistinctInstance() { // Arrange: Create an original SpdxAnnotation object @@ -91,21 +105,27 @@ public void SpdxAnnotation_DeepCopy_CreatesEqualButDistinctInstance() var a2 = a1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(a1, a2, SpdxAnnotation.Same); - Assert.AreEqual(a1.Annotator, a2.Annotator); - Assert.AreEqual(a1.Date, a2.Date); - Assert.AreEqual(a1.Type, a2.Type); - Assert.AreEqual(a1.Comment, a2.Comment); + Assert.Equal(a1, a2, SpdxAnnotation.Same); + Assert.Equal(a1.Annotator, a2.Annotator); + Assert.Equal(a1.Date, a2.Date); + Assert.Equal(a1.Type, a2.Type); + Assert.Equal(a1.Comment, a2.Comment); // Assert: Verify deep-copy has distinct instance - Assert.IsFalse(ReferenceEquals(a1, a2)); + Assert.False(ReferenceEquals(a1, a2)); } /// /// Tests the method adds or updates /// information correctly /// - [TestMethod] + /// + /// Verifies two sub-scenarios in one test: (1) an existing annotation is enhanced with + /// additional field values from a matching entry (Id is populated), and (2) a new + /// annotation is appended when no match exists. Both sub-scenarios use the + /// comparer for matching. + /// + [Fact] public void SpdxAnnotation_Enhance_AddsOrUpdatesInformationCorrectly() { // Arrange: Create an array of annotations with one annotation @@ -142,21 +162,26 @@ public void SpdxAnnotation_Enhance_AddsOrUpdatesInformationCorrectly() ]); // Assert: Verify the annotations array has correct information - Assert.HasCount(2, annotations); - Assert.AreEqual("SPDXRef-Annotation1", annotations[0].Id); - Assert.AreEqual("Person: Malcolm Nixon", annotations[0].Annotator); - Assert.AreEqual("2024-05-28T01:30:00Z", annotations[0].Date); - Assert.AreEqual(SpdxAnnotationType.Review, annotations[0].Type); - Assert.AreEqual("Looks good", annotations[0].Comment); - Assert.AreEqual("Person: John Doe", annotations[1].Annotator); - Assert.AreEqual("2023-11-20T12:34:23Z", annotations[1].Date); - Assert.AreEqual(SpdxAnnotationType.Other, annotations[1].Type); + Assert.Equal(2, annotations.Length); + Assert.Equal("SPDXRef-Annotation1", annotations[0].Id); + Assert.Equal("Person: Malcolm Nixon", annotations[0].Annotator); + Assert.Equal("2024-05-28T01:30:00Z", annotations[0].Date); + Assert.Equal(SpdxAnnotationType.Review, annotations[0].Type); + Assert.Equal("Looks good", annotations[0].Comment); + Assert.Equal("Person: John Doe", annotations[1].Annotator); + Assert.Equal("2023-11-20T12:34:23Z", annotations[1].Date); + Assert.Equal(SpdxAnnotationType.Other, annotations[1].Type); } /// /// Tests the method reports bad annotators. /// - [TestMethod] + /// + /// Boundary: an empty string is the only invalid + /// field; all other fields are valid so that the issue list contains exactly one entry + /// for the annotator. + /// + [Fact] public void SpdxAnnotation_Validate_InvalidAnnotator() { // Arrange: Create a bad annotation @@ -173,13 +198,17 @@ public void SpdxAnnotation_Validate_InvalidAnnotator() annotation.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Test Invalid Annotator Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Test Invalid Annotator Field - Empty")); } /// /// Tests the method reports bad dates. /// - [TestMethod] + /// + /// Boundary: a non-ISO-8601 date string is the only invalid field; all other fields are + /// valid so that the issue list contains exactly one entry for the date. + /// + [Fact] public void SpdxAnnotation_Validate_InvalidDate() { // Arrange: Create a bad annotation @@ -196,13 +225,17 @@ public void SpdxAnnotation_Validate_InvalidDate() annotation.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Test Invalid Annotation Date Field 'BadDate'"), issues); + Assert.Contains(issues, issue => issue.Contains("Test Invalid Annotation Date Field 'BadDate'")); } /// /// Tests the method reports bad annotation types. /// - [TestMethod] + /// + /// Boundary: is the only invalid field; all + /// other fields are valid so that the issue list contains exactly one entry for the type. + /// + [Fact] public void SpdxAnnotation_Validate_InvalidType() { // Arrange: Create a bad annotation @@ -219,13 +252,19 @@ public void SpdxAnnotation_Validate_InvalidType() annotation.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Test Invalid Annotation Type Field - Missing"), issues); + Assert.Contains(issues, issue => issue.Contains("Test Invalid Annotation Type Field - Missing")); } /// /// Tests the method reports bad comments. /// - [TestMethod] + /// + /// Boundary: an empty string is the only invalid + /// field; is set to + /// (valid) so that the issue list contains + /// exactly one entry for the comment. + /// + [Fact] public void SpdxAnnotation_Validate_InvalidComment() { // Arrange: Create a bad annotation @@ -233,7 +272,7 @@ public void SpdxAnnotation_Validate_InvalidComment() { Annotator = "Person: Malcolm Nixon", Date = "2024-05-28T01:30:00Z", - Type = SpdxAnnotationType.Missing, + Type = SpdxAnnotationType.Review, Comment = "" }; @@ -242,53 +281,104 @@ public void SpdxAnnotation_Validate_InvalidComment() annotation.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Test Invalid Annotation Comment - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Test Invalid Annotation Comment - Empty")); } /// /// Tests the method for valid annotation types. /// - [TestMethod] + /// + /// Verifies case-insensitive mapping: "REVIEW", "review", and "Review" all map to + /// ; similarly for "OTHER". An empty string maps + /// to . + /// + [Fact] public void SpdxAnnotationTypeExtensions_FromText_Valid() { - Assert.AreEqual(SpdxAnnotationType.Missing, SpdxAnnotationTypeExtensions.FromText("")); - Assert.AreEqual(SpdxAnnotationType.Review, SpdxAnnotationTypeExtensions.FromText("REVIEW")); - Assert.AreEqual(SpdxAnnotationType.Review, SpdxAnnotationTypeExtensions.FromText("review")); - Assert.AreEqual(SpdxAnnotationType.Review, SpdxAnnotationTypeExtensions.FromText("Review")); - Assert.AreEqual(SpdxAnnotationType.Other, SpdxAnnotationTypeExtensions.FromText("OTHER")); - Assert.AreEqual(SpdxAnnotationType.Other, SpdxAnnotationTypeExtensions.FromText("other")); - Assert.AreEqual(SpdxAnnotationType.Other, SpdxAnnotationTypeExtensions.FromText("Other")); + // Arrange: no setup needed - testing pure string-to-enum conversion + + // Act / Assert: each recognized text converts to the correct enum value + Assert.Equal(SpdxAnnotationType.Missing, SpdxAnnotationTypeExtensions.FromText("")); + Assert.Equal(SpdxAnnotationType.Review, SpdxAnnotationTypeExtensions.FromText("REVIEW")); + Assert.Equal(SpdxAnnotationType.Review, SpdxAnnotationTypeExtensions.FromText("review")); + Assert.Equal(SpdxAnnotationType.Review, SpdxAnnotationTypeExtensions.FromText("Review")); + Assert.Equal(SpdxAnnotationType.Other, SpdxAnnotationTypeExtensions.FromText("OTHER")); + Assert.Equal(SpdxAnnotationType.Other, SpdxAnnotationTypeExtensions.FromText("other")); + Assert.Equal(SpdxAnnotationType.Other, SpdxAnnotationTypeExtensions.FromText("Other")); } /// /// Tests the method for an invalid annotation type. /// - [TestMethod] + /// + /// Boundary: an unrecognized string causes with + /// the expected message, confirming the error path is not silently swallowed. + /// + [Fact] public void SpdxAnnotationTypeExtensions_FromText_Invalid() { + // Arrange: no setup needed — testing pure string-to-enum conversion error path + + // Act / Assert: var exception = - Assert.ThrowsExactly(() => SpdxAnnotationTypeExtensions.FromText("invalid")); - Assert.AreEqual("Unsupported SPDX Annotation Type 'invalid'", exception.Message); + Assert.Throws(() => SpdxAnnotationTypeExtensions.FromText("invalid")); + Assert.Equal("Unsupported SPDX Annotation Type 'invalid'", exception.Message); } /// /// Tests the method for valid annotation types. /// - [TestMethod] + /// + /// Verifies that produces "REVIEW" and + /// produces "OTHER". + /// + [Fact] public void SpdxAnnotationTypeExtensions_ToText_Valid() { - Assert.AreEqual("REVIEW", SpdxAnnotationType.Review.ToText()); - Assert.AreEqual("OTHER", SpdxAnnotationType.Other.ToText()); + // Arrange: no setup needed - testing pure enum-to-string conversion + + // Act / Assert: each recognized enum value converts to the expected text + Assert.Equal("REVIEW", SpdxAnnotationType.Review.ToText()); + Assert.Equal("OTHER", SpdxAnnotationType.Other.ToText()); } /// /// Tests the method for an invalid annotation /// type. /// - [TestMethod] + /// + /// Boundary: an unrecognized numeric enum value (1000, which is not a defined + /// member) causes + /// with the expected message. + /// + [Fact] public void SpdxAnnotationTypeExtensions_ToText_Invalid() { - var exception = Assert.ThrowsExactly(() => ((SpdxAnnotationType)1000).ToText()); - Assert.AreEqual("Unsupported SPDX Annotation Type '1000'", exception.Message); + // Arrange: no setup needed - testing pure enum-to-string conversion error path + + // Act / Assert: an unknown numeric enum value throws + var exception = Assert.Throws(() => ((SpdxAnnotationType)1000).ToText()); + Assert.Equal("Unsupported SPDX Annotation Type '1000'", exception.Message); + } + + /// + /// Tests that throws for the + /// sentinel value. + /// + /// + /// Boundary: is a sentinel value that must + /// never be serialized. Calling ToText with it throws + /// with the expected "Attempt to serialize + /// missing SPDX Annotation Type" message. + /// + [Fact] + public void SpdxAnnotationTypeExtensions_ToText_Missing() + { + // Arrange: no setup needed - testing the Missing sentinel value error path + + // Act / Assert: Missing throws with the expected message + var exception = Assert.Throws( + () => SpdxAnnotationType.Missing.ToText()); + Assert.Equal("Attempt to serialize missing SPDX Annotation Type", exception.Message); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxChecksumTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxChecksumTests.cs index 64e4450..c6b4d51 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxChecksumTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxChecksumTests.cs @@ -23,15 +23,25 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Tests the class and the +/// extension methods. Uses xUnit v3 as the +/// test framework. Each test method is fully self-contained with no shared fixture state. +/// public class SpdxChecksumTests { /// /// Tests the comparer compares checksums correctly. /// - [TestMethod] - public void SpdxChecksum_SameComparer_ComparesCorrectly() + /// + /// Sets up three checksums: two with identical algorithm and value (c1 and c2) and one + /// with a different algorithm and value (c3). Verifies reflexive, symmetric, and + /// cross-inequality comparisons, and that equal checksums produce identical hash codes. + /// + [Fact] + public void SpdxChecksum_SameComparer_SameOrDifferentValues_ReturnsCorrectEquality() { + // Arrange: Create three checksums with different algorithm/value combinations var c1 = new SpdxChecksum { Algorithm = SpdxChecksumAlgorithm.Sha1, @@ -50,28 +60,33 @@ public void SpdxChecksum_SameComparer_ComparesCorrectly() Value = "624c1abb3664f4b35547e7c73864ad24" }; - // Assert checksums compare to themselves - Assert.IsTrue(SpdxChecksum.Same.Equals(c1, c1)); - Assert.IsTrue(SpdxChecksum.Same.Equals(c2, c2)); - Assert.IsTrue(SpdxChecksum.Same.Equals(c3, c3)); - - // Assert checksums compare correctly - Assert.IsTrue(SpdxChecksum.Same.Equals(c1, c2)); - Assert.IsTrue(SpdxChecksum.Same.Equals(c2, c1)); - Assert.IsFalse(SpdxChecksum.Same.Equals(c1, c3)); - Assert.IsFalse(SpdxChecksum.Same.Equals(c3, c1)); - Assert.IsFalse(SpdxChecksum.Same.Equals(c2, c3)); - Assert.IsFalse(SpdxChecksum.Same.Equals(c3, c2)); - - // Assert same checksums have identical hashes - Assert.AreEqual(SpdxChecksum.Same.GetHashCode(c1), SpdxChecksum.Same.GetHashCode(c2)); + // Act / Assert: Verify checksums compare to themselves + Assert.True(SpdxChecksum.Same.Equals(c1, c1)); + Assert.True(SpdxChecksum.Same.Equals(c2, c2)); + Assert.True(SpdxChecksum.Same.Equals(c3, c3)); + + // Assert: Verify checksums compare correctly + Assert.True(SpdxChecksum.Same.Equals(c1, c2)); + Assert.True(SpdxChecksum.Same.Equals(c2, c1)); + Assert.False(SpdxChecksum.Same.Equals(c1, c3)); + Assert.False(SpdxChecksum.Same.Equals(c3, c1)); + Assert.False(SpdxChecksum.Same.Equals(c2, c3)); + Assert.False(SpdxChecksum.Same.Equals(c3, c2)); + + // Assert: same checksums have identical hashes + Assert.Equal(SpdxChecksum.Same.GetHashCode(c1), SpdxChecksum.Same.GetHashCode(c2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxChecksum_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Creates a fully-populated checksum instance and deep-copies it. Verifies that the + /// copy has equal field values (Algorithm and Value) but is a distinct object reference + /// from the original, confirming no shallow aliasing. + /// + [Fact] + public void SpdxChecksum_DeepCopy_PopulatedChecksum_CreatesEqualButDistinctInstance() { // Arrange: Create a checksum instance var c1 = new SpdxChecksum @@ -84,20 +99,25 @@ public void SpdxChecksum_DeepCopy_CreatesEqualButDistinctInstance() var c2 = c1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(c1, c2, SpdxChecksum.Same); - Assert.AreEqual(c1.Algorithm, c2.Algorithm); - Assert.AreEqual(c1.Value, c2.Value); + Assert.Equal(c1, c2, SpdxChecksum.Same); + Assert.Equal(c1.Algorithm, c2.Algorithm); + Assert.Equal(c1.Value, c2.Value); // Assert: Verify deep-copy has distinct instance - Assert.IsFalse(ReferenceEquals(c1, c2)); + Assert.False(ReferenceEquals(c1, c2)); } /// /// Tests the method adds or updates information /// correctly. /// - [TestMethod] - public void SpdxChecksum_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Starts with a single SHA1 checksum and enhances with a list containing the same SHA1 + /// entry and a new MD5 entry. Verifies that the existing entry is preserved and the new + /// entry is appended, resulting in exactly two checksums. + /// + [Fact] + public void SpdxChecksum_Enhance_ExistingAndNewAlgorithms_AddsOrUpdatesInformation() { // Arrange: Create an original checksum var checksums = new[] @@ -126,18 +146,23 @@ public void SpdxChecksum_Enhance_AddsOrUpdatesInformationCorrectly() ]); // Assert: Verify checksums contain the expected values - Assert.HasCount(2, checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, checksums[0].Algorithm); - Assert.AreEqual("c2b4e1c67a2d28fced849ee1bb76e7391b93f125", checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, checksums[1].Algorithm); - Assert.AreEqual("624c1abb3664f4b35547e7c73864ad24", checksums[1].Value); + Assert.Equal(2, checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, checksums[0].Algorithm); + Assert.Equal("c2b4e1c67a2d28fced849ee1bb76e7391b93f125", checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Md5, checksums[1].Algorithm); + Assert.Equal("624c1abb3664f4b35547e7c73864ad24", checksums[1].Value); } /// /// Tests the method reports bad algorithms. /// - [TestMethod] - public void SpdxChecksum_Validate_InvalidAlgorithm() + /// + /// Uses as the algorithm sentinel to confirm + /// that the validator catches the absent algorithm and includes the expected description + /// string in the reported issue. + /// + [Fact] + public void SpdxChecksum_Validate_MissingAlgorithm_ReportsAlgorithmIssue() { // Arrange: Create a bad instance var checksum = new SpdxChecksum @@ -151,14 +176,19 @@ public void SpdxChecksum_Validate_InvalidAlgorithm() checksum.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Test Invalid Checksum Algorithm Field - Missing"), issues); + Assert.Contains(issues, issue => issue.Contains("Test Invalid Checksum Algorithm Field - Missing")); } /// /// Tests the method reports bad values. /// - [TestMethod] - public void SpdxChecksum_Validate_InvalidValue() + /// + /// Uses an empty string as the checksum value — the minimal invalid state — to confirm + /// that the validator catches the empty value and includes the expected description string + /// in the reported issue. + /// + [Fact] + public void SpdxChecksum_Validate_EmptyValue_ReportsValueIssue() { // Arrange: Create a bad instance var checksum = new SpdxChecksum @@ -172,80 +202,255 @@ public void SpdxChecksum_Validate_InvalidValue() checksum.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Test Invalid Checksum Value Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Test Invalid Checksum Value Field - Empty")); + } + + /// + /// Tests the method reports unknown numeric algorithms. + /// + /// + /// Casts the integer literal 1000 to to produce an + /// out-of-range enum value. Verifies that the validator treats it as an unknown algorithm + /// and reports the expected diagnostic message. + /// + [Fact] + public void SpdxChecksum_Validate_UnknownNumericAlgorithm_ReportsAlgorithmIssue() + { + // Arrange: Create a checksum with an out-of-range numeric algorithm value + var checksum = new SpdxChecksum + { + Algorithm = (SpdxChecksumAlgorithm)1000, + Value = "c2b4e1c67a2d28fced849ee1bb76e7391b93f125" + }; + + // Act: Perform validation on the SpdxChecksum instance + var issues = new List(); + checksum.Validate("Test", issues); + + // Assert: Verify that the validation reports the unknown algorithm + Assert.Contains(issues, issue => issue.Contains("Test Invalid Checksum Algorithm Field - Unknown")); } /// - /// Tests the method. + /// Tests that FromText maps every known SPDX algorithm string to the correct enum value. /// - [TestMethod] - public void SpdxChecksumAlgorithmExtensions_FromText_Valid() + /// + /// Exercises every known algorithm string defined by the SPDX 2.x specification, plus + /// case-variant inputs for SHA1, to confirm that + /// maps each string to the correct enum value. + /// + [Fact] + public void SpdxChecksumAlgorithmExtensions_FromText_KnownAlgorithmStrings_ReturnsCorrectEnumValues() { - Assert.AreEqual(SpdxChecksumAlgorithm.Missing, SpdxChecksumAlgorithmExtensions.FromText("")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, SpdxChecksumAlgorithmExtensions.FromText("SHA1")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, SpdxChecksumAlgorithmExtensions.FromText("sha1")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, SpdxChecksumAlgorithmExtensions.FromText("Sha1")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha224, SpdxChecksumAlgorithmExtensions.FromText("SHA224")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha256, SpdxChecksumAlgorithmExtensions.FromText("SHA256")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha384, SpdxChecksumAlgorithmExtensions.FromText("SHA384")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha512, SpdxChecksumAlgorithmExtensions.FromText("SHA512")); - Assert.AreEqual(SpdxChecksumAlgorithm.Md2, SpdxChecksumAlgorithmExtensions.FromText("MD2")); - Assert.AreEqual(SpdxChecksumAlgorithm.Md4, SpdxChecksumAlgorithmExtensions.FromText("MD4")); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, SpdxChecksumAlgorithmExtensions.FromText("MD5")); - Assert.AreEqual(SpdxChecksumAlgorithm.Md6, SpdxChecksumAlgorithmExtensions.FromText("MD6")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha3256, SpdxChecksumAlgorithmExtensions.FromText("SHA3-256")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha3384, SpdxChecksumAlgorithmExtensions.FromText("SHA3-384")); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha3512, SpdxChecksumAlgorithmExtensions.FromText("SHA3-512")); - Assert.AreEqual(SpdxChecksumAlgorithm.Blake2B256, SpdxChecksumAlgorithmExtensions.FromText("BLAKE2b-256")); - Assert.AreEqual(SpdxChecksumAlgorithm.Blake2B384, SpdxChecksumAlgorithmExtensions.FromText("BLAKE2b-384")); - Assert.AreEqual(SpdxChecksumAlgorithm.Blake2B512, SpdxChecksumAlgorithmExtensions.FromText("BLAKE2b-512")); - Assert.AreEqual(SpdxChecksumAlgorithm.Blake3, SpdxChecksumAlgorithmExtensions.FromText("BLAKE3")); - Assert.AreEqual(SpdxChecksumAlgorithm.Adler32, SpdxChecksumAlgorithmExtensions.FromText("ADLER32")); + // Arrange: Known algorithm strings are implicit in the Act/Assert pairs below + + // Act / Assert: Verify each known algorithm string maps to the correct enum value + Assert.Equal(SpdxChecksumAlgorithm.Missing, SpdxChecksumAlgorithmExtensions.FromText("")); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, SpdxChecksumAlgorithmExtensions.FromText("SHA1")); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, SpdxChecksumAlgorithmExtensions.FromText("sha1")); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, SpdxChecksumAlgorithmExtensions.FromText("Sha1")); + Assert.Equal(SpdxChecksumAlgorithm.Sha224, SpdxChecksumAlgorithmExtensions.FromText("SHA224")); + Assert.Equal(SpdxChecksumAlgorithm.Sha256, SpdxChecksumAlgorithmExtensions.FromText("SHA256")); + Assert.Equal(SpdxChecksumAlgorithm.Sha384, SpdxChecksumAlgorithmExtensions.FromText("SHA384")); + Assert.Equal(SpdxChecksumAlgorithm.Sha512, SpdxChecksumAlgorithmExtensions.FromText("SHA512")); + Assert.Equal(SpdxChecksumAlgorithm.Md2, SpdxChecksumAlgorithmExtensions.FromText("MD2")); + Assert.Equal(SpdxChecksumAlgorithm.Md4, SpdxChecksumAlgorithmExtensions.FromText("MD4")); + Assert.Equal(SpdxChecksumAlgorithm.Md5, SpdxChecksumAlgorithmExtensions.FromText("MD5")); + Assert.Equal(SpdxChecksumAlgorithm.Md6, SpdxChecksumAlgorithmExtensions.FromText("MD6")); + Assert.Equal(SpdxChecksumAlgorithm.Sha3256, SpdxChecksumAlgorithmExtensions.FromText("SHA3-256")); + Assert.Equal(SpdxChecksumAlgorithm.Sha3384, SpdxChecksumAlgorithmExtensions.FromText("SHA3-384")); + Assert.Equal(SpdxChecksumAlgorithm.Sha3512, SpdxChecksumAlgorithmExtensions.FromText("SHA3-512")); + Assert.Equal(SpdxChecksumAlgorithm.Blake2B256, SpdxChecksumAlgorithmExtensions.FromText("BLAKE2b-256")); + Assert.Equal(SpdxChecksumAlgorithm.Blake2B384, SpdxChecksumAlgorithmExtensions.FromText("BLAKE2b-384")); + Assert.Equal(SpdxChecksumAlgorithm.Blake2B512, SpdxChecksumAlgorithmExtensions.FromText("BLAKE2b-512")); + Assert.Equal(SpdxChecksumAlgorithm.Blake3, SpdxChecksumAlgorithmExtensions.FromText("BLAKE3")); + Assert.Equal(SpdxChecksumAlgorithm.Adler32, SpdxChecksumAlgorithmExtensions.FromText("ADLER32")); } /// - /// Tests the method. + /// Tests that FromText throws InvalidOperationException for an unrecognized algorithm string. /// - [TestMethod] - public void SpdxChecksumAlgorithmExtensions_FromText_InvalidAlgorithm() + /// + /// Passes the unrecognized string "unknown" to confirm that + /// throws + /// with the expected message rather than + /// returning a default value or silently succeeding. + /// + [Fact] + public void SpdxChecksumAlgorithmExtensions_FromText_UnknownAlgorithmString_ThrowsInvalidOperationException() { + // Arrange: Use an algorithm string that is not in the known-algorithm list + // (No variable needed — the input is inlined directly into the Act / Assert.) + + // Act / Assert: Verify that FromText throws for an unrecognized algorithm string var exception = - Assert.ThrowsExactly(() => SpdxChecksumAlgorithmExtensions.FromText("unknown")); - Assert.AreEqual("Unsupported SPDX Checksum Algorithm 'unknown'", exception.Message); + Assert.Throws(() => SpdxChecksumAlgorithmExtensions.FromText("unknown")); + Assert.Equal("Unsupported SPDX Checksum Algorithm 'unknown'", exception.Message); + } + + /// + /// Tests that ToText returns the correct SPDX string for every serializable algorithm enum value. + /// + /// + /// Exercises every serializable enum value to confirm + /// that returns the canonical + /// SPDX 2.x algorithm string for each value. + /// + [Fact] + public void SpdxChecksumAlgorithmExtensions_ToText_KnownAlgorithmEnums_ReturnsCorrectStrings() + { + // Arrange: Known algorithm enum values are implicit in the Act/Assert pairs below + + // Act / Assert: Verify each known algorithm enum maps to the correct string + Assert.Equal("SHA1", SpdxChecksumAlgorithm.Sha1.ToText()); + Assert.Equal("SHA224", SpdxChecksumAlgorithm.Sha224.ToText()); + Assert.Equal("SHA256", SpdxChecksumAlgorithm.Sha256.ToText()); + Assert.Equal("SHA384", SpdxChecksumAlgorithm.Sha384.ToText()); + Assert.Equal("SHA512", SpdxChecksumAlgorithm.Sha512.ToText()); + Assert.Equal("MD2", SpdxChecksumAlgorithm.Md2.ToText()); + Assert.Equal("MD4", SpdxChecksumAlgorithm.Md4.ToText()); + Assert.Equal("MD5", SpdxChecksumAlgorithm.Md5.ToText()); + Assert.Equal("MD6", SpdxChecksumAlgorithm.Md6.ToText()); + Assert.Equal("SHA3-256", SpdxChecksumAlgorithm.Sha3256.ToText()); + Assert.Equal("SHA3-384", SpdxChecksumAlgorithm.Sha3384.ToText()); + Assert.Equal("SHA3-512", SpdxChecksumAlgorithm.Sha3512.ToText()); + Assert.Equal("BLAKE2b-256", SpdxChecksumAlgorithm.Blake2B256.ToText()); + Assert.Equal("BLAKE2b-384", SpdxChecksumAlgorithm.Blake2B384.ToText()); + Assert.Equal("BLAKE2b-512", SpdxChecksumAlgorithm.Blake2B512.ToText()); + Assert.Equal("BLAKE3", SpdxChecksumAlgorithm.Blake3.ToText()); + Assert.Equal("ADLER32", SpdxChecksumAlgorithm.Adler32.ToText()); + } + + /// + /// Tests that ToText throws InvalidOperationException for an out-of-range numeric enum value. + /// + /// + /// Casts the integer literal 1000 to to produce an + /// out-of-range value. Verifies that + /// throws with the expected message. + /// + [Fact] + public void SpdxChecksumAlgorithmExtensions_ToText_OutOfRangeEnum_ThrowsInvalidOperationException() + { + // Arrange: Use a numeric enum value that has no named member in SpdxChecksumAlgorithm + // (No variable needed — the value is inlined directly into the Act / Assert.) + + // Act / Assert: Verify that ToText throws for an out-of-range enum value + var exception = Assert.Throws(() => ((SpdxChecksumAlgorithm)1000).ToText()); + Assert.Equal("Unsupported SPDX Checksum Algorithm '1000'", exception.Message); } /// - /// Tests the method. + /// Tests the method throws for + /// . /// - [TestMethod] - public void SpdxChecksumAlgorithmExtensions_ToText_Valid() + /// + /// Passes — the sentinel value that must + /// never be serialized — to confirm that + /// throws with the expected message. + /// + [Fact] + public void SpdxChecksumAlgorithmExtensions_ToText_MissingAlgorithm_ThrowsInvalidOperationException() { - Assert.AreEqual("SHA1", SpdxChecksumAlgorithm.Sha1.ToText()); - Assert.AreEqual("SHA224", SpdxChecksumAlgorithm.Sha224.ToText()); - Assert.AreEqual("SHA256", SpdxChecksumAlgorithm.Sha256.ToText()); - Assert.AreEqual("SHA384", SpdxChecksumAlgorithm.Sha384.ToText()); - Assert.AreEqual("SHA512", SpdxChecksumAlgorithm.Sha512.ToText()); - Assert.AreEqual("MD2", SpdxChecksumAlgorithm.Md2.ToText()); - Assert.AreEqual("MD4", SpdxChecksumAlgorithm.Md4.ToText()); - Assert.AreEqual("MD5", SpdxChecksumAlgorithm.Md5.ToText()); - Assert.AreEqual("MD6", SpdxChecksumAlgorithm.Md6.ToText()); - Assert.AreEqual("SHA3-256", SpdxChecksumAlgorithm.Sha3256.ToText()); - Assert.AreEqual("SHA3-384", SpdxChecksumAlgorithm.Sha3384.ToText()); - Assert.AreEqual("SHA3-512", SpdxChecksumAlgorithm.Sha3512.ToText()); - Assert.AreEqual("BLAKE2b-256", SpdxChecksumAlgorithm.Blake2B256.ToText()); - Assert.AreEqual("BLAKE2b-384", SpdxChecksumAlgorithm.Blake2B384.ToText()); - Assert.AreEqual("BLAKE2b-512", SpdxChecksumAlgorithm.Blake2B512.ToText()); - Assert.AreEqual("BLAKE3", SpdxChecksumAlgorithm.Blake3.ToText()); - Assert.AreEqual("ADLER32", SpdxChecksumAlgorithm.Adler32.ToText()); + // Arrange: Use the Missing sentinel value, which must never be serialized + + // Act / Assert: Verify that ToText throws for the Missing sentinel + var exception = Assert.Throws( + () => SpdxChecksumAlgorithm.Missing.ToText()); + Assert.Equal("Attempt to serialize missing SPDX Checksum Algorithm", exception.Message); } /// - /// Tests the method. + /// Tests that returns false when the first argument is null. /// - [TestMethod] - public void SpdxChecksumAlgorithmExtensions_ToText_InvalidAlgorithm() + /// + /// Passes a null reference as the first argument and a valid checksum as the second. + /// Verifies that the comparer returns false rather than throwing, exercising the null-guard + /// on the left-hand operand. + /// + [Fact] + public void SpdxChecksum_SameComparer_NullFirstArgument_ReturnsFalse() { - var exception = Assert.ThrowsExactly(() => ((SpdxChecksumAlgorithm)1000).ToText()); - Assert.AreEqual("Unsupported SPDX Checksum Algorithm '1000'", exception.Message); + // Arrange: Create one valid checksum and one null reference + var c1 = new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.Sha1, + Value = "c2b4e1c67a2d28fced849ee1bb76e7391b93f125" + }; + SpdxChecksum? nullChecksum = null; + + // Act: Compare null with a valid checksum + var result = SpdxChecksum.Same.Equals(nullChecksum!, c1); + + // Assert: Verify null-first comparison returns false + Assert.False(result); + } + + /// + /// Tests that returns false when the second argument is null. + /// + /// + /// Passes a valid checksum as the first argument and a null reference as the second. + /// Verifies that the comparer returns false rather than throwing, exercising the null-guard + /// on the right-hand operand. + /// + [Fact] + public void SpdxChecksum_SameComparer_NullSecondArgument_ReturnsFalse() + { + // Arrange: Create one valid checksum and one null reference + var c1 = new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.Sha1, + Value = "c2b4e1c67a2d28fced849ee1bb76e7391b93f125" + }; + SpdxChecksum? nullChecksum = null; + + // Act: Compare a valid checksum with null + var result = SpdxChecksum.Same.Equals(c1, nullChecksum!); + + // Assert: Verify null-second comparison returns false + Assert.False(result); + } + + /// + /// Tests that returns true when both arguments are null. + /// + /// + /// Passes two null references to confirm that the comparer returns true when both + /// operands are null, consistent with standard equality-comparer semantics. + /// + [Fact] + public void SpdxChecksum_SameComparer_BothArgumentsNull_ReturnsTrue() + { + // Arrange: Two null references + SpdxChecksum? c1 = null; + SpdxChecksum? c2 = null; + + // Act: Compare null with null + var result = SpdxChecksum.Same.Equals(c1!, c2!); + + // Assert: Verify null-null comparison returns true + Assert.True(result); + } + + /// + /// Tests that returns Missing for an empty string. + /// + /// + /// Passes an empty string to confirm that + /// returns rather than throwing, treating + /// the empty string as the absent-algorithm sentinel. + /// + [Fact] + public void SpdxChecksumAlgorithmExtensions_FromText_EmptyString_ReturnsMissing() + { + // Arrange: An empty string + var input = ""; + + // Act: Convert the empty string to an algorithm + var result = SpdxChecksumAlgorithmExtensions.FromText(input); + + // Assert: Verify empty string maps to Missing + Assert.Equal(SpdxChecksumAlgorithm.Missing, result); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxCreationInformationTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxCreationInformationTests.cs index f18533f..a60eb1f 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxCreationInformationTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxCreationInformationTests.cs @@ -23,14 +23,24 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Unit tests for . Each test is self-contained +/// with no shared state and no external dependencies. Tests cover deep copy, enhance, +/// validate, and edge-case behaviors. +/// public class SpdxCreationInformationTests { /// /// Tests the method successfully creates a deep copy /// - [TestMethod] - public void SpdxCreationInformation_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Exercises the deep-copy path with all four fields populated (two or more creators, + /// a created timestamp, a comment, and a license-list version). Verifying that both + /// the top-level reference and the Creators array reference are distinct confirms that + /// no shallow-copy aliasing occurs. + /// + [Fact] + public void SpdxCreationInformation_DeepCopy_WithAllFieldsPopulated_CreatesEqualButDistinctInstance() { // Arrange: Create an instance of SpdxCreationInformation with multiple creators var c1 = new SpdxCreationInformation @@ -45,21 +55,26 @@ public void SpdxCreationInformation_DeepCopy_CreatesEqualButDistinctInstance() var c2 = c1.DeepCopy(); // Assert: Verify deep-copy is equal to original - CollectionAssert.AreEqual(c1.Creators, c2.Creators); - Assert.AreEqual(c1.Created, c2.Created); - Assert.AreEqual(c1.Comment, c2.Comment); - Assert.AreEqual(c1.LicenseListVersion, c2.LicenseListVersion); + Assert.Equal(c1.Creators, c2.Creators); + Assert.Equal(c1.Created, c2.Created); + Assert.Equal(c1.Comment, c2.Comment); + Assert.Equal(c1.LicenseListVersion, c2.LicenseListVersion); // Assert: Verify deep-copy has distinct instances - Assert.IsFalse(ReferenceEquals(c1, c2)); - Assert.IsFalse(ReferenceEquals(c1.Creators, c2.Creators)); + Assert.False(ReferenceEquals(c1, c2)); + Assert.False(ReferenceEquals(c1.Creators, c2.Creators)); } /// /// Tests the method adds or updates information correctly /// - [TestMethod] - public void SpdxCreationInformation_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Exercises the enhance path where the base instance is missing a creator and the + /// LicenseListVersion field. The source instance provides both, allowing the test to + /// confirm additive merging of creators and fill-if-absent semantics for scalar fields. + /// + [Fact] + public void SpdxCreationInformation_Enhance_WithMissingFieldsInBase_AddsOrUpdatesInformationCorrectly() { // Arrange: Create an instance of SpdxCreationInformation with initial values var info = new SpdxCreationInformation @@ -78,20 +93,25 @@ public void SpdxCreationInformation_Enhance_AddsOrUpdatesInformationCorrectly() }); // Assert: Verify the enhanced information - Assert.HasCount(3, info.Creators); - Assert.AreEqual("Tool: LicenseFind-1.0", info.Creators[0]); - Assert.AreEqual("Organization: ExampleCodeInspect ()", info.Creators[1]); - Assert.AreEqual("Person: Jane Doe ()", info.Creators[2]); - Assert.AreEqual("2010-01-29T18:30:22Z", info.Created); - Assert.AreEqual("This package has been shipped in source and binary form.", info.Comment); - Assert.AreEqual("3.9", info.LicenseListVersion); + Assert.Equal(3, info.Creators.Length); + Assert.Equal("Tool: LicenseFind-1.0", info.Creators[0]); + Assert.Equal("Organization: ExampleCodeInspect ()", info.Creators[1]); + Assert.Equal("Person: Jane Doe ()", info.Creators[2]); + Assert.Equal("2010-01-29T18:30:22Z", info.Created); + Assert.Equal("This package has been shipped in source and binary form.", info.Comment); + Assert.Equal("3.9", info.LicenseListVersion); } /// /// Tests the method reports missing creators. /// - [TestMethod] - public void SpdxCreationInformation_Validate_MissingCreators() + /// + /// Boundary test: an empty Creators array is the minimal invalid state. Chosen to + /// confirm that the absence of any creator entry is caught independently of other + /// field values. + /// + [Fact] + public void SpdxCreationInformation_Validate_MissingCreators_ReportsIssue() { // Arrange: Create creation information with empty creators array var info = new SpdxCreationInformation @@ -106,14 +126,19 @@ public void SpdxCreationInformation_Validate_MissingCreators() info.Validate(issues); // Assert: Verify that the validation reports the missing creators - Assert.Contains(issue => issue.Contains("Document Invalid Creator Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Document Invalid Creator Field - Empty")); } /// /// Tests the method reports invalid creators. /// - [TestMethod] - public void SpdxCreationInformation_Validate_InvalidCreator() + /// + /// Exercises the per-entry validation rule that each creator must start with + /// Person:, Organization:, or Tool:. The input "BadCreator" + /// fails all three prefixes, making the expected issue deterministic. + /// + [Fact] + public void SpdxCreationInformation_Validate_InvalidCreator_ReportsIssue() { // Arrange: Create creation information with invalid creator format var info = new SpdxCreationInformation @@ -128,14 +153,19 @@ public void SpdxCreationInformation_Validate_InvalidCreator() info.Validate(issues); // Assert: Verify that the validation reports the invalid creator - Assert.Contains(issue => issue.Contains("Document Invalid Creator Entry 'BadCreator'"), issues); + Assert.Contains(issues, issue => issue.Contains("Document Invalid Creator Entry 'BadCreator'")); } /// /// Tests the method reports invalid created dates. /// - [TestMethod] - public void SpdxCreationInformation_Validate_InvalidCreatedDate() + /// + /// Exercises the Created field validation rule. The value "BadDate" is + /// chosen because it is unambiguously non-empty and non-conforming, confirming that + /// the regex/helper rejects it without false negatives. + /// + [Fact] + public void SpdxCreationInformation_Validate_InvalidCreatedDate_ReportsIssue() { // Arrange: Create creation information with invalid created date var info = new SpdxCreationInformation @@ -150,20 +180,25 @@ public void SpdxCreationInformation_Validate_InvalidCreatedDate() info.Validate(issues); // Assert: Verify that the validation reports the invalid created date - Assert.Contains(issue => issue.Contains("Document Invalid Created Field 'BadDate'"), issues); + Assert.Contains(issues, issue => issue.Contains("Document Invalid Created Field 'BadDate'")); } /// /// Tests the method reports invalid versions. /// - [TestMethod] - public void SpdxCreationInformation_Validate_InvalidVersion() + /// + /// Exercises the LicenseListVersion field validation rule. The value + /// "BadVersion" does not match the \d+\.\d+ pattern and confirms + /// that the regex rejects non-numeric version strings. + /// + [Fact] + public void SpdxCreationInformation_Validate_InvalidVersion_ReportsIssue() { // Arrange: Create creation information with invalid license list version var info = new SpdxCreationInformation { Creators = ["Tool: LicenseFind-1.0", "Organization: ExampleCodeInspect ()"], - Created = "BadDate", + Created = "2021-01-01T00:00:00Z", Comment = "This package has been shipped in source and binary form.", LicenseListVersion = "BadVersion" }; @@ -173,6 +208,92 @@ public void SpdxCreationInformation_Validate_InvalidVersion() info.Validate(issues); // Assert: Verify that the validation reports the invalid license list version - Assert.Contains(issue => issue.Contains("Document Invalid License List Version Field 'BadVersion'"), issues); + Assert.Contains(issues, issue => issue.Contains("Document Invalid License List Version Field 'BadVersion'")); + } + + /// + /// Tests the method reports no issues for valid information. + /// + /// + /// Happy-path test using a fully-populated valid instance. Confirms that no + /// spurious validation issues are reported when all fields satisfy their + /// respective rules. + /// + [Fact] + public void SpdxCreationInformation_Validate_ValidInformation_NoIssues() + { + // Arrange: Create valid creation information + var info = new SpdxCreationInformation + { + Creators = ["Tool: LicenseFind-1.0", "Organization: ExampleCodeInspect ()"], + Created = "2010-01-29T18:30:22Z", + Comment = "This package has been shipped in source and binary form.", + LicenseListVersion = "3.9" + }; + + // Act: Perform validation on the SpdxCreationInformation instance + var issues = new List(); + info.Validate(issues); + + // Assert: Verify that no issues are reported + Assert.Empty(issues); + } + + /// + /// Tests the method accepts an empty Created field. + /// + /// + /// Boundary test: an empty Created field is permitted for partially-constructed + /// documents. Confirms that the validator does not report a date-format issue when + /// the field is intentionally left blank. + /// + [Fact] + public void SpdxCreationInformation_Validate_EmptyCreatedField_NoDateIssue() + { + // Arrange: Create creation information with an empty Created field + var info = new SpdxCreationInformation + { + Creators = ["Tool: LicenseFind-1.0"], + Created = "" + }; + + // Act: Perform validation + var issues = new List(); + info.Validate(issues); + + // Assert: Verify that no date-related issue is reported (empty Created is permitted) + Assert.DoesNotContain(issues, issue => issue.Contains("Invalid Created Field")); + } + + /// + /// Tests the method deduplicates creators. + /// + /// + /// Exercises the deduplication branch of the Enhance method. The base and source + /// instances share one common creator, allowing the test to confirm that the merged + /// Creators array contains exactly three distinct entries without duplicates. + /// + [Fact] + public void SpdxCreationInformation_Enhance_DuplicateCreators_DeduplicatesCreators() + { + // Arrange: Create creation information with an initial creator list that contains a duplicate + var info = new SpdxCreationInformation + { + Creators = ["Tool: LicenseFind-1.0", "Organization: ExampleCodeInspect ()"], + Created = "2010-01-29T18:30:22Z" + }; + + // Act: Enhance with an overlapping creator list + info.Enhance( + new SpdxCreationInformation + { + Creators = ["Tool: LicenseFind-1.0", "Person: Jane Doe ()"] + }); + + // Assert: Verify that duplicate creators are removed and unique entries are preserved + Assert.Equal(3, info.Creators.Length); + Assert.True(info.Creators.Contains("Tool: LicenseFind-1.0")); + Assert.True(info.Creators.Contains("Organization: ExampleCodeInspect ()")); + Assert.True(info.Creators.Contains("Person: Jane Doe ()")); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxDocumentTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxDocumentTests.cs index fbbbe8b..535ed91 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxDocumentTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxDocumentTests.cs @@ -25,14 +25,23 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Tests the class using xUnit v3. Each test constructs its own document state +/// from scratch or deserializes the embedded JSON fixture +/// SPDXJSONExample-v2.3.spdx.json; no shared instance state is used. +/// public class SpdxDocumentTests { /// /// Tests the method returns the correct root packages /// - [TestMethod] - public void SpdxDocument_GetRootPackages_CorrectPackages() + /// + /// Constructs a document with three packages and two root-package indicators (one via the + /// Describes list and one via a DescribedBy relationship) to verify that GetRootPackages + /// returns exactly the two packages named as roots and excludes the third. + /// + [Fact] + public void SpdxDocument_GetRootPackages_WithDescribesAndRelationships_ReturnsCorrectPackages() { // Arrange: Create a sample SPDX document with multiple packages and relationships var document = new SpdxDocument @@ -77,16 +86,22 @@ public void SpdxDocument_GetRootPackages_CorrectPackages() var packages = document.GetRootPackages(); // Assert: Verify the correct root packages are returned - Assert.HasCount(2, packages); - Assert.IsTrue(Array.Exists(packages, p => p.Id == "SPDXRef-Package1")); - Assert.IsTrue(Array.Exists(packages, p => p.Id == "SPDXRef-Package2")); + Assert.Equal(2, packages.Length); + Assert.True(Array.Exists(packages, p => p.Id == "SPDXRef-Package1")); + Assert.True(Array.Exists(packages, p => p.Id == "SPDXRef-Package2")); } /// /// Tests the comparer compares documents correctly. /// - [TestMethod] - public void SpdxDocument_SameComparer_ComparesCorrectly() + /// + /// Sets up three documents: d1 and d2 both describe a root package with the same name + /// and version (but different IDs and versions), and d3 describes a different package. + /// Verifies reflexive, symmetric, cross-inequality comparisons, and hash-code consistency + /// for equal documents. + /// + [Fact] + public void SpdxDocument_Same_DocumentsWithMatchingRootPackages_AreEqual() { // Arrange: Create three documents with different properties var d1 = new SpdxDocument @@ -143,28 +158,34 @@ public void SpdxDocument_SameComparer_ComparesCorrectly() ] }; - // Assert: Verify documents compare to themselves - Assert.IsTrue(SpdxDocument.Same.Equals(d1, d1)); - Assert.IsTrue(SpdxDocument.Same.Equals(d2, d2)); - Assert.IsTrue(SpdxDocument.Same.Equals(d3, d3)); + // Act / Assert: Verify documents compare to themselves + Assert.True(SpdxDocument.Same.Equals(d1, d1)); + Assert.True(SpdxDocument.Same.Equals(d2, d2)); + Assert.True(SpdxDocument.Same.Equals(d3, d3)); - // Assert: Verify documents compare correctly - Assert.IsTrue(SpdxDocument.Same.Equals(d1, d2)); - Assert.IsTrue(SpdxDocument.Same.Equals(d2, d1)); - Assert.IsFalse(SpdxDocument.Same.Equals(d1, d3)); - Assert.IsFalse(SpdxDocument.Same.Equals(d3, d1)); - Assert.IsFalse(SpdxDocument.Same.Equals(d2, d3)); - Assert.IsFalse(SpdxDocument.Same.Equals(d3, d2)); + // Act / Assert: Verify documents compare correctly + Assert.True(SpdxDocument.Same.Equals(d1, d2)); + Assert.True(SpdxDocument.Same.Equals(d2, d1)); + Assert.False(SpdxDocument.Same.Equals(d1, d3)); + Assert.False(SpdxDocument.Same.Equals(d3, d1)); + Assert.False(SpdxDocument.Same.Equals(d2, d3)); + Assert.False(SpdxDocument.Same.Equals(d3, d2)); // Assert: Verify same documents have identical hashes - Assert.AreEqual(SpdxDocument.Same.GetHashCode(d1), SpdxDocument.Same.GetHashCode(d2)); + Assert.Equal(SpdxDocument.Same.GetHashCode(d1), SpdxDocument.Same.GetHashCode(d2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxDocument_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Constructs a fully-populated document with external document references, extracted + /// licensing info, annotations, files, packages, snippets, relationships, and a Describes + /// list, then deep-copies it. Verifies that every collection and scalar field is equal + /// but that all array references are distinct from the original. + /// + [Fact] + public void SpdxDocument_DeepCopy_WithPopulatedDocument_CreatesEqualButDistinctInstance() { // Arrange: Create a sample SPDX document with various elements var d1 = new SpdxDocument @@ -255,65 +276,74 @@ public void SpdxDocument_DeepCopy_CreatesEqualButDistinctInstance() var d2 = d1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(d1, d2, SpdxDocument.Same); - Assert.AreEqual(d1.Id, d2.Id); - Assert.AreEqual(d1.Version, d2.Version); - Assert.AreEqual(d1.Name, d2.Name); - CollectionAssert.AreEquivalent(d1.ExternalDocumentReferences, d2.ExternalDocumentReferences, + Assert.Equal(d1, d2, SpdxDocument.Same); + Assert.Equal(d1.Id, d2.Id); + Assert.Equal(d1.Version, d2.Version); + Assert.Equal(d1.Name, d2.Name); + SpdxTestHelpers.AssertEquivalent(d1.ExternalDocumentReferences, d2.ExternalDocumentReferences, SpdxExternalDocumentReference.Same); - CollectionAssert.AreEquivalent(d1.ExtractedLicensingInfo, d2.ExtractedLicensingInfo, + SpdxTestHelpers.AssertEquivalent(d1.ExtractedLicensingInfo, d2.ExtractedLicensingInfo, SpdxExtractedLicensingInfo.Same); - CollectionAssert.AreEquivalent(d1.Annotations, d2.Annotations, SpdxAnnotation.Same); - CollectionAssert.AreEquivalent(d1.Files, d2.Files, SpdxFile.Same); - CollectionAssert.AreEquivalent(d1.Packages, d2.Packages, SpdxPackage.Same); - CollectionAssert.AreEquivalent(d1.Snippets, d2.Snippets, SpdxSnippet.Same); - CollectionAssert.AreEquivalent(d1.Relationships, d2.Relationships, SpdxRelationship.Same); - CollectionAssert.AreEqual(d1.Describes, d2.Describes); + SpdxTestHelpers.AssertEquivalent(d1.Annotations, d2.Annotations, SpdxAnnotation.Same); + SpdxTestHelpers.AssertEquivalent(d1.Files, d2.Files, SpdxFile.Same); + SpdxTestHelpers.AssertEquivalent(d1.Packages, d2.Packages, SpdxPackage.Same); + SpdxTestHelpers.AssertEquivalent(d1.Snippets, d2.Snippets, SpdxSnippet.Same); + SpdxTestHelpers.AssertEquivalent(d1.Relationships, d2.Relationships, SpdxRelationship.Same); + Assert.Equal(d1.Describes, d2.Describes); // Assert: Verify deep-copy has distinct instances - Assert.IsFalse(ReferenceEquals(d1, d2)); - Assert.IsFalse(ReferenceEquals(d1.ExternalDocumentReferences, d2.ExternalDocumentReferences)); - Assert.IsFalse(ReferenceEquals(d1.ExtractedLicensingInfo, d2.ExtractedLicensingInfo)); - Assert.IsFalse(ReferenceEquals(d1.Annotations, d2.Annotations)); - Assert.IsFalse(ReferenceEquals(d1.Files, d2.Files)); - Assert.IsFalse(ReferenceEquals(d1.Packages, d2.Packages)); - Assert.IsFalse(ReferenceEquals(d1.Snippets, d2.Snippets)); - Assert.IsFalse(ReferenceEquals(d1.Relationships, d2.Relationships)); - Assert.IsFalse(ReferenceEquals(d1.Describes, d2.Describes)); + Assert.False(ReferenceEquals(d1, d2)); + Assert.False(ReferenceEquals(d1.ExternalDocumentReferences, d2.ExternalDocumentReferences)); + Assert.False(ReferenceEquals(d1.ExtractedLicensingInfo, d2.ExtractedLicensingInfo)); + Assert.False(ReferenceEquals(d1.Annotations, d2.Annotations)); + Assert.False(ReferenceEquals(d1.Files, d2.Files)); + Assert.False(ReferenceEquals(d1.Packages, d2.Packages)); + Assert.False(ReferenceEquals(d1.Snippets, d2.Snippets)); + Assert.False(ReferenceEquals(d1.Relationships, d2.Relationships)); + Assert.False(ReferenceEquals(d1.Describes, d2.Describes)); } /// /// Tests the method successfully validates a document with no issues. /// - [TestMethod] - public void SpdxDocument_Validate_NoIssues() + /// + /// Deserializes SPDXJSONExample-v2.3.spdx.json (a known-good SPDX 2.3 example) + /// to obtain a fully valid document and verifies that the validator reports no issues. + /// Using the embedded JSON fixture ensures the document satisfies all field constraints. + /// + [Fact] + public void SpdxDocument_Validate_ValidDocument_ReportsNoIssues() { + // Arrange: Load a valid SPDX JSON document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); - - // Deserialize the document var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); - // Perform validation + // Act: Perform validation on the document var issues = new List(); doc.Validate(issues); - // Ensure no validation issues - Assert.IsEmpty(issues); + // Assert: Verify no validation issues are reported + Assert.Empty(issues); } /// /// Tests the method reports invalid IDs. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidId() + /// + /// Deserializes the embedded JSON fixture to obtain a valid baseline document, then + /// overwrites its SPDX-ID with "BadId". Verifies that the validator reports + /// the expected diagnostic for a malformed SPDX identifier. + /// + [Fact] + public void SpdxDocument_Validate_InvalidId_ReportsIssue() { // Arrange: Load and deserialize a valid SPDX document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Arrange: Corrupt the document with invalid ID doc.Id = "BadId"; @@ -329,14 +359,19 @@ public void SpdxDocument_Validate_InvalidId() /// /// Tests the method reports invalid names. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidName() + /// + /// Deserializes the embedded JSON fixture to obtain a valid baseline document, then + /// clears its name field. Verifies that the validator reports the expected diagnostic + /// for an empty document name. + /// + [Fact] + public void SpdxDocument_Validate_InvalidName_ReportsIssue() { // Arrange: Load and deserialize a valid SPDX document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Arrange: Corrupt the document with empty name doc.Name = ""; @@ -352,14 +387,19 @@ public void SpdxDocument_Validate_InvalidName() /// /// Tests the method reports invalid versions. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidVersion() + /// + /// Deserializes the embedded JSON fixture to obtain a valid baseline document, then + /// overwrites the version with "BadVersion". Verifies that the validator reports + /// the expected diagnostic for a version string that does not match the SPDX-2.x pattern. + /// + [Fact] + public void SpdxDocument_Validate_InvalidVersion_ReportsIssue() { // Arrange: Load and deserialize a valid SPDX document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Arrange: Corrupt the document with invalid version doc.Version = "BadVersion"; @@ -375,14 +415,19 @@ public void SpdxDocument_Validate_InvalidVersion() /// /// Tests the method reports invalid data licenses. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidDataLicense() + /// + /// Deserializes the embedded JSON fixture to obtain a valid baseline document, then + /// overwrites the data license with "BadLicense". Verifies that the validator + /// reports the expected diagnostic for a value other than the mandatory CC0-1.0 license. + /// + [Fact] + public void SpdxDocument_Validate_InvalidDataLicense_ReportsIssue() { // Arrange: Load and deserialize a valid SPDX document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Arrange: Corrupt the document with invalid data license doc.DataLicense = "BadLicense"; @@ -398,14 +443,19 @@ public void SpdxDocument_Validate_InvalidDataLicense() /// /// Tests the method reports invalid namespaces. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidNameSpace() + /// + /// Deserializes the embedded JSON fixture to obtain a valid baseline document, then + /// clears the namespace URI. Verifies that the validator reports the expected diagnostic + /// for an empty document namespace. + /// + [Fact] + public void SpdxDocument_Validate_InvalidNameSpace_ReportsIssue() { // Arrange: Load and deserialize a valid SPDX document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Arrange: Corrupt the document with empty namespace doc.DocumentNamespace = ""; @@ -421,8 +471,13 @@ public void SpdxDocument_Validate_InvalidNameSpace() /// /// Tests the method detects duplicate IDs. /// - [TestMethod] - public void SpdxDocument_Validate_DuplicatePackageIds() + /// + /// Constructs a minimal document containing two packages with the same SPDX-ID. Verifies + /// that the validator reports the expected duplicate-ID diagnostic rather than silently + /// accepting the malformed document. + /// + [Fact] + public void SpdxDocument_Validate_DuplicatePackageIds_ReportsIssue() { // Arrange: Create a sample SPDX document with duplicate package IDs var doc = new SpdxDocument @@ -464,8 +519,13 @@ public void SpdxDocument_Validate_DuplicatePackageIds() /// /// Tests the method detects bad relationships. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidRelationship() + /// + /// Constructs a minimal document containing a relationship whose + /// RelatedSpdxElement references an ID that does not exist in the document. + /// Verifies that the validator reports the expected dangling-reference diagnostic. + /// + [Fact] + public void SpdxDocument_Validate_InvalidRelationship_ReportsIssue() { // Arrange: Create a sample SPDX document with a relationship to a non-existent package var doc = new SpdxDocument @@ -503,14 +563,19 @@ public void SpdxDocument_Validate_InvalidRelationship() /// /// Tests the method detects NTIA issues. /// - [TestMethod] - public void SpdxDocument_Validate_NtiaIssues() + /// + /// Deserializes the embedded JSON fixture, which deliberately omits required NTIA fields + /// for some packages (supplier and version for Apache Commons Lang; supplier for Jena and + /// Saxon). Verifies that the NTIA validation mode reports exactly those expected issues. + /// + [Fact] + public void SpdxDocument_Validate_NtiaMinimumElements_ReportsIssues() { // Arrange: Load a sample SPDX JSON document with known NTIA issues var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Act: Perform validation var issues = new List(); @@ -526,76 +591,153 @@ public void SpdxDocument_Validate_NtiaIssues() /// /// Tests the method returns all elements in the document. /// - [TestMethod] - public void SpdxDocument_GetAllElements_Correct() + /// + /// Deserializes the embedded JSON fixture, which contains a document element, four + /// packages, and five files. Verifies that + /// returns exactly those elements and that entries are + /// excluded from the result. + /// + [Fact] + public void SpdxDocument_GetAllElements_WithMixedElements_ReturnsAllNonRelationshipElements() { // Arrange: Load a sample SPDX JSON document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); // Act: Get all elements var elements = doc.GetAllElements().ToList(); // Assert: Verify the document is in the list - Assert.AreEqual(1, elements.OfType().Count()); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-DOCUMENT")); + Assert.Single(elements.OfType()); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-DOCUMENT")); // Assert: Verify all packages are in the list - Assert.AreEqual(4, elements.OfType().Count()); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-Package")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-fromDoap-1")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-fromDoap-0")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-Saxon")); + Assert.Equal(4, elements.OfType().Count()); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-Package")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-fromDoap-1")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-fromDoap-0")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-Saxon")); // Assert: Verify all files are in the list - Assert.AreEqual(5, elements.OfType().Count()); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-DoapSource")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-CommonsLangSrc")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-JenaLib")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-Specification")); - Assert.IsNotNull(elements.Find(e => e.Id == "SPDXRef-File")); + Assert.Equal(5, elements.OfType().Count()); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-DoapSource")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-CommonsLangSrc")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-JenaLib")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-Specification")); + Assert.NotNull(elements.Find(e => e.Id == "SPDXRef-File")); } /// - /// Tests the method returns the correct element by ID. + /// Tests the method returns the document element. /// - [TestMethod] - public void SpdxDocument_GetElement_Correct() + /// + /// Deserializes the embedded JSON fixture and queries for the document element by its + /// SPDX-ID. Verifies that the returned object is the document itself and has the expected + /// document name. + /// + [Fact] + public void SpdxDocument_GetElement_Document_ReturnsDocumentElement() { // Arrange: Load a sample SPDX JSON document var json22Example = SpdxTestHelpers.GetEmbeddedResource( "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); var doc = Spdx2JsonDeserializer.Deserialize(json22Example); - Assert.IsNotNull(doc); + Assert.NotNull(doc); - // Assert: Verify finding the document returns the correct element + // Act: Find the document element by ID var foundDoc = doc.GetElement("SPDXRef-DOCUMENT"); - Assert.IsNotNull(foundDoc); - Assert.AreEqual("SPDX-Tools-v2.0", foundDoc.Name); - // Assert: Verify finding a file returns the correct element + // Assert: Verify the document element is correct + Assert.NotNull(foundDoc); + Assert.Equal("SPDX-Tools-v2.0", foundDoc.Name); + } + + /// + /// Tests the method returns the correct file element. + /// + /// + /// Deserializes the embedded JSON fixture and queries for a file element by its SPDX-ID. + /// Verifies that the returned object is the correct file and has the expected file name. + /// + [Fact] + public void SpdxDocument_GetElement_File_ReturnsFileElement() + { + // Arrange: Load a sample SPDX JSON document + var json22Example = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var doc = Spdx2JsonDeserializer.Deserialize(json22Example); + Assert.NotNull(doc); + + // Act: Find a file element by ID var foundFile = doc.GetElement("SPDXRef-JenaLib"); - Assert.IsNotNull(foundFile); - Assert.AreEqual("./lib-source/jena-2.6.3-sources.jar", foundFile.FileName); - // Assert: Verify finding a package returns the correct element + // Assert: Verify the file element is correct + Assert.NotNull(foundFile); + Assert.Equal("./lib-source/jena-2.6.3-sources.jar", foundFile.FileName); + } + + /// + /// Tests the method returns the correct package element. + /// + /// + /// Deserializes the embedded JSON fixture and queries for a package element by its + /// SPDX-ID. Verifies that the returned object is the correct package and has the + /// expected file name property. + /// + [Fact] + public void SpdxDocument_GetElement_Package_ReturnsPackageElement() + { + // Arrange: Load a sample SPDX JSON document + var json22Example = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var doc = Spdx2JsonDeserializer.Deserialize(json22Example); + Assert.NotNull(doc); + + // Act: Find a package element by ID var foundPackage = doc.GetElement("SPDXRef-Saxon"); - Assert.IsNotNull(foundPackage); - Assert.AreEqual("saxonB-8.8.zip", foundPackage.FileName); - // Assert: Verify finding a snippet returns the correct element + // Assert: Verify the package element is correct + Assert.NotNull(foundPackage); + Assert.Equal("saxonB-8.8.zip", foundPackage.FileName); + } + + /// + /// Tests the method returns the correct snippet element. + /// + /// + /// Deserializes the embedded JSON fixture and queries for a snippet element by its + /// SPDX-ID. Verifies that the returned object is the correct snippet and references + /// the expected source file SPDX-ID. + /// + [Fact] + public void SpdxDocument_GetElement_Snippet_ReturnsSnippetElement() + { + // Arrange: Load a sample SPDX JSON document + var json22Example = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var doc = Spdx2JsonDeserializer.Deserialize(json22Example); + Assert.NotNull(doc); + + // Act: Find a snippet element by ID var foundSnippet = doc.GetElement("SPDXRef-Snippet"); - Assert.IsNotNull(foundSnippet); - Assert.AreEqual("SPDXRef-DoapSource", foundSnippet.SnippetFromFile); + + // Assert: Verify the snippet element is correct + Assert.NotNull(foundSnippet); + Assert.Equal("SPDXRef-DoapSource", foundSnippet.SnippetFromFile); } /// /// Tests the method validates document-level annotations. /// - [TestMethod] - public void SpdxDocument_Validate_InvalidAnnotation() + /// + /// Constructs a minimal valid document that contains a single annotation with an empty + /// Annotator field. Verifies that the validator reports the annotation-level issue using + /// the document element prefix so the issue can be attributed to the correct context. + /// + [Fact] + public void SpdxDocument_Validate_InvalidAnnotation_ReportsIssue() { // Arrange: Create a document with an invalid annotation var doc = new SpdxDocument diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxElementTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxElementTests.cs new file mode 100644 index 0000000..7017fe0 --- /dev/null +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxElementTests.cs @@ -0,0 +1,77 @@ +// Copyright(c) 2024 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. + +namespace DemaConsulting.SpdxModel.Tests; + +/// +/// Tests for the base class identity behavior. +/// +/// +/// is abstract; is used as the +/// concrete subclass to exercise base-class identity behavior, as its Validate method +/// uses the standard SpdxRefRegex check. +/// +public class SpdxElementTests +{ + /// + /// Tests that an element with a valid SPDXRef-<name> identifier passes identity validation. + /// + /// + /// Uses SPDXRef-valid as the identifier because it is a minimal, well-formed + /// value that satisfies the SPDXRef- prefix pattern and contains only allowed characters, + /// making it the simplest positive example to confirm the happy-path acceptance. + /// + [Fact] + public void SpdxElement_Id_ValidFormat_PassesValidation() + { + // Arrange: Create a minimal package element with a valid SPDXRef- ID + var element = new SpdxPackage { Id = "SPDXRef-valid", Name = "test-package", Version = "1.0" }; + + // Act: Validate the element + var issues = new List(); + element.Validate(issues, null); + + // Assert: No issue about the SPDX Identifier field + Assert.DoesNotContain(issues, i => i.Contains("Invalid SPDX Identifier")); + } + + /// + /// Tests that an element with an ID that does not follow the SPDXRef-<name> format + /// reports an identity validation issue. + /// + /// + /// Uses BadId as the identifier because it is a concise, obviously invalid value + /// that omits the required SPDXRef- prefix entirely, making the expected failure + /// unambiguous and the diagnostic message easy to verify. + /// + [Fact] + public void SpdxElement_Id_InvalidFormat_ReportsValidationIssue() + { + // Arrange: Create a minimal package element with a bad ID + var element = new SpdxPackage { Id = "BadId", Name = "test-package", Version = "1.0" }; + + // Act: Validate the element + var issues = new List(); + element.Validate(issues, null); + + // Assert: The invalid identifier is reported + Assert.Contains(issues, i => i.Contains("Invalid SPDX Identifier Field 'BadId'")); + } +} diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxExternalDocumentReferenceTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxExternalDocumentReferenceTests.cs index 8feb76c..2cbc35d 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxExternalDocumentReferenceTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxExternalDocumentReferenceTests.cs @@ -23,15 +23,25 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Tests the class using xUnit v3. Each test method constructs +/// its own instance state with no shared fixture, covering the Same comparer, DeepCopy, +/// Enhance, and Validate methods. +/// public class SpdxExternalDocumentReferenceTests { /// /// Tests the comparer compares external document references /// correctly. /// - [TestMethod] - public void SpdxExternalDocumentReference_SameComparer_ComparesCorrectly() + /// + /// Constructs three references: r1 and r2 share the same Document URI (making them + /// equal under the Same comparer) while r3 has a different URI. Verifies reflexive, + /// symmetric, and cross-inequality comparisons, and that equal references produce + /// identical hash codes. + /// + [Fact] + public void SpdxExternalDocumentReference_SameComparer_SameDocument_ReturnsEqual() { // Arrange: Create three external document references with different properties var r1 = new SpdxExternalDocumentReference @@ -65,29 +75,34 @@ public void SpdxExternalDocumentReference_SameComparer_ComparesCorrectly() Document = "http://demo.com/some-document" }; - // Assert: Verify external-document-references compare to themselves - Assert.IsTrue(SpdxExternalDocumentReference.Same.Equals(r1, r1)); - Assert.IsTrue(SpdxExternalDocumentReference.Same.Equals(r2, r2)); - Assert.IsTrue(SpdxExternalDocumentReference.Same.Equals(r3, r3)); + // Act / Assert: Verify external-document-references compare to themselves + Assert.True(SpdxExternalDocumentReference.Same.Equals(r1, r1)); + Assert.True(SpdxExternalDocumentReference.Same.Equals(r2, r2)); + Assert.True(SpdxExternalDocumentReference.Same.Equals(r3, r3)); // Assert: Verify external-document-references compare correctly - Assert.IsTrue(SpdxExternalDocumentReference.Same.Equals(r1, r2)); - Assert.IsTrue(SpdxExternalDocumentReference.Same.Equals(r2, r1)); - Assert.IsFalse(SpdxExternalDocumentReference.Same.Equals(r1, r3)); - Assert.IsFalse(SpdxExternalDocumentReference.Same.Equals(r3, r1)); - Assert.IsFalse(SpdxExternalDocumentReference.Same.Equals(r2, r3)); - Assert.IsFalse(SpdxExternalDocumentReference.Same.Equals(r3, r2)); + Assert.True(SpdxExternalDocumentReference.Same.Equals(r1, r2)); + Assert.True(SpdxExternalDocumentReference.Same.Equals(r2, r1)); + Assert.False(SpdxExternalDocumentReference.Same.Equals(r1, r3)); + Assert.False(SpdxExternalDocumentReference.Same.Equals(r3, r1)); + Assert.False(SpdxExternalDocumentReference.Same.Equals(r2, r3)); + Assert.False(SpdxExternalDocumentReference.Same.Equals(r3, r2)); // Assert: Verify same external-document-references have identical hashes - Assert.AreEqual(SpdxExternalDocumentReference.Same.GetHashCode(r1), + Assert.Equal(SpdxExternalDocumentReference.Same.GetHashCode(r1), SpdxExternalDocumentReference.Same.GetHashCode(r2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxExternalDocumentReference_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Creates a fully-populated external document reference with a checksum and deep-copies + /// it. Verifies that the copy has equal field values but that both the top-level reference + /// and the nested Checksum are distinct object references from the original. + /// + [Fact] + public void SpdxExternalDocumentReference_DeepCopy_ValidInstance_ReturnsEqualButDistinctInstance() { // Arrange: Create an external document reference with a checksum var r1 = new SpdxExternalDocumentReference @@ -105,14 +120,14 @@ public void SpdxExternalDocumentReference_DeepCopy_CreatesEqualButDistinctInstan var r2 = r1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(r1, r2, SpdxExternalDocumentReference.Same); - Assert.AreEqual(r1.ExternalDocumentId, r2.ExternalDocumentId); - Assert.AreEqual(r1.Checksum, r2.Checksum, SpdxChecksum.Same); - Assert.AreEqual(r1.Document, r2.Document); + Assert.Equal(r1, r2, SpdxExternalDocumentReference.Same); + Assert.Equal(r1.ExternalDocumentId, r2.ExternalDocumentId); + Assert.Equal(r1.Checksum, r2.Checksum, SpdxChecksum.Same); + Assert.Equal(r1.Document, r2.Document); // Assert: Verify deep-copy has distinct instances - Assert.IsFalse(ReferenceEquals(r1, r2)); - Assert.IsFalse(ReferenceEquals(r1.Checksum, r2.Checksum)); + Assert.False(ReferenceEquals(r1, r2)); + Assert.False(ReferenceEquals(r1.Checksum, r2.Checksum)); } /// @@ -120,8 +135,14 @@ public void SpdxExternalDocumentReference_DeepCopy_CreatesEqualButDistinctInstan /// /// method adds or updates information correctly. /// - [TestMethod] - public void SpdxExternalDocumentReference_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Starts with a single reference that lacks a checksum, then enhances with a list + /// containing one entry that updates the existing reference's checksum and one entirely + /// new reference. Verifies that the merged array has exactly two entries with the correct + /// checksum data and new reference details. + /// + [Fact] + public void SpdxExternalDocumentReference_Enhance_WithNewAndMatchingEntries_MergesAndAppendsCorrectly() { // Arrange: Create an array of external document references var references = new[] @@ -159,23 +180,28 @@ public void SpdxExternalDocumentReference_Enhance_AddsOrUpdatesInformationCorrec ]); // Assert: Verify the references array has correct information - Assert.HasCount(2, references); - Assert.AreEqual("DocumentRef-spdx-tool-1.2", references[0].ExternalDocumentId); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, references[0].Checksum.Algorithm); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2759", references[0].Checksum.Value); - Assert.AreEqual("http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301", + Assert.Equal(2, references.Length); + Assert.Equal("DocumentRef-spdx-tool-1.2", references[0].ExternalDocumentId); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, references[0].Checksum.Algorithm); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2759", references[0].Checksum.Value); + Assert.Equal("http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301", references[0].Document); - Assert.AreEqual("DocumentRef-OtherDoc", references[1].ExternalDocumentId); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, references[1].Checksum.Algorithm); - Assert.AreEqual("c2b4e1c67a2d28fced849ee1bb76e7391b93f125", references[1].Checksum.Value); - Assert.AreEqual("http://demo.com/some-document", references[1].Document); + Assert.Equal("DocumentRef-OtherDoc", references[1].ExternalDocumentId); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, references[1].Checksum.Algorithm); + Assert.Equal("c2b4e1c67a2d28fced849ee1bb76e7391b93f125", references[1].Checksum.Value); + Assert.Equal("http://demo.com/some-document", references[1].Document); } /// /// Tests the method reports missing external document ID /// - [TestMethod] - public void SpdxExternalDocumentReference_Validate_MissingId() + /// + /// Sets ExternalDocumentId to an empty string — the minimal invalid state — to confirm + /// that the validator catches the absent ID and includes the expected description string + /// in the reported issue. + /// + [Fact] + public void SpdxExternalDocumentReference_Validate_MissingId_ReportsIssue() { // Arrange: Create a bad reference var reference = new SpdxExternalDocumentReference @@ -189,14 +215,19 @@ public void SpdxExternalDocumentReference_Validate_MissingId() reference.Validate(issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("External Document Reference Invalid External Document ID Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("External Document Reference Invalid External Document ID Field - Empty")); } /// /// Tests the method reports missing document URI /// - [TestMethod] - public void SpdxExternalDocumentReference_Validate_MissingDocument() + /// + /// Sets the Document URI to an empty string with a valid ExternalDocumentId to confirm + /// that the validator catches the absent URI and includes the expected description string + /// (including the reference ID) in the reported issue. + /// + [Fact] + public void SpdxExternalDocumentReference_Validate_MissingDocument_ReportsIssue() { // Arrange: Create a bad reference var reference = new SpdxExternalDocumentReference @@ -210,6 +241,39 @@ public void SpdxExternalDocumentReference_Validate_MissingDocument() reference.Validate(issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("External Document Reference 'DocumentRef-spdx-tool-1.2' Invalid SPDX Document URI Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("External Document Reference 'DocumentRef-spdx-tool-1.2' Invalid SPDX Document URI Field - Empty")); + } + + /// + /// Tests the method reports an invalid checksum. + /// + /// + /// Constructs a reference with a algorithm + /// and an empty checksum value to confirm that the validator delegates to the checksum + /// validator and surfaces the algorithm-missing diagnostic in the reported issues. + /// + [Fact] + public void SpdxExternalDocumentReference_Validate_InvalidChecksum_ReportsIssue() + { + // Arrange: Create a reference with a missing-algorithm checksum + var reference = new SpdxExternalDocumentReference + { + ExternalDocumentId = "DocumentRef-spdx-tool-1.2", + Checksum = new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.Missing, + Value = "" + }, + Document = "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301" + }; + + // Act: Perform validation + var issues = new List(); + reference.Validate(issues); + + // Assert: Verify that the checksum algorithm issue is reported + Assert.Contains( + issues, + issue => issue.Contains("Invalid Checksum Algorithm Field - Missing")); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxExternalReferenceTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxExternalReferenceTests.cs index ffc4351..374b176 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxExternalReferenceTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxExternalReferenceTests.cs @@ -23,14 +23,22 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers the Same equality comparer, DeepCopy, Enhance merge, Validate, and the +/// SpdxReferenceCategory text-conversion extension methods (FromText/ToText). +/// public class SpdxExternalReferenceTests { /// /// Tests the comparer compares external references correctly. /// - [TestMethod] - public void SpdxExternalReference_SameComparer_ComparesCorrectly() + /// + /// Verifies that two references with the same Category, Type, and Locator are equal + /// regardless of Comment differences, that differing field values produce inequality, + /// and that equal references produce identical hash codes. + /// + [Fact] + public void SpdxExternalReference_SameComparer_EqualAndUnequalInstances_ComparesCorrectly() { // Arrange: Create three external references with different properties var r1 = new SpdxExternalReference @@ -53,28 +61,32 @@ public void SpdxExternalReference_SameComparer_ComparesCorrectly() Locator = "pkg:nuget/SomePackage@0.0.0" }; - // Assert: Verify external-references compare to themselves - Assert.IsTrue(SpdxExternalReference.Same.Equals(r1, r1)); - Assert.IsTrue(SpdxExternalReference.Same.Equals(r2, r2)); - Assert.IsTrue(SpdxExternalReference.Same.Equals(r3, r3)); + // Act / Assert: Verify external-references compare to themselves + Assert.True(SpdxExternalReference.Same.Equals(r1, r1)); + Assert.True(SpdxExternalReference.Same.Equals(r2, r2)); + Assert.True(SpdxExternalReference.Same.Equals(r3, r3)); // Assert: Verify external-references compare correctly - Assert.IsTrue(SpdxExternalReference.Same.Equals(r1, r2)); - Assert.IsTrue(SpdxExternalReference.Same.Equals(r2, r1)); - Assert.IsFalse(SpdxExternalReference.Same.Equals(r1, r3)); - Assert.IsFalse(SpdxExternalReference.Same.Equals(r3, r1)); - Assert.IsFalse(SpdxExternalReference.Same.Equals(r2, r3)); - Assert.IsFalse(SpdxExternalReference.Same.Equals(r3, r2)); + Assert.True(SpdxExternalReference.Same.Equals(r1, r2)); + Assert.True(SpdxExternalReference.Same.Equals(r2, r1)); + Assert.False(SpdxExternalReference.Same.Equals(r1, r3)); + Assert.False(SpdxExternalReference.Same.Equals(r3, r1)); + Assert.False(SpdxExternalReference.Same.Equals(r2, r3)); + Assert.False(SpdxExternalReference.Same.Equals(r3, r2)); // Assert: Verify same external-references have identical hashes - Assert.AreEqual(SpdxExternalReference.Same.GetHashCode(r1), SpdxExternalReference.Same.GetHashCode(r2)); + Assert.Equal(SpdxExternalReference.Same.GetHashCode(r1), SpdxExternalReference.Same.GetHashCode(r2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxExternalReference_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Verifies that the copy has equal field values to the original and that it is a distinct + /// object reference (not the same instance). + /// + [Fact] + public void SpdxExternalReference_DeepCopy_WithAllFields_CreatesEqualButDistinctInstance() { // Arrange: Create an external reference var r1 = new SpdxExternalReference @@ -89,22 +101,26 @@ public void SpdxExternalReference_DeepCopy_CreatesEqualButDistinctInstance() var r2 = r1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(r1, r2, SpdxExternalReference.Same); - Assert.AreEqual(r1.Category, r2.Category); - Assert.AreEqual(r1.Type, r2.Type); - Assert.AreEqual(r1.Locator, r2.Locator); - Assert.AreEqual(r1.Comment, r2.Comment); + Assert.Equal(r1, r2, SpdxExternalReference.Same); + Assert.Equal(r1.Category, r2.Category); + Assert.Equal(r1.Type, r2.Type); + Assert.Equal(r1.Locator, r2.Locator); + Assert.Equal(r1.Comment, r2.Comment); // Assert: Verify deep-copy has distinct instance - Assert.IsFalse(ReferenceEquals(r1, r2)); + Assert.False(ReferenceEquals(r1, r2)); } /// /// Tests the method /// adds or updates information correctly. /// - [TestMethod] - public void SpdxExternalReference_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Verifies that matching entries are enhanced in place and unmatched entries from the + /// source array are appended as new independent copies. + /// + [Fact] + public void SpdxExternalReference_Enhance_WithMatchingAndNewEntries_MergesCorrectly() { // Arrange: Create an array of external references var references = new[] @@ -137,21 +153,25 @@ public void SpdxExternalReference_Enhance_AddsOrUpdatesInformationCorrectly() ]); // Assert: Verify the references array has correct information - Assert.HasCount(2, references); - Assert.AreEqual(SpdxReferenceCategory.Security, references[0].Category); - Assert.AreEqual("cpe23Type", references[0].Type); - Assert.AreEqual("cpe:2.3:a:company:product:0.0.0:*:*:*:*:*:*:*", references[0].Locator); - Assert.AreEqual("CPE23 Standard Identifier", references[0].Comment); - Assert.AreEqual(SpdxReferenceCategory.PackageManager, references[1].Category); - Assert.AreEqual("purl", references[1].Type); - Assert.AreEqual("pkg:nuget/SomePackage@0.0.0", references[1].Locator); + Assert.Equal(2, references.Length); + Assert.Equal(SpdxReferenceCategory.Security, references[0].Category); + Assert.Equal("cpe23Type", references[0].Type); + Assert.Equal("cpe:2.3:a:company:product:0.0.0:*:*:*:*:*:*:*", references[0].Locator); + Assert.Equal("CPE23 Standard Identifier", references[0].Comment); + Assert.Equal(SpdxReferenceCategory.PackageManager, references[1].Category); + Assert.Equal("purl", references[1].Type); + Assert.Equal("pkg:nuget/SomePackage@0.0.0", references[1].Locator); } /// /// Tests the method reports invalid categories. /// - [TestMethod] - public void SpdxExternalReference_Validate_InvalidCategory() + /// + /// Verifies that Validate appends an issue message when the Category field is + /// . + /// + [Fact] + public void SpdxExternalReference_Validate_InvalidCategory_ReportsIssue() { // Arrange: Create an external reference with invalid category var reference = new SpdxExternalReference @@ -167,14 +187,17 @@ public void SpdxExternalReference_Validate_InvalidCategory() reference.Validate("Test", issues); // Assert: Verify that the validation reports the invalid category - Assert.Contains(issue => issue.Contains("Package 'Test' Invalid External Reference Category Field - Missing"), issues); + Assert.Contains(issues, issue => issue.Contains("Package 'Test' Invalid External Reference Category Field - Missing")); } /// /// Tests the method reports invalid types. /// - [TestMethod] - public void SpdxExternalReference_Validate_InvalidType() + /// + /// Verifies that Validate appends an issue message when the Type field is empty. + /// + [Fact] + public void SpdxExternalReference_Validate_InvalidType_ReportsIssue() { // Arrange: Create an external reference with invalid type var reference = new SpdxExternalReference @@ -190,14 +213,17 @@ public void SpdxExternalReference_Validate_InvalidType() reference.Validate("Test", issues); // Assert: Verify that the validation reports the invalid type - Assert.Contains(issue => issue.Contains("Package 'Test' Invalid External Reference Type Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Package 'Test' Invalid External Reference Type Field - Empty")); } /// /// Tests the method reports invalid locators. /// - [TestMethod] - public void SpdxExternalReference_Validate_InvalidLocator() + /// + /// Verifies that Validate appends an issue message when the Locator field is empty. + /// + [Fact] + public void SpdxExternalReference_Validate_InvalidLocator_ReportsIssue() { // Arrange: Create an external reference with invalid locator var reference = new SpdxExternalReference @@ -213,75 +239,103 @@ public void SpdxExternalReference_Validate_InvalidLocator() reference.Validate("Test", issues); // Assert: Verify that the validation reports the invalid locator - Assert.Contains(issue => issue.Contains("Package 'Test' Invalid External Reference Locator Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Package 'Test' Invalid External Reference Locator Field - Empty")); } /// /// Tests the method with valid input. /// - [TestMethod] - public void SpdxReferenceCategoryExtensions_FromText_Valid() + /// + /// Verifies that all recognized category strings, including case variants, map to the + /// expected enum values, and that an empty string maps to Missing. + /// + [Fact] + public void SpdxReferenceCategoryExtensions_FromText_ValidInput_ParsesCorrectly() { - Assert.AreEqual(SpdxReferenceCategory.Missing, SpdxReferenceCategoryExtensions.FromText("")); - Assert.AreEqual(SpdxReferenceCategory.Security, SpdxReferenceCategoryExtensions.FromText("SECURITY")); - Assert.AreEqual(SpdxReferenceCategory.Security, SpdxReferenceCategoryExtensions.FromText("security")); - Assert.AreEqual(SpdxReferenceCategory.Security, SpdxReferenceCategoryExtensions.FromText("Security")); - Assert.AreEqual(SpdxReferenceCategory.PackageManager, + // Arrange: (no external state needed) + + // Act / Assert: Verify all recognized category strings map to expected enum values + Assert.Equal(SpdxReferenceCategory.Missing, SpdxReferenceCategoryExtensions.FromText("")); + Assert.Equal(SpdxReferenceCategory.Security, SpdxReferenceCategoryExtensions.FromText("SECURITY")); + Assert.Equal(SpdxReferenceCategory.Security, SpdxReferenceCategoryExtensions.FromText("security")); + Assert.Equal(SpdxReferenceCategory.Security, SpdxReferenceCategoryExtensions.FromText("Security")); + Assert.Equal(SpdxReferenceCategory.PackageManager, SpdxReferenceCategoryExtensions.FromText("PACKAGE-MANAGER")); - Assert.AreEqual(SpdxReferenceCategory.PackageManager, + Assert.Equal(SpdxReferenceCategory.PackageManager, SpdxReferenceCategoryExtensions.FromText("PACKAGE_MANAGER")); - Assert.AreEqual(SpdxReferenceCategory.PersistentId, SpdxReferenceCategoryExtensions.FromText("PERSISTENT-ID")); - Assert.AreEqual(SpdxReferenceCategory.Other, SpdxReferenceCategoryExtensions.FromText("OTHER")); + Assert.Equal(SpdxReferenceCategory.PersistentId, SpdxReferenceCategoryExtensions.FromText("PERSISTENT-ID")); + Assert.Equal(SpdxReferenceCategory.Other, SpdxReferenceCategoryExtensions.FromText("OTHER")); } /// /// Tests the method with invalid input. /// - [TestMethod] - public void SpdxReferenceCategoryExtensions_FromText_Invalid() + /// + /// Verifies that FromText throws with a message + /// identifying the unsupported value when given an unrecognized category string. + /// + [Fact] + public void SpdxReferenceCategoryExtensions_FromText_InvalidInput_ThrowsInvalidOperationException() { + // Arrange: (no external state needed) + + // Act / Assert: Verify that FromText throws for an unrecognized category string var exception = - Assert.ThrowsExactly(() => SpdxReferenceCategoryExtensions.FromText("invalid")); - Assert.AreEqual("Unsupported SPDX Reference Category 'invalid'", exception.Message); + Assert.Throws(() => SpdxReferenceCategoryExtensions.FromText("invalid")); + Assert.Equal("Unsupported SPDX Reference Category 'invalid'", exception.Message); } /// /// Tests the method with valid input. /// - [TestMethod] - public void SpdxReferenceCategoryExtensions_ToText_Valid() + /// + /// Verifies that all known enum values map to their expected SPDX text representations. + /// + [Fact] + public void SpdxReferenceCategoryExtensions_ToText_ValidReference_FormatsCorrectly() { - Assert.AreEqual("SECURITY", SpdxReferenceCategory.Security.ToText()); - Assert.AreEqual("PACKAGE-MANAGER", SpdxReferenceCategory.PackageManager.ToText()); - Assert.AreEqual("PERSISTENT-ID", SpdxReferenceCategory.PersistentId.ToText()); - Assert.AreEqual("OTHER", SpdxReferenceCategory.Other.ToText()); + // Arrange: (no external state needed) + + // Act / Assert: Verify all known enum values map to expected text representations + Assert.Equal("SECURITY", SpdxReferenceCategory.Security.ToText()); + Assert.Equal("PACKAGE-MANAGER", SpdxReferenceCategory.PackageManager.ToText()); + Assert.Equal("PERSISTENT-ID", SpdxReferenceCategory.PersistentId.ToText()); + Assert.Equal("OTHER", SpdxReferenceCategory.Other.ToText()); } /// /// Tests the method with invalid input. /// - [TestMethod] - public void SpdxReferenceCategoryExtensions_ToText_InvalidCategory() + /// + /// Verifies that ToText throws with the + /// unsupported-category message when called with an unrecognized enum value. + /// + [Fact] + public void SpdxReferenceCategoryExtensions_ToText_InvalidCategory_ThrowsInvalidOperationException() { // Arrange: Create an invalid reference category var invalidCategory = (SpdxReferenceCategory)1000; - // Act & Assert: Verify that ToText throws an exception for unsupported category - var exception = Assert.ThrowsExactly(() => invalidCategory.ToText()); - Assert.AreEqual("Unsupported SPDX Reference Category '1000'", exception.Message); + // Act / Assert: Verify that ToText throws an exception for unsupported category + var exception = Assert.Throws(() => invalidCategory.ToText()); + Assert.Equal("Unsupported SPDX Reference Category '1000'", exception.Message); } /// /// Tests the method with Missing category. /// - [TestMethod] - public void SpdxReferenceCategoryExtensions_ToText_MissingCategory() + /// + /// Verifies that ToText throws with a specific + /// message when called with . + /// + [Fact] + public void SpdxReferenceCategoryExtensions_ToText_MissingCategory_ThrowsInvalidOperationException() { // Arrange: Use Missing reference category var category = SpdxReferenceCategory.Missing; - // Act & Assert: Verify that ToText throws an exception for Missing category - var exception = Assert.ThrowsExactly(() => category.ToText()); - Assert.AreEqual("Attempt to serialize missing SPDX Reference Category", exception.Message); + // Act / Assert: Verify that ToText throws an exception for Missing category + var exception = Assert.Throws(() => category.ToText()); + Assert.Equal("Attempt to serialize missing SPDX Reference Category", exception.Message); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxExtractedLicensingInfoTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxExtractedLicensingInfoTests.cs index a577088..9c71339 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxExtractedLicensingInfoTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxExtractedLicensingInfoTests.cs @@ -23,13 +23,21 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers the Same equality comparer, DeepCopy, Enhance merge, and Validate methods. +/// public class SpdxExtractedLicensingInfoTests { /// /// Tests the comparer compares extracted licensing infos correctly. /// - [TestMethod] + /// + /// Validates that the Same comparer treats two instances with identical ExtractedText as equal + /// regardless of other field differences, treats instances with differing ExtractedText as + /// distinct, handles reference equality, null arguments, and produces consistent hash codes + /// for equal instances. + /// + [Fact] public void SpdxExtractedLicensingInfo_SameComparer_ComparesCorrectly() { // Arrange: Create three extracted licensing infos with different properties @@ -50,28 +58,37 @@ public void SpdxExtractedLicensingInfo_SameComparer_ComparesCorrectly() ExtractedText = "Some Random License" }; - // Assert: Verify extracted-licensing-infos compare to themselves - Assert.IsTrue(SpdxExtractedLicensingInfo.Same.Equals(l1, l1)); - Assert.IsTrue(SpdxExtractedLicensingInfo.Same.Equals(l2, l2)); - Assert.IsTrue(SpdxExtractedLicensingInfo.Same.Equals(l3, l3)); + // Act / Assert: Verify extracted-licensing-infos compare to themselves + Assert.True(SpdxExtractedLicensingInfo.Same.Equals(l1, l1)); + Assert.True(SpdxExtractedLicensingInfo.Same.Equals(l2, l2)); + Assert.True(SpdxExtractedLicensingInfo.Same.Equals(l3, l3)); // Assert: Verify extracted-licensing-infos compare correctly - Assert.IsTrue(SpdxExtractedLicensingInfo.Same.Equals(l1, l2)); - Assert.IsTrue(SpdxExtractedLicensingInfo.Same.Equals(l2, l1)); - Assert.IsFalse(SpdxExtractedLicensingInfo.Same.Equals(l1, l3)); - Assert.IsFalse(SpdxExtractedLicensingInfo.Same.Equals(l3, l1)); - Assert.IsFalse(SpdxExtractedLicensingInfo.Same.Equals(l2, l3)); - Assert.IsFalse(SpdxExtractedLicensingInfo.Same.Equals(l3, l2)); + Assert.True(SpdxExtractedLicensingInfo.Same.Equals(l1, l2)); + Assert.True(SpdxExtractedLicensingInfo.Same.Equals(l2, l1)); + Assert.False(SpdxExtractedLicensingInfo.Same.Equals(l1, l3)); + Assert.False(SpdxExtractedLicensingInfo.Same.Equals(l3, l1)); + Assert.False(SpdxExtractedLicensingInfo.Same.Equals(l2, l3)); + Assert.False(SpdxExtractedLicensingInfo.Same.Equals(l3, l2)); + + // Assert: Verify null handling + Assert.True(SpdxExtractedLicensingInfo.Same.Equals(null!, null!)); + Assert.False(SpdxExtractedLicensingInfo.Same.Equals(null!, l1)); + Assert.False(SpdxExtractedLicensingInfo.Same.Equals(l1, null!)); // Assert: Verify same extracted-licensing-infos have identical hashes - Assert.AreEqual(SpdxExtractedLicensingInfo.Same.GetHashCode(l1), + Assert.Equal(SpdxExtractedLicensingInfo.Same.GetHashCode(l1), SpdxExtractedLicensingInfo.Same.GetHashCode(l2)); } /// /// Tests the method. /// - [TestMethod] + /// + /// Validates that DeepCopy produces a new instance with field values equal to the original + /// and that arrays are independently copied (no shared references between original and copy). + /// + [Fact] public void SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance() { // Arrange: Create an extracted licensing info object @@ -79,6 +96,7 @@ public void SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance( { LicenseId = "LicenseRef-1", ExtractedText = "The CyberNeko Software License", + CrossReferences = ["https://example.com/license"], Comment = "Extracted from files" }; @@ -86,13 +104,14 @@ public void SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance( var l2 = l1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(l1, l2, SpdxExtractedLicensingInfo.Same); - Assert.AreEqual(l1.LicenseId, l2.LicenseId); - Assert.AreEqual(l1.ExtractedText, l2.ExtractedText); - Assert.AreEqual(l1.Comment, l2.Comment); + Assert.Equal(l1, l2, SpdxExtractedLicensingInfo.Same); + Assert.Equal(l1.LicenseId, l2.LicenseId); + Assert.Equal(l1.ExtractedText, l2.ExtractedText); + Assert.Equal(l1.Comment, l2.Comment); // Assert: Verify deep-copy has distinct instance - Assert.IsFalse(ReferenceEquals(l1, l2)); + Assert.False(ReferenceEquals(l1, l2)); + Assert.False(ReferenceEquals(l1.CrossReferences, l2.CrossReferences)); } /// @@ -100,7 +119,11 @@ public void SpdxExtractedLicensingInfo_DeepCopy_CreatesEqualButDistinctInstance( /// /// method adds or updates information correctly. /// - [TestMethod] + /// + /// Validates that the static Enhance merges arrays by enhancing matching entries (matched + /// by ExtractedText) and appending unmatched entries as new deep copies. + /// + [Fact] public void SpdxExtractedLicensingInfo_Enhance_AddsOrUpdatesInformationCorrectly() { // Arrange: Create an array of extracted licensing infos @@ -131,19 +154,48 @@ public void SpdxExtractedLicensingInfo_Enhance_AddsOrUpdatesInformationCorrectly ]); // Assert: Verify the infos array has correct information - Assert.HasCount(2, infos); - Assert.AreEqual("LicenseRef-1", infos[0].LicenseId); - Assert.AreEqual("The CyberNeko Software License", infos[0].ExtractedText); - Assert.AreEqual("Extracted from files", infos[0].Comment); - Assert.AreEqual("LicenseRef-2", infos[1].LicenseId); - Assert.AreEqual("Some Random License", infos[1].ExtractedText); + Assert.Equal(2, infos.Length); + Assert.Equal("LicenseRef-1", infos[0].LicenseId); + Assert.Equal("The CyberNeko Software License", infos[0].ExtractedText); + Assert.Equal("Extracted from files", infos[0].Comment); + Assert.Equal("LicenseRef-2", infos[1].LicenseId); + Assert.Equal("Some Random License", infos[1].ExtractedText); + } + + /// + /// Tests the method returns no issues for a valid input. + /// + /// + /// Validates that Validate reports no issues when both LicenseId and ExtractedText are + /// non-empty, confirming the happy-path behavior of the validation logic. + /// + [Fact] + public void SpdxExtractedLicensingInfo_Validate_ValidInput_ReturnsNoIssues() + { + // Arrange: Create a valid extracted licensing info + var info = new SpdxExtractedLicensingInfo + { + LicenseId = "LicenseRef-1", + ExtractedText = "The CyberNeko Software License" + }; + + // Act: Perform validation on the SpdxExtractedLicensingInfo instance. + var issues = new List(); + info.Validate(issues); + + // Assert: Verify that the validation reports no issues + Assert.Empty(issues); } /// /// Tests the method reports bad license IDs /// - [TestMethod] - public void SpdxExtractedLicensingInfo_Validate_InvalidLicenseId() + /// + /// Validates that Validate appends an issue message to the supplied list when LicenseId is + /// empty, confirming the LicenseId validation path. + /// + [Fact] + public void SpdxExtractedLicensingInfo_Validate_InvalidLicenseId_ReportsIssue() { // Arrange: Create a bad licensing info var info = new SpdxExtractedLicensingInfo @@ -157,14 +209,18 @@ public void SpdxExtractedLicensingInfo_Validate_InvalidLicenseId() info.Validate(issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Extracted License Information Invalid License ID Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Extracted License Information Invalid License ID Field - Empty")); } /// /// Tests the method reports bad extracted text /// - [TestMethod] - public void SpdxExtractedLicensingInfo_Validate_InvalidExtractedText() + /// + /// Validates that Validate appends an issue message to the supplied list when ExtractedText + /// is empty, confirming the ExtractedText validation path. + /// + [Fact] + public void SpdxExtractedLicensingInfo_Validate_InvalidExtractedText_ReportsIssue() { // Arrange: Create a bad licensing info var info = new SpdxExtractedLicensingInfo @@ -178,6 +234,6 @@ public void SpdxExtractedLicensingInfo_Validate_InvalidExtractedText() info.Validate(issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Extracted License Information 'LicenseRef-1' Invalid Extracted Text Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Extracted License Information 'LicenseRef-1' Invalid Extracted Text Field - Empty")); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxFileTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxFileTests.cs index 2b27830..76557e2 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxFileTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxFileTests.cs @@ -23,14 +23,22 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers the Same equality comparer, DeepCopy, Enhance merge, Validate, and the +/// SpdxFileType text-conversion extension methods (FromText/ToText). +/// public class SpdxFileTests { /// /// Tests the comparer compares files correctly. /// - [TestMethod] - public void SpdxFile_SameComparer_ComparesCorrectly() + /// + /// Verifies that two files with the same FileName and compatible SHA1 checksums are + /// considered equal, that differing SHA1 checksums or file names produce inequality, + /// and that equal files produce identical hash codes. + /// + [Fact] + public void SpdxFile_SameComparer_MatchingAndDistinctFiles_ComparesCorrectly() { // Arrange: Create several SpdxFile instances with different IDs, names, and checksums var f1 = new SpdxFile @@ -76,35 +84,49 @@ public void SpdxFile_SameComparer_ComparesCorrectly() } ] }; + var f4 = new SpdxFile + { + FileName = "./file1.txt" + // no checksums — should still match f1/f2 by FileName + }; + + // Act / Assert: Verify files compare to themselves + Assert.True(SpdxFile.Same.Equals(f1, f1)); + Assert.True(SpdxFile.Same.Equals(f2, f2)); + Assert.True(SpdxFile.Same.Equals(f3, f3)); + + // Act / Assert: Verify files compare correctly + Assert.True(SpdxFile.Same.Equals(f1, f2)); + Assert.True(SpdxFile.Same.Equals(f2, f1)); + Assert.False(SpdxFile.Same.Equals(f1, f3)); + Assert.False(SpdxFile.Same.Equals(f3, f1)); + Assert.False(SpdxFile.Same.Equals(f2, f3)); + Assert.False(SpdxFile.Same.Equals(f3, f2)); - // Assert: Verify files compare to themselves - Assert.IsTrue(SpdxFile.Same.Equals(f1, f1)); - Assert.IsTrue(SpdxFile.Same.Equals(f2, f2)); - Assert.IsTrue(SpdxFile.Same.Equals(f3, f3)); - - // Assert: Verify files compare correctly - Assert.IsTrue(SpdxFile.Same.Equals(f1, f2)); - Assert.IsTrue(SpdxFile.Same.Equals(f2, f1)); - Assert.IsFalse(SpdxFile.Same.Equals(f1, f3)); - Assert.IsFalse(SpdxFile.Same.Equals(f3, f1)); - Assert.IsFalse(SpdxFile.Same.Equals(f2, f3)); - Assert.IsFalse(SpdxFile.Same.Equals(f3, f2)); - - // Assert: Verify same files have identical hashes - Assert.AreEqual(SpdxFile.Same.GetHashCode(f1), SpdxFile.Same.GetHashCode(f2)); + // Act / Assert: Verify one-sided SHA1 boundary — same FileName, one has SHA1, other does not + Assert.True(SpdxFile.Same.Equals(f1, f4)); + Assert.True(SpdxFile.Same.Equals(f4, f1)); + + // Act / Assert: Verify same files have identical hashes + Assert.Equal(SpdxFile.Same.GetHashCode(f1), SpdxFile.Same.GetHashCode(f2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxFile_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Verifies that the copy has equal field values to the original and that all array + /// fields are independently copied with no shared references between original and copy. + /// + [Fact] + public void SpdxFile_DeepCopy_FullyPopulatedFile_CreatesEqualButDistinctCopy() { - // Arrange: Create an SpdxFile instance with checksums and comments + // Arrange: Create an SpdxFile instance with all deep-copied fields populated var f1 = new SpdxFile { Id = "SPDXRef-File1", FileName = "./file1.txt", + FileTypes = [SpdxFileType.Source, SpdxFileType.Text], Checksums = [ new SpdxChecksum @@ -118,29 +140,63 @@ public void SpdxFile_DeepCopy_CreatesEqualButDistinctInstance() Value = "624c1abb3664f4b35547e7c73864ad24" } ], - Comment = "File 1" + LicenseInfoInFiles = ["MIT"], + LicenseComments = "No issues", + ConcludedLicense = "MIT", + CopyrightText = "Copyright 2024", + Comment = "File 1", + Notice = "See LICENSE", + Contributors = ["Contributor A"], + AttributionText = ["Attribution notice"], + Annotations = + [ + new SpdxAnnotation + { + Annotator = "Tool: test", + Date = "2024-01-01T00:00:00Z", + Type = SpdxAnnotationType.Review, + Comment = "Reviewed" + } + ] }; // Act: Create a deep copy of the SpdxFile instance var f2 = f1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(f1, f2, SpdxFile.Same); - Assert.AreEqual(f1.Id, f2.Id); - Assert.AreEqual(f1.FileName, f2.FileName); - CollectionAssert.AreEquivalent(f1.Checksums, f2.Checksums, SpdxChecksum.Same); - Assert.AreEqual(f1.Comment, f2.Comment); + Assert.Equal(f1, f2, SpdxFile.Same); + Assert.Equal(f1.Id, f2.Id); + Assert.Equal(f1.FileName, f2.FileName); + Assert.Equal(f1.FileTypes, f2.FileTypes); + SpdxTestHelpers.AssertEquivalent(f1.Checksums, f2.Checksums, SpdxChecksum.Same); + Assert.Equal(f1.LicenseInfoInFiles, f2.LicenseInfoInFiles); + Assert.Equal(f1.LicenseComments, f2.LicenseComments); + Assert.Equal(f1.ConcludedLicense, f2.ConcludedLicense); + Assert.Equal(f1.CopyrightText, f2.CopyrightText); + Assert.Equal(f1.Comment, f2.Comment); + Assert.Equal(f1.Notice, f2.Notice); + Assert.Equal(f1.Contributors, f2.Contributors); + Assert.Equal(f1.AttributionText, f2.AttributionText); // Assert: Verify deep-copy has distinct instances - Assert.IsFalse(ReferenceEquals(f1, f2)); - Assert.IsFalse(ReferenceEquals(f1.Checksums, f2.Checksums)); + Assert.False(ReferenceEquals(f1, f2)); + Assert.False(ReferenceEquals(f1.Checksums, f2.Checksums)); + Assert.False(ReferenceEquals(f1.FileTypes, f2.FileTypes)); + Assert.False(ReferenceEquals(f1.LicenseInfoInFiles, f2.LicenseInfoInFiles)); + Assert.False(ReferenceEquals(f1.Contributors, f2.Contributors)); + Assert.False(ReferenceEquals(f1.AttributionText, f2.AttributionText)); + Assert.False(ReferenceEquals(f1.Annotations, f2.Annotations)); } /// /// Tests the method correctly adds or updates information /// - [TestMethod] - public void SpdxFile_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Verifies that matching entries are enhanced in place and unmatched entries from the + /// source array are appended as new independent copies. + /// + [Fact] + public void SpdxFile_Enhance_MatchingAndNewFiles_MergesCorrectly() { // Arrange: Create an array of SpdxFile objects with one file var files = new[] @@ -197,26 +253,30 @@ public void SpdxFile_Enhance_AddsOrUpdatesInformationCorrectly() ]); // Assert: Verify the files array has been enhanced correctly - Assert.HasCount(2, files); - Assert.AreEqual("SPDXRef-File1", files[0].Id); - Assert.AreEqual("./file1.txt", files[0].FileName); - Assert.HasCount(2, files[0].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, files[0].Checksums[0].Algorithm); - Assert.AreEqual("85ed0817af83a24ad8da68c2b5094de69833983c", files[0].Checksums[0].Value); - Assert.AreEqual(SpdxChecksumAlgorithm.Md5, files[0].Checksums[1].Algorithm); - Assert.AreEqual("624c1abb3664f4b35547e7c73864ad24", files[0].Checksums[1].Value); - Assert.AreEqual("File 1", files[0].Comment); - Assert.AreEqual("./file2.txt", files[1].FileName); - Assert.HasCount(1, files[1].Checksums); - Assert.AreEqual(SpdxChecksumAlgorithm.Sha1, files[1].Checksums[0].Algorithm); - Assert.AreEqual("c2b4e1c67a2d28fced849ee1bb76e7391b93f125", files[1].Checksums[0].Value); + Assert.Equal(2, files.Length); + Assert.Equal("SPDXRef-File1", files[0].Id); + Assert.Equal("./file1.txt", files[0].FileName); + Assert.Equal(2, files[0].Checksums.Length); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, files[0].Checksums[0].Algorithm); + Assert.Equal("85ed0817af83a24ad8da68c2b5094de69833983c", files[0].Checksums[0].Value); + Assert.Equal(SpdxChecksumAlgorithm.Md5, files[0].Checksums[1].Algorithm); + Assert.Equal("624c1abb3664f4b35547e7c73864ad24", files[0].Checksums[1].Value); + Assert.Equal("File 1", files[0].Comment); + Assert.Equal("./file2.txt", files[1].FileName); + Assert.Single(files[1].Checksums); + Assert.Equal(SpdxChecksumAlgorithm.Sha1, files[1].Checksums[0].Algorithm); + Assert.Equal("c2b4e1c67a2d28fced849ee1bb76e7391b93f125", files[1].Checksums[0].Value); } /// /// Tests that an invalid file ID fails validation. /// - [TestMethod] - public void SpdxFile_Validate_ReportsInvalidFileId() + /// + /// Verifies that Validate appends an issue message when the SPDX-ID does not conform + /// to the required SPDXRef- prefix format. + /// + [Fact] + public void SpdxFile_Validate_InvalidFileId_ReportsIssue() { // Arrange: Create an SpdxFile instance with an invalid ID format var spdxFile = new SpdxFile @@ -238,14 +298,84 @@ public void SpdxFile_Validate_ReportsInvalidFileId() spdxFile.Validate(issues); // Assert: Verify that the validation fails and the error message includes the invalid ID. - Assert.Contains(issue => issue.Contains("File './file1.txt' Invalid SPDX Identifier Field"), issues); + Assert.Contains(issues, issue => issue.Contains("File './file1.txt' Invalid SPDX Identifier Field")); + } + + /// + /// Tests that an invalid file name fails validation. + /// + /// + /// Verifies that Validate appends an issue message when FileName does not start with + /// the required "./" prefix. + /// + [Fact] + public void SpdxFile_Validate_InvalidFileName_ReportsIssue() + { + // Arrange: Create an SpdxFile instance with a FileName that has no "./" prefix + var spdxFile = new SpdxFile + { + Id = "SPDXRef-File1", + FileName = "file1.txt", + Checksums = + [ + new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.Sha1, + Value = "85ed0817af83a24ad8da68c2b5094de69833983c" + } + ] + }; + + // Act: Perform validation on the SpdxFile instance. + var issues = new List(); + spdxFile.Validate(issues); + + // Assert: Verify that the validation reports the invalid file name. + Assert.Contains(issues, issue => issue.Contains("Invalid File Name Field")); + } + + /// + /// Tests that a missing SHA1 checksum fails validation. + /// + /// + /// Verifies that Validate appends an issue message when no SHA1 checksum is present + /// in the Checksums array. + /// + [Fact] + public void SpdxFile_Validate_MissingSha1Checksum_ReportsIssue() + { + // Arrange: Create an SpdxFile instance with only an MD5 checksum (no SHA1) + var spdxFile = new SpdxFile + { + Id = "SPDXRef-File1", + FileName = "./file1.txt", + Checksums = + [ + new SpdxChecksum + { + Algorithm = SpdxChecksumAlgorithm.Md5, + Value = "624c1abb3664f4b35547e7c73864ad24" + } + ] + }; + + // Act: Perform validation on the SpdxFile instance. + var issues = new List(); + spdxFile.Validate(issues); + + // Assert: Verify that the validation reports the missing SHA1. + Assert.Contains(issues, issue => issue.Contains("missing SHA1")); } /// /// Tests that a valid file passes validation. /// - [TestMethod] - public void SpdxFile_Validate_Success() + /// + /// Verifies that a fully populated valid SpdxFile passes all validation checks + /// without reporting any issues. + /// + [Fact] + public void SpdxFile_Validate_ValidFile_ReportsNoIssues() { // Arrange: Create a valid SpdxFile instance var spdxFile = new SpdxFile @@ -272,67 +402,95 @@ public void SpdxFile_Validate_Success() spdxFile.Validate(issues); // Assert: Verify that the validation reports no issues. - Assert.IsEmpty(issues); + Assert.Empty(issues); } /// /// Tests the method with valid inputs. /// - [TestMethod] - public void SpdxFileTypeExtensions_FromText_Valid() + /// + /// Verifies that all recognized file type strings, including case variants, map to the + /// expected enum values. + /// + [Fact] + public void SpdxFileTypeExtensions_FromText_ValidInput_ParsesCorrectly() { - Assert.AreEqual(SpdxFileType.Source, SpdxFileTypeExtensions.FromText("SOURCE")); - Assert.AreEqual(SpdxFileType.Source, SpdxFileTypeExtensions.FromText("source")); - Assert.AreEqual(SpdxFileType.Source, SpdxFileTypeExtensions.FromText("Source")); - Assert.AreEqual(SpdxFileType.Binary, SpdxFileTypeExtensions.FromText("BINARY")); - Assert.AreEqual(SpdxFileType.Archive, SpdxFileTypeExtensions.FromText("ARCHIVE")); - Assert.AreEqual(SpdxFileType.Application, SpdxFileTypeExtensions.FromText("APPLICATION")); - Assert.AreEqual(SpdxFileType.Audio, SpdxFileTypeExtensions.FromText("AUDIO")); - Assert.AreEqual(SpdxFileType.Image, SpdxFileTypeExtensions.FromText("IMAGE")); - Assert.AreEqual(SpdxFileType.Text, SpdxFileTypeExtensions.FromText("TEXT")); - Assert.AreEqual(SpdxFileType.Video, SpdxFileTypeExtensions.FromText("VIDEO")); - Assert.AreEqual(SpdxFileType.Documentation, SpdxFileTypeExtensions.FromText("DOCUMENTATION")); - Assert.AreEqual(SpdxFileType.Spdx, SpdxFileTypeExtensions.FromText("SPDX")); - Assert.AreEqual(SpdxFileType.Other, SpdxFileTypeExtensions.FromText("OTHER")); + // Arrange: (no external state needed) + + // Act / Assert: Verify all recognized file type strings map to expected enum values + Assert.Equal(SpdxFileType.Source, SpdxFileTypeExtensions.FromText("SOURCE")); + Assert.Equal(SpdxFileType.Source, SpdxFileTypeExtensions.FromText("source")); + Assert.Equal(SpdxFileType.Source, SpdxFileTypeExtensions.FromText("Source")); + Assert.Equal(SpdxFileType.Binary, SpdxFileTypeExtensions.FromText("BINARY")); + Assert.Equal(SpdxFileType.Archive, SpdxFileTypeExtensions.FromText("ARCHIVE")); + Assert.Equal(SpdxFileType.Application, SpdxFileTypeExtensions.FromText("APPLICATION")); + Assert.Equal(SpdxFileType.Audio, SpdxFileTypeExtensions.FromText("AUDIO")); + Assert.Equal(SpdxFileType.Image, SpdxFileTypeExtensions.FromText("IMAGE")); + Assert.Equal(SpdxFileType.Text, SpdxFileTypeExtensions.FromText("TEXT")); + Assert.Equal(SpdxFileType.Video, SpdxFileTypeExtensions.FromText("VIDEO")); + Assert.Equal(SpdxFileType.Documentation, SpdxFileTypeExtensions.FromText("DOCUMENTATION")); + Assert.Equal(SpdxFileType.Spdx, SpdxFileTypeExtensions.FromText("SPDX")); + Assert.Equal(SpdxFileType.Other, SpdxFileTypeExtensions.FromText("OTHER")); } /// /// Tests the method with invalid input. /// - [TestMethod] - public void SpdxFileTypeExtensions_FromText_Invalid() + /// + /// Verifies that FromText throws with a message + /// identifying the unsupported value when given an unrecognized file type string. + /// + [Fact] + public void SpdxFileTypeExtensions_FromText_InvalidInput_ThrowsException() { + // Arrange: An unrecognized file type string + + // Act / Assert: Verify that FromText throws with a message identifying the unsupported value var exception = - Assert.ThrowsExactly(() => SpdxFileTypeExtensions.FromText("invalid")); - Assert.AreEqual("Unsupported SPDX File Type 'invalid'", exception.Message); + Assert.Throws(() => SpdxFileTypeExtensions.FromText("invalid")); + Assert.Equal("Unsupported SPDX File Type 'invalid'", exception.Message); } /// /// Tests the method with valid inputs /// - [TestMethod] - public void SpdxFileTypeExtensions_ToText_Valid() + /// + /// Verifies that all known file type enum values map to their expected SPDX text + /// representations. + /// + [Fact] + public void SpdxFileTypeExtensions_ToText_ValidEnum_FormatsCorrectly() { - Assert.AreEqual("SOURCE", SpdxFileType.Source.ToText()); - Assert.AreEqual("BINARY", SpdxFileType.Binary.ToText()); - Assert.AreEqual("ARCHIVE", SpdxFileType.Archive.ToText()); - Assert.AreEqual("APPLICATION", SpdxFileType.Application.ToText()); - Assert.AreEqual("AUDIO", SpdxFileType.Audio.ToText()); - Assert.AreEqual("IMAGE", SpdxFileType.Image.ToText()); - Assert.AreEqual("TEXT", SpdxFileType.Text.ToText()); - Assert.AreEqual("VIDEO", SpdxFileType.Video.ToText()); - Assert.AreEqual("DOCUMENTATION", SpdxFileType.Documentation.ToText()); - Assert.AreEqual("SPDX", SpdxFileType.Spdx.ToText()); - Assert.AreEqual("OTHER", SpdxFileType.Other.ToText()); + // Arrange: (no external state needed) + + // Act / Assert: Verify all known enum values map to expected text representations + Assert.Equal("SOURCE", SpdxFileType.Source.ToText()); + Assert.Equal("BINARY", SpdxFileType.Binary.ToText()); + Assert.Equal("ARCHIVE", SpdxFileType.Archive.ToText()); + Assert.Equal("APPLICATION", SpdxFileType.Application.ToText()); + Assert.Equal("AUDIO", SpdxFileType.Audio.ToText()); + Assert.Equal("IMAGE", SpdxFileType.Image.ToText()); + Assert.Equal("TEXT", SpdxFileType.Text.ToText()); + Assert.Equal("VIDEO", SpdxFileType.Video.ToText()); + Assert.Equal("DOCUMENTATION", SpdxFileType.Documentation.ToText()); + Assert.Equal("SPDX", SpdxFileType.Spdx.ToText()); + Assert.Equal("OTHER", SpdxFileType.Other.ToText()); } /// /// Tests the method with invalid input. /// - [TestMethod] - public void SpdxFileTypeExtensions_ToText_Invalid() + /// + /// Verifies that ToText throws when given an + /// unsupported file type enum value. + /// + [Fact] + public void SpdxFileTypeExtensions_ToText_InvalidEnum_ThrowsException() { - var exception = Assert.ThrowsExactly(() => ((SpdxFileType)1000).ToText()); - Assert.AreEqual("Unsupported SPDX File Type '1000'", exception.Message); + // Arrange: An unsupported file type enum value + + // Act / Assert: Verify that ToText throws when given an unsupported enum value + var exception = Assert.Throws(() => ((SpdxFileType)1000).ToText()); + Assert.Equal("Unsupported SPDX File Type '1000'", exception.Message); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxHelpersTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxHelpersTests.cs new file mode 100644 index 0000000..9d2a785 --- /dev/null +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxHelpersTests.cs @@ -0,0 +1,122 @@ +// Copyright(c) 2024 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. + +namespace DemaConsulting.SpdxModel.Tests; + +/// +/// Tests for the class. +/// +public class SpdxHelpersTests +{ + /// + /// Tests that returns true for null input. + /// + [Fact] + public void SpdxHelpers_IsValidSpdxDateTime_NullInput_ReturnsTrue() + { + // Arrange: null represents a not-set date-time field + + // Act + var result = SpdxHelpers.IsValidSpdxDateTime(null); + + // Assert + Assert.True(result); + } + + /// + /// Tests that returns true for an empty string. + /// + [Fact] + public void SpdxHelpers_IsValidSpdxDateTime_EmptyInput_ReturnsTrue() + { + // Arrange: empty string represents a not-set date-time field + + // Act + var result = SpdxHelpers.IsValidSpdxDateTime(""); + + // Assert + Assert.True(result); + } + + /// + /// Tests that returns true for a valid ISO 8601 UTC timestamp. + /// + [Fact] + public void SpdxHelpers_IsValidSpdxDateTime_ValidFormat_ReturnsTrue() + { + // Arrange + const string validDateTime = "2024-01-01T00:00:00Z"; + + // Act + var result = SpdxHelpers.IsValidSpdxDateTime(validDateTime); + + // Assert + Assert.True(result); + } + + /// + /// Tests that returns false for an invalid format. + /// + [Fact] + public void SpdxHelpers_IsValidSpdxDateTime_InvalidFormat_ReturnsFalse() + { + // Arrange + const string invalidDateTime = "not-a-date"; + + // Act + var result = SpdxHelpers.IsValidSpdxDateTime(invalidDateTime); + + // Assert + Assert.False(result); + } + + /// + /// Tests that returns the concrete value when given a + /// mix of concrete value and NOASSERTION. + /// + [Fact] + public void SpdxHelpers_EnhanceString_ConcretePreferredOverNoAssertion_ReturnsConcreteValue() + { + // Arrange + const string concrete = "MIT"; + const string noAssertion = SpdxElement.NoAssertion; + + // Act + var result = SpdxHelpers.EnhanceString(noAssertion, concrete); + + // Assert + Assert.Equal(concrete, result); + } + + /// + /// Tests that returns null when all inputs are null. + /// + [Fact] + public void SpdxHelpers_EnhanceString_NullInputs_ReturnsNull() + { + // Arrange: all candidates are null + + // Act + var result = SpdxHelpers.EnhanceString(null, null); + + // Assert + Assert.Null(result); + } +} diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxLicenseElementTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxLicenseElementTests.cs new file mode 100644 index 0000000..22b71f6 --- /dev/null +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxLicenseElementTests.cs @@ -0,0 +1,244 @@ +// Copyright(c) 2024 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. + +namespace DemaConsulting.SpdxModel.Tests; + +/// +/// Tests for the abstract base class. +/// +/// +/// Exercises the fitness-based field merging implemented in EnhanceLicenseElement +/// through the concrete subclass. Covers all five inherited +/// fields: ConcludedLicense, LicenseComments, CopyrightText, +/// AttributionText, and Annotations. +/// +public class SpdxLicenseElementTests +{ + /// + /// Tests that empty and null license-element fields are replaced by concrete source values. + /// + /// + /// Verifies the lowest-fitness case: an empty ConcludedLicense, empty + /// CopyrightText, and null LicenseComments are all replaced when the + /// source carries concrete (rank-3) values. + /// + [Fact] + public void SpdxLicenseElement_Enhance_EmptyAndNullFields_ReplacedByConcreteValues() + { + // Arrange: Create a package with empty/null license-element fields + var primary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + ConcludedLicense = "", + CopyrightText = "", + LicenseComments = null + }; + var secondary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + ConcludedLicense = "MIT", + CopyrightText = "Copyright 2024 DEMA Consulting", + LicenseComments = "License determined from source headers" + }; + + // Act: Enhance the primary package with the secondary + primary.Enhance(secondary); + + // Assert: Verify that empty/null fields were replaced with concrete values + Assert.Equal("MIT", primary.ConcludedLicense); + Assert.Equal("Copyright 2024 DEMA Consulting", primary.CopyrightText); + Assert.Equal("License determined from source headers", primary.LicenseComments); + } + + /// + /// Tests that NOASSERTION license-element fields are replaced by concrete source values. + /// + /// + /// Verifies the mid-fitness case: ConcludedLicense and CopyrightText set to + /// NOASSERTION (rank 2) are replaced when the source carries concrete (rank-3) + /// values. LicenseComments set to NOASSERTION is similarly replaced. + /// + [Fact] + public void SpdxLicenseElement_Enhance_NoAssertionFields_ReplacedByConcreteValues() + { + // Arrange: Create a package with NOASSERTION license-element fields + var primary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + ConcludedLicense = SpdxElement.NoAssertion, + CopyrightText = SpdxElement.NoAssertion, + LicenseComments = SpdxElement.NoAssertion + }; + var secondary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + ConcludedLicense = "Apache-2.0", + CopyrightText = "Copyright 2024 DEMA Consulting", + LicenseComments = "Apache license confirmed" + }; + + // Act: Enhance the primary package with the secondary + primary.Enhance(secondary); + + // Assert: Verify that NOASSERTION fields were replaced with concrete values + Assert.Equal("Apache-2.0", primary.ConcludedLicense); + Assert.Equal("Copyright 2024 DEMA Consulting", primary.CopyrightText); + Assert.Equal("Apache license confirmed", primary.LicenseComments); + } + + /// + /// Tests that concrete license-element fields are not replaced by secondary values. + /// + /// + /// Verifies the highest-fitness case: once a field holds a concrete (rank-3) value it + /// must not be overwritten by any secondary value regardless of the secondary's fitness + /// level (null, empty, NOASSERTION, or another concrete value). + /// + [Fact] + public void SpdxLicenseElement_Enhance_ConcreteFields_NotReplacedBySecondaryValues() + { + // Arrange: Create a package with concrete license-element fields + var primary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + ConcludedLicense = "MIT", + CopyrightText = "Copyright 2024 DEMA Consulting", + LicenseComments = "MIT license confirmed" + }; + var secondary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + ConcludedLicense = "Apache-2.0", + CopyrightText = "Copyright 2024 Other Corp", + LicenseComments = "Different comment" + }; + + // Act: Enhance the primary package with the secondary + primary.Enhance(secondary); + + // Assert: Verify that concrete fields were not replaced + Assert.Equal("MIT", primary.ConcludedLicense); + Assert.Equal("Copyright 2024 DEMA Consulting", primary.CopyrightText); + Assert.Equal("MIT license confirmed", primary.LicenseComments); + } + + /// + /// Tests that attribution text entries are merged by concatenation and deduplication. + /// + /// + /// Verifies that unique entries from the source are appended to the target's + /// AttributionText array while duplicate entries are discarded so that each + /// attribution notice appears exactly once in the merged result. + /// + [Fact] + public void SpdxLicenseElement_Enhance_AttributionText_MergedByDeduplication() + { + // Arrange: Create packages with overlapping and unique attribution texts + var primary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + AttributionText = ["Attribution A", "Attribution B"] + }; + var secondary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + AttributionText = ["Attribution B", "Attribution C"] + }; + + // Act: Enhance the primary package with the secondary + primary.Enhance(secondary); + + // Assert: Verify that attribution texts were merged with deduplication + Assert.Equal(3, primary.AttributionText.Length); + Assert.Contains("Attribution A", primary.AttributionText); + Assert.Contains("Attribution B", primary.AttributionText); + Assert.Contains("Attribution C", primary.AttributionText); + } + + /// + /// Tests that annotations are merged by identity-match and append. + /// + /// + /// Verifies that annotations matching an existing entry are recognized as the same + /// (identity-match on all four fields) and that annotations with no matching entry in + /// the primary are appended as new independent copies, leaving the total annotation + /// count equal to the number of distinct annotations across both sources. + /// + [Fact] + public void SpdxLicenseElement_Enhance_Annotations_MergedByIdentityAndAppend() + { + // Arrange: Create packages where primary has one annotation and secondary adds a new one + var primary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + Annotations = + [ + new SpdxAnnotation + { + Annotator = "Tool: tool-a", + Date = "2024-01-01T00:00:00Z", + Type = SpdxAnnotationType.Review, + Comment = "Initial review" + } + ] + }; + var secondary = new SpdxPackage + { + Name = "TestPackage", + Version = "1.0.0", + Annotations = + [ + new SpdxAnnotation + { + Annotator = "Tool: tool-a", + Date = "2024-01-01T00:00:00Z", + Type = SpdxAnnotationType.Review, + Comment = "Initial review" + }, + new SpdxAnnotation + { + Annotator = "Tool: tool-b", + Date = "2024-02-01T00:00:00Z", + Type = SpdxAnnotationType.Other, + Comment = "Additional review" + } + ] + }; + + // Act: Enhance the primary package with the secondary + primary.Enhance(secondary); + + // Assert: Verify that annotations were merged by identity-match and append + Assert.Equal(2, primary.Annotations.Length); + Assert.Equal("Tool: tool-a", primary.Annotations[0].Annotator); + Assert.Equal("Initial review", primary.Annotations[0].Comment); + Assert.Equal("Tool: tool-b", primary.Annotations[1].Annotator); + Assert.Equal("Additional review", primary.Annotations[1].Comment); + } +} diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxModelTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxModelTests.cs index 495a427..e84c97b 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxModelTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxModelTests.cs @@ -25,13 +25,12 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// System-level integration tests for the SpdxModel library. /// -[TestClass] public class SpdxModelTests { /// /// Tests that an SPDX 2.2 JSON document can be read by the library. /// - [TestMethod] + [Fact] public void SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully() { // Arrange: Load the SPDX 2.2 JSON example from embedded resources @@ -42,16 +41,16 @@ public void SpdxModel_ReadSpdxJson_Spdx22Example_ParsesSuccessfully() var document = Spdx2JsonDeserializer.Deserialize(json); // Assert: Verify the document was read correctly - Assert.IsNotNull(document); - Assert.AreEqual("SPDX-Tools-v2.0", document.Name); - Assert.AreEqual("SPDX-2.2", document.Version); - Assert.AreEqual("CC0-1.0", document.DataLicense); + Assert.NotNull(document); + Assert.Equal("SPDX-Tools-v2.0", document.Name); + Assert.Equal("SPDX-2.2", document.Version); + Assert.Equal("CC0-1.0", document.DataLicense); } /// /// Tests that an SPDX 2.3 JSON document can be read by the library. /// - [TestMethod] + [Fact] public void SpdxModel_ReadSpdxJson_Spdx23Example_ParsesSuccessfully() { // Arrange: Load the SPDX 2.3 JSON example from embedded resources @@ -62,16 +61,16 @@ public void SpdxModel_ReadSpdxJson_Spdx23Example_ParsesSuccessfully() var document = Spdx2JsonDeserializer.Deserialize(json); // Assert: Verify the document was read correctly - Assert.IsNotNull(document); - Assert.AreEqual("SPDX-Tools-v2.0", document.Name); - Assert.AreEqual("SPDX-2.3", document.Version); - Assert.AreEqual("CC0-1.0", document.DataLicense); + Assert.NotNull(document); + Assert.Equal("SPDX-Tools-v2.0", document.Name); + Assert.Equal("SPDX-2.3", document.Version); + Assert.Equal("CC0-1.0", document.DataLicense); } /// /// Tests that an SPDX 2.2 document loaded by the library passes validation. /// - [TestMethod] + [Fact] public void SpdxModel_ReadSpdxJson_Spdx22Example_PassesValidation() { // Arrange: Load and deserialize the SPDX 2.2 JSON example @@ -84,13 +83,13 @@ public void SpdxModel_ReadSpdxJson_Spdx22Example_PassesValidation() document.Validate(issues); // Assert: Verify no validation issues were found - Assert.IsEmpty(issues); + Assert.Empty(issues); } /// /// Tests that an SPDX 2.3 document loaded by the library passes validation. /// - [TestMethod] + [Fact] public void SpdxModel_ReadSpdxJson_Spdx23Example_PassesValidation() { // Arrange: Load and deserialize the SPDX 2.3 JSON example @@ -103,13 +102,13 @@ public void SpdxModel_ReadSpdxJson_Spdx23Example_PassesValidation() document.Validate(issues); // Assert: Verify no validation issues were found - Assert.IsEmpty(issues); + Assert.Empty(issues); } /// /// Tests that root packages can be identified from a loaded SPDX document. /// - [TestMethod] + [Fact] public void SpdxModel_ReadSpdxJson_Spdx23Example_RootPackagesIdentified() { // Arrange: Load and deserialize the SPDX 2.3 JSON example @@ -121,15 +120,15 @@ public void SpdxModel_ReadSpdxJson_Spdx23Example_RootPackagesIdentified() var rootPackages = document.GetRootPackages(); // Assert: Verify that root packages were identified - Assert.IsNotNull(rootPackages); - Assert.IsTrue(rootPackages.Length > 0); - Assert.IsTrue(Array.Exists(rootPackages, p => p.Id == "SPDXRef-Package")); + Assert.NotNull(rootPackages); + Assert.True(rootPackages.Length > 0); + Assert.True(Array.Exists(rootPackages, p => p.Id == "SPDXRef-Package")); } /// /// Tests that a deep copy of a loaded SPDX document produces an equivalent document. /// - [TestMethod] + [Fact] public void SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocument() { // Arrange: Load and deserialize the SPDX 2.3 JSON example @@ -141,15 +140,15 @@ public void SpdxModel_ReadSpdxJson_Spdx23Example_DeepCopyProducesEquivalentDocum var copy = original.DeepCopy(); // Assert: Verify the copy is equivalent but a distinct instance - Assert.IsNotNull(copy); - Assert.AreNotSame(original, copy); - Assert.IsTrue(SpdxDocument.Same.Equals(original, copy)); + Assert.NotNull(copy); + Assert.NotSame(original, copy); + Assert.True(SpdxDocument.Same.Equals(original, copy)); } /// /// Tests that an SPDX document can be written and read back in a complete round trip. /// - [TestMethod] + [Fact] public void SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds() { // Arrange: Load and deserialize the SPDX 2.3 JSON example @@ -162,11 +161,150 @@ public void SpdxModel_WriteReadSpdxJson_Spdx23Example_RoundTripSucceeds() var roundTripped = Spdx2JsonDeserializer.Deserialize(serialized); // Assert: Verify the round-tripped document matches the original and passes validation - Assert.IsNotNull(roundTripped); - Assert.AreEqual(original.Name, roundTripped.Name); - Assert.AreEqual(original.Version, roundTripped.Version); + Assert.NotNull(roundTripped); + Assert.Equal(original.Name, roundTripped.Name); + Assert.Equal(original.Version, roundTripped.Version); var issues = new List(); roundTripped.Validate(issues); - Assert.IsEmpty(issues); + Assert.Empty(issues); + } + + /// + /// Tests that malformed JSON throws a JsonException when deserialized. + /// + [Fact] + public void SpdxModel_Deserialize_MalformedJson_ThrowsJsonException() + { + // Arrange: Prepare malformed JSON text + const string malformedJson = "{ this is not valid json }"; + + // Act / Assert: malformed JSON throws a JsonException + Assert.ThrowsAny(() => Spdx2JsonDeserializer.Deserialize(malformedJson)); + } + + /// + /// Tests that an invalid SPDX document reports specific validation issues. + /// + [Fact] + public void SpdxModel_Validate_InvalidDocument_ReportsIssues() + { + // Arrange: Create a deliberately incomplete SPDX document + var document = new SpdxDocument + { + Id = "", + Name = "", + Version = "", + DataLicense = "", + DocumentNamespace = "" + }; + + // Act: Validate the document + var issues = new List(); + document.Validate(issues); + + // Assert: Verify that specific validation issues are reported + Assert.True(issues.Count > 0, "Expected validation issues but none were reported."); + Assert.True(issues.Exists(i => i.Contains("SPDX Version")), + "Expected a SPDX Version validation issue."); + } + + /// + /// Tests that required fields on SPDX data model types are non-nullable, + /// and optional fields are nullable. + /// + [Fact] + public void SpdxModel_FieldOptionality_RequiredFieldsNotNull_OptionalFieldsNullable() + { + // Arrange: Create a default instance of key data model types + var document = new SpdxDocument(); + var package = new SpdxPackage(); + var file = new SpdxFile(); + var relationship = new SpdxRelationship(); + + // Act / Assert: default-constructed instances have the expected field nullability + + // Assert: Required fields are non-nullable (strings default to empty, not null) + Assert.NotNull(document.Id); + Assert.NotNull(document.Name); + Assert.NotNull(document.Version); + Assert.NotNull(document.DataLicense); + Assert.NotNull(document.DocumentNamespace); + Assert.NotNull(package.Id); + Assert.NotNull(package.Name); + Assert.NotNull(package.DownloadLocation); + Assert.NotNull(file.Id); + Assert.NotNull(file.FileName); + Assert.NotNull(relationship.Id); + Assert.NotNull(relationship.RelatedSpdxElement); + + // Assert: Optional fields are nullable + Assert.Null(document.Comment); + Assert.Null(package.Comment); + Assert.Null(package.Version); + Assert.Null(file.Comment); + Assert.Null(relationship.Comment); + } + + /// + /// Tests that SPDX date-time validation helper behavior is observable through the document model. + /// + /// + /// Demonstrates that is exercised end-to-end when + /// a real SPDX document is validated, satisfying the system-level observability requirement for + /// helper utilities. + /// + [Fact] + public void SpdxModel_Helpers_DateTimeValidation_IsObservableThroughDocumentModel() + { + // Arrange: Load and deserialize a real SPDX 2.3 document + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + + // Act: Validate the document — validation internally invokes IsValidSpdxDateTime on + // the creation-information timestamp, making helper behavior observable at system level + var issues = new List(); + document.Validate(issues); + + // Assert: The document is valid and the Created timestamp is a non-empty, well-formed value + Assert.Empty(issues); + Assert.False(string.IsNullOrEmpty(document.CreationInformation.Created), + "Expected the Created field to be non-empty after deserialization."); + } + + /// + /// Tests that adding a relationship via the transform API is observable through the document model. + /// + /// + /// Demonstrates end-to-end transform behavior: deserialize a document, add a relationship using + /// the public transform API, and verify the relationship is present in the document model. + /// This satisfies the system-level observability requirement for the transform subsystem. + /// + [Fact] + public void SpdxModel_Transform_AddRelationship_IsObservableThroughDocumentModel() + { + // Arrange: Load and deserialize a real SPDX 2.3 document + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + var initialCount = document.Relationships.Length; + var newRelationship = new SpdxRelationship + { + Id = "SPDXRef-DOCUMENT", + RelationshipType = SpdxRelationshipType.DependsOn, + RelatedSpdxElement = "SPDXRef-Package" + }; + + // Act: Add a relationship using the transform public API + DemaConsulting.SpdxModel.Transform.SpdxRelationships.Add(document, newRelationship); + + // Assert: The relationship is now present in the document + Assert.Equal(initialCount + 1, document.Relationships.Length); + Assert.True( + Array.Exists(document.Relationships, r => + r.Id == "SPDXRef-DOCUMENT" && + r.RelationshipType == SpdxRelationshipType.DependsOn && + r.RelatedSpdxElement == "SPDXRef-Package"), + "Expected the newly added relationship to be present in the document."); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxPackageTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxPackageTests.cs index 076f4b0..81d3fc2 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxPackageTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxPackageTests.cs @@ -23,13 +23,23 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers equality comparison via the comparer, deep-copy independence, +/// field merging via , and full +/// validation including NTIA minimum-elements checks. Each test exercises a single scenario or +/// boundary condition in isolation with no shared state between tests. +/// public class SpdxPackageTests { /// /// Tests the comparer compares packages correctly. /// - [TestMethod] + /// + /// Verifies that two packages with the same Name and Version are considered equal + /// regardless of differing Id values, that packages with different names or versions are + /// distinct, and that null arguments are handled correctly. + /// + [Fact] public void SpdxPackage_SameComparer_ComparesCorrectly() { // Arrange: Create several SpdxPackage instances with different IDs, names, and versions @@ -52,27 +62,36 @@ public void SpdxPackage_SameComparer_ComparesCorrectly() Version = "1.2.3" }; - // Assert: Verify packages compare to themselves - Assert.IsTrue(SpdxPackage.Same.Equals(p1, p1)); - Assert.IsTrue(SpdxPackage.Same.Equals(p2, p2)); - Assert.IsTrue(SpdxPackage.Same.Equals(p3, p3)); + // Act / Assert: Verify packages compare to themselves + Assert.True(SpdxPackage.Same.Equals(p1, p1)); + Assert.True(SpdxPackage.Same.Equals(p2, p2)); + Assert.True(SpdxPackage.Same.Equals(p3, p3)); // Assert: Verify packages compare correctly - Assert.IsTrue(SpdxPackage.Same.Equals(p1, p2)); - Assert.IsTrue(SpdxPackage.Same.Equals(p2, p1)); - Assert.IsFalse(SpdxPackage.Same.Equals(p1, p3)); - Assert.IsFalse(SpdxPackage.Same.Equals(p3, p1)); - Assert.IsFalse(SpdxPackage.Same.Equals(p2, p3)); - Assert.IsFalse(SpdxPackage.Same.Equals(p3, p2)); + Assert.True(SpdxPackage.Same.Equals(p1, p2)); + Assert.True(SpdxPackage.Same.Equals(p2, p1)); + Assert.False(SpdxPackage.Same.Equals(p1, p3)); + Assert.False(SpdxPackage.Same.Equals(p3, p1)); + Assert.False(SpdxPackage.Same.Equals(p2, p3)); + Assert.False(SpdxPackage.Same.Equals(p3, p2)); // Assert: Verify same packages have identical hashes - Assert.AreEqual(SpdxPackage.Same.GetHashCode(p1), SpdxPackage.Same.GetHashCode(p2)); + Assert.Equal(SpdxPackage.Same.GetHashCode(p1), SpdxPackage.Same.GetHashCode(p2)); + + // Assert: Verify null handling + Assert.False(SpdxPackage.Same.Equals(null!, p1)); + Assert.False(SpdxPackage.Same.Equals(p1, null!)); + Assert.True(SpdxPackage.Same.Equals(null!, null!)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] + /// + /// Verifies that the returned instance has equal field values, that all array and nested object + /// fields are new independent instances, and that mutating the copy does not affect the original. + /// + [Fact] public void SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance() { // Arrange: Create a SpdxPackage with various properties @@ -81,6 +100,10 @@ public void SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance() Id = "SPDXRef-Package1", Name = "DemaConsulting.SpdxModel", Version = "0.0.0", + VerificationCode = new SpdxPackageVerificationCode + { + Value = "d6a770ba38583ed4bb4525bd96e50461655d2759" + }, Checksums = [ new SpdxChecksum @@ -114,32 +137,40 @@ public void SpdxPackage_DeepCopy_CreatesEqualButDistinctInstance() var p2 = p1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(p1, p2, SpdxPackage.Same); - Assert.AreEqual(p1.Id, p2.Id); - Assert.AreEqual(p1.Name, p2.Name); - Assert.AreEqual(p1.Version, p2.Version); - CollectionAssert.AreEquivalent(p1.HasFiles, p2.HasFiles); - CollectionAssert.AreEquivalent(p1.Checksums, p2.Checksums, SpdxChecksum.Same); - CollectionAssert.AreEquivalent(p1.LicenseInfoFromFiles, p2.LicenseInfoFromFiles); - CollectionAssert.AreEquivalent(p1.ExternalReferences, p2.ExternalReferences, SpdxExternalReference.Same); - CollectionAssert.AreEquivalent(p1.AttributionText, p2.AttributionText); - CollectionAssert.AreEquivalent(p1.Annotations, p2.Annotations, SpdxAnnotation.Same); + Assert.Equal(p1, p2, SpdxPackage.Same); + Assert.Equal(p1.Id, p2.Id); + Assert.Equal(p1.Name, p2.Name); + Assert.Equal(p1.Version, p2.Version); + Assert.Equal(p1.HasFiles, p2.HasFiles); + SpdxTestHelpers.AssertEquivalent(p1.Checksums, p2.Checksums, SpdxChecksum.Same); + SpdxTestHelpers.AssertEquivalent(p1.LicenseInfoFromFiles, p2.LicenseInfoFromFiles, StringComparer.Ordinal); + SpdxTestHelpers.AssertEquivalent(p1.ExternalReferences, p2.ExternalReferences, SpdxExternalReference.Same); + Assert.Equal(p1.AttributionText, p2.AttributionText); + SpdxTestHelpers.AssertEquivalent(p1.Annotations, p2.Annotations, SpdxAnnotation.Same); + Assert.NotNull(p2.VerificationCode); + Assert.Equal(p1.VerificationCode!.Value, p2.VerificationCode.Value); // Assert: Verify deep-copy has distinct instances - Assert.IsFalse(ReferenceEquals(p1, p2)); - Assert.IsFalse(ReferenceEquals(p1.HasFiles, p2.HasFiles)); - Assert.IsFalse(ReferenceEquals(p1.Checksums, p2.Checksums)); - Assert.IsFalse(ReferenceEquals(p1.LicenseInfoFromFiles, p2.LicenseInfoFromFiles)); - Assert.IsFalse(ReferenceEquals(p1.ExternalReferences, p2.ExternalReferences)); - Assert.IsFalse(ReferenceEquals(p1.AttributionText, p2.AttributionText)); - Assert.IsFalse(ReferenceEquals(p1.Annotations, p2.Annotations)); + Assert.False(ReferenceEquals(p1, p2)); + Assert.False(ReferenceEquals(p1.HasFiles, p2.HasFiles)); + Assert.False(ReferenceEquals(p1.Checksums, p2.Checksums)); + Assert.False(ReferenceEquals(p1.LicenseInfoFromFiles, p2.LicenseInfoFromFiles)); + Assert.False(ReferenceEquals(p1.ExternalReferences, p2.ExternalReferences)); + Assert.False(ReferenceEquals(p1.AttributionText, p2.AttributionText)); + Assert.False(ReferenceEquals(p1.Annotations, p2.Annotations)); + Assert.False(ReferenceEquals(p1.VerificationCode, p2.VerificationCode)); } /// /// Tests the method correctly adds or updates /// packages. /// - [TestMethod] + /// + /// Verifies that a matching package (same name and version) is enhanced in place — including populating a null + /// FilesAnalyzed field from the source — and that a non-matching package from the source array is + /// deep-copied and appended, resulting in an array of length two. + /// + [Fact] public void SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly() { // Arrange: Set up the initial packages and the packages to enhance with. @@ -161,7 +192,8 @@ public void SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly() { Id = "SPDXRef-Package-SpdxModel", Name = "DemaConsulting.SpdxModel", - Version = "0.0.0" + Version = "0.0.0", + FilesAnalyzed = true }, new SpdxPackage { @@ -172,19 +204,24 @@ public void SpdxPackage_Enhance_AddsOrUpdatesPackagesCorrectly() ]); // Assert: Verify the resulting packages are as expected. - Assert.HasCount(2, packages); - Assert.AreEqual("SPDXRef-Package1", packages[0].Id); - Assert.AreEqual("DemaConsulting.SpdxModel", packages[0].Name); - Assert.AreEqual("0.0.0", packages[0].Version); - Assert.AreEqual("SPDXRef-Package1", packages[1].Id); - Assert.AreEqual("SomePackage", packages[1].Name); - Assert.AreEqual("1.2.3", packages[1].Version); + Assert.Equal(2, packages.Length); + Assert.Equal("SPDXRef-Package1", packages[0].Id); + Assert.Equal("DemaConsulting.SpdxModel", packages[0].Name); + Assert.Equal("0.0.0", packages[0].Version); + Assert.True(packages[0].FilesAnalyzed); + Assert.Equal("SPDXRef-Package1", packages[1].Id); + Assert.Equal("SomePackage", packages[1].Name); + Assert.Equal("1.2.3", packages[1].Version); } /// /// Tests that a valid package passes validation. /// - [TestMethod] + /// + /// Exercises the happy-path: a fully populated package with a valid SPDX ID, non-empty name, download + /// location, and a conforming supplier string passes all validation checks including NTIA minimum elements. + /// + [Fact] public void SpdxPackage_Validate_Success() { // Arrange: Construct a valid SpdxPackage @@ -202,14 +239,18 @@ public void SpdxPackage_Validate_Success() package.Validate(issues, null, true); // Assert: Verify that the validation reports no issues. - Assert.IsEmpty(issues); + Assert.Empty(issues); } /// /// Tests the method reports missing package names /// - [TestMethod] - public void SpdxPackage_Validate_MissingPackageName() + /// + /// Verifies the boundary condition where Name is empty: validation must report the + /// "Invalid Package Name Field - Empty" issue. + /// + [Fact] + public void SpdxPackage_Validate_MissingPackageName_ReportsIssue() { // Arrange: Construct a bad SpdxPackage var package = new SpdxPackage @@ -232,8 +273,12 @@ public void SpdxPackage_Validate_MissingPackageName() /// /// Tests the method reports invalid package IDs /// - [TestMethod] - public void SpdxPackage_Validate_InvalidPackageId() + /// + /// Verifies that an Id not starting with SPDXRef- is flagged as an invalid SPDX + /// identifier field. + /// + [Fact] + public void SpdxPackage_Validate_InvalidPackageId_ReportsIssue() { // Arrange: Construct a bad SpdxPackage var package = new SpdxPackage @@ -256,8 +301,12 @@ public void SpdxPackage_Validate_InvalidPackageId() /// /// Tests the method reports missing download locations /// - [TestMethod] - public void SpdxPackage_Validate_MissingDownload() + /// + /// Verifies that an empty DownloadLocation causes the "Invalid Package Download Location Field - Empty" + /// issue to be reported. + /// + [Fact] + public void SpdxPackage_Validate_MissingDownload_ReportsIssue() { // Arrange: Construct a bad SpdxPackage var package = new SpdxPackage @@ -280,8 +329,12 @@ public void SpdxPackage_Validate_MissingDownload() /// /// Tests the method reports invalid suppliers. /// - [TestMethod] - public void SpdxPackage_Validate_InvalidSupplier() + /// + /// Verifies that a supplier value that does not start with Person:, Organization:, or equal + /// NOASSERTION is flagged as an invalid supplier field. + /// + [Fact] + public void SpdxPackage_Validate_InvalidSupplier_ReportsIssue() { // Arrange: Construct a package with invalid supplier format var package = new SpdxPackage @@ -304,8 +357,12 @@ public void SpdxPackage_Validate_InvalidSupplier() /// /// Tests the method reports invalid originators. /// - [TestMethod] - public void SpdxPackage_Validate_InvalidOriginator() + /// + /// Verifies that an originator value that does not start with Person:, Organization:, or equal + /// NOASSERTION is flagged as an invalid originator field. + /// + [Fact] + public void SpdxPackage_Validate_InvalidOriginator_ReportsIssue() { // Arrange: Construct a package with invalid originator format var package = new SpdxPackage @@ -329,8 +386,12 @@ public void SpdxPackage_Validate_InvalidOriginator() /// /// Tests the method reports invalid release dates. /// - [TestMethod] - public void SpdxPackage_Validate_InvalidReleaseDate() + /// + /// Verifies that a ReleaseDate that does not conform to the SPDX date-time format causes the + /// "Invalid Release Date Field" issue to be reported. + /// + [Fact] + public void SpdxPackage_Validate_InvalidReleaseDate_ReportsIssue() { // Arrange: Construct a package with invalid release date format var package = new SpdxPackage @@ -354,8 +415,12 @@ public void SpdxPackage_Validate_InvalidReleaseDate() /// /// Tests the method reports invalid built dates. /// - [TestMethod] - public void SpdxPackage_Validate_InvalidBuiltDate() + /// + /// Verifies that a BuiltDate that does not conform to the SPDX date-time format causes the + /// "Invalid Built Date Field" issue to be reported. + /// + [Fact] + public void SpdxPackage_Validate_InvalidBuiltDate_ReportsIssue() { // Arrange: Construct a package with invalid built date format var package = new SpdxPackage @@ -379,8 +444,12 @@ public void SpdxPackage_Validate_InvalidBuiltDate() /// /// Tests the method reports bad valid until dates /// - [TestMethod] - public void SpdxPackage_Validate_InvalidValidUntilDate() + /// + /// Verifies that a ValidUntilDate that does not conform to the SPDX date-time format causes the + /// "Invalid Valid Until Date Field" issue to be reported. + /// + [Fact] + public void SpdxPackage_Validate_InvalidValidUntilDate_ReportsIssue() { // Arrange: Construct a bad SpdxPackage var package = new SpdxPackage @@ -404,8 +473,12 @@ public void SpdxPackage_Validate_InvalidValidUntilDate() /// /// Tests the method validates annotations. /// - [TestMethod] - public void SpdxPackage_Validate_InvalidAnnotation() + /// + /// Verifies that an annotation with an empty Annotator field causes the + /// "Invalid Annotator Field - Empty" issue to be reported with the correct package prefix. + /// + [Fact] + public void SpdxPackage_Validate_InvalidAnnotation_ReportsIssue() { // Arrange: Construct a package with an invalid annotation var package = new SpdxPackage @@ -434,4 +507,91 @@ public void SpdxPackage_Validate_InvalidAnnotation() // Assert: Verify the annotation issue is reported with the correct prefix Assert.Contains("Package 'DemaConsulting.SpdxModel' Invalid Annotator Field - Empty", issues); } + + /// + /// Tests the method reports HasFiles references to missing files. + /// + /// + /// Verifies that when a document is provided and HasFiles references a file ID that does not + /// exist in doc.Files, the "HasFiles references missing files" issue is reported. + /// + [Fact] + public void SpdxPackage_Validate_HasFilesReferencesMissingFile_ReportsIssue() + { + // Arrange: Create a package that references a file that does not exist in the document + var package = new SpdxPackage + { + Id = "SPDXRef-Package-SpdxModel", + Name = "DemaConsulting.SpdxModel", + Version = "0.0.0", + DownloadLocation = "https://www.nuget.org/packages/DemaConsulting.SpdxModel", + Supplier = "Organization: DemaConsulting", + HasFiles = ["SPDXRef-Missing-File"] + }; + var doc = new SpdxDocument + { + Files = [] + }; + + // Act: Validate the package with the document + var issues = new List(); + package.Validate(issues, doc); + + // Assert: Verify the HasFiles reference issue is reported + Assert.Contains(issues, issue => issue.Contains("Package 'DemaConsulting.SpdxModel' HasFiles references missing files")); + } + + /// + /// Tests the method reports missing NTIA supplier. + /// + /// + /// Verifies that when NTIA validation is enabled, a package without a supplier causes the + /// "NTIA: Package Missing Supplier" issue to be reported. + /// + [Fact] + public void SpdxPackage_ValidateNtia_MissingSupplier_ReportsIssue() + { + // Arrange: Create a package with no supplier + var package = new SpdxPackage + { + Id = "SPDXRef-Package-SpdxModel", + Name = "DemaConsulting.SpdxModel", + Version = "0.0.0", + DownloadLocation = "https://www.nuget.org/packages/DemaConsulting.SpdxModel" + }; + + // Act: Validate the package with NTIA checks enabled + var issues = new List(); + package.Validate(issues, null, ntia: true); + + // Assert: Verify the missing supplier issue is reported + Assert.Contains(issues, issue => issue.Contains("NTIA: Package 'DemaConsulting.SpdxModel' Missing Supplier")); + } + + /// + /// Tests the method reports missing NTIA version. + /// + /// + /// Verifies that when NTIA validation is enabled, a package without a version string causes the + /// "NTIA: Package Missing Version" issue to be reported. + /// + [Fact] + public void SpdxPackage_ValidateNtia_MissingVersion_ReportsIssue() + { + // Arrange: Create a package with no version + var package = new SpdxPackage + { + Id = "SPDXRef-Package-SpdxModel", + Name = "DemaConsulting.SpdxModel", + DownloadLocation = "https://www.nuget.org/packages/DemaConsulting.SpdxModel", + Supplier = "Organization: DemaConsulting" + }; + + // Act: Validate the package with NTIA checks enabled + var issues = new List(); + package.Validate(issues, null, ntia: true); + + // Assert: Verify the missing version issue is reported + Assert.Contains(issues, issue => issue.Contains("NTIA: Package 'DemaConsulting.SpdxModel' Missing Version")); + } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxPackageVerificationCodeTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxPackageVerificationCodeTests.cs index a4a074e..3e7b0dd 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxPackageVerificationCodeTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxPackageVerificationCodeTests.cs @@ -23,14 +23,22 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers equality comparison via the comparer, +/// deep-copy independence, field merging via , +/// and SHA1 hex digest validation via . +/// public class SpdxPackageVerificationCodeTests { /// /// Tests the comparer compares package verification codes correctly. /// - [TestMethod] - public void SpdxPackageVerificationCode_SameComparer_ComparesCorrectly() + /// + /// Verifies that two codes with the same Value but different ExcludedFiles are considered equal, + /// while codes with different values are considered distinct. Also validates null handling and hash consistency. + /// + [Fact] + public void SpdxPackageVerificationCode_SameComparer_SameValueDifferentExcludedFiles_ReturnsEqual() { // Arrange: Create three package verification codes with different properties var v1 = new SpdxPackageVerificationCode @@ -47,29 +55,38 @@ public void SpdxPackageVerificationCode_SameComparer_ComparesCorrectly() Value = "85ed0817af83a24ad8da68c2b5094de69833983c" }; - // Assert: Verify package-verification-codes compare to themselves - Assert.IsTrue(SpdxPackageVerificationCode.Same.Equals(v1, v1)); - Assert.IsTrue(SpdxPackageVerificationCode.Same.Equals(v2, v2)); - Assert.IsTrue(SpdxPackageVerificationCode.Same.Equals(v3, v3)); + // Act / Assert: Verify package-verification-codes compare to themselves + Assert.True(SpdxPackageVerificationCode.Same.Equals(v1, v1)); + Assert.True(SpdxPackageVerificationCode.Same.Equals(v2, v2)); + Assert.True(SpdxPackageVerificationCode.Same.Equals(v3, v3)); // Assert: Verify package-verification-codes compare correctly - Assert.IsTrue(SpdxPackageVerificationCode.Same.Equals(v1, v2)); - Assert.IsTrue(SpdxPackageVerificationCode.Same.Equals(v2, v1)); - Assert.IsFalse(SpdxPackageVerificationCode.Same.Equals(v1, v3)); - Assert.IsFalse(SpdxPackageVerificationCode.Same.Equals(v3, v1)); - Assert.IsFalse(SpdxPackageVerificationCode.Same.Equals(v2, v3)); - Assert.IsFalse(SpdxPackageVerificationCode.Same.Equals(v3, v2)); + Assert.True(SpdxPackageVerificationCode.Same.Equals(v1, v2)); + Assert.True(SpdxPackageVerificationCode.Same.Equals(v2, v1)); + Assert.False(SpdxPackageVerificationCode.Same.Equals(v1, v3)); + Assert.False(SpdxPackageVerificationCode.Same.Equals(v3, v1)); + Assert.False(SpdxPackageVerificationCode.Same.Equals(v2, v3)); + Assert.False(SpdxPackageVerificationCode.Same.Equals(v3, v2)); // Assert: Verify same package-verification-codes have identical hashes - Assert.AreEqual(SpdxPackageVerificationCode.Same.GetHashCode(v1), + Assert.Equal(SpdxPackageVerificationCode.Same.GetHashCode(v1), SpdxPackageVerificationCode.Same.GetHashCode(v2)); + + // Assert: Verify null handling + Assert.True(SpdxPackageVerificationCode.Same.Equals(null!, null!)); + Assert.False(SpdxPackageVerificationCode.Same.Equals(null!, v1)); + Assert.False(SpdxPackageVerificationCode.Same.Equals(v1, null!)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxPackageVerificationCode_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Verifies that the deep copy has equal field values and that both the code object and its + /// ExcludedFiles array are distinct instances that can be mutated independently. + /// + [Fact] + public void SpdxPackageVerificationCode_DeepCopy_FullyPopulatedCode_CreatesEqualButDistinctCopy() { // Arrange: Create a package verification code with excluded files and a value var v1 = new SpdxPackageVerificationCode @@ -82,20 +99,24 @@ public void SpdxPackageVerificationCode_DeepCopy_CreatesEqualButDistinctInstance var v2 = v1.DeepCopy(); // Assert: Verify deep-copy is equal to original - Assert.AreEqual(v1, v2, SpdxPackageVerificationCode.Same); - CollectionAssert.AreEqual(v1.ExcludedFiles, v2.ExcludedFiles); - Assert.AreEqual(v1.Value, v2.Value); + Assert.Equal(v1, v2, SpdxPackageVerificationCode.Same); + Assert.Equal(v1.ExcludedFiles, v2.ExcludedFiles); + Assert.Equal(v1.Value, v2.Value); // Assert: Verify deep-copy has distinct instances - Assert.IsFalse(ReferenceEquals(v1, v2)); - Assert.IsFalse(ReferenceEquals(v1.ExcludedFiles, v2.ExcludedFiles)); + Assert.False(ReferenceEquals(v1, v2)); + Assert.False(ReferenceEquals(v1.ExcludedFiles, v2.ExcludedFiles)); } /// /// Tests the method adds or updates information correctly. /// - [TestMethod] - public void SpdxPackageVerificationCode_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Verifies that enhancing a code with excluded files merges them by deduplication, and that an + /// existing non-empty Value is not overwritten by the source. + /// + [Fact] + public void SpdxPackageVerificationCode_Enhance_MissingFields_MergesCorrectly() { // Arrange: Create a package verification code with a value var info = new SpdxPackageVerificationCode @@ -112,16 +133,20 @@ public void SpdxPackageVerificationCode_Enhance_AddsOrUpdatesInformationCorrectl }); // Assert: Verify the excluded files and value are updated correctly - Assert.HasCount(1, info.ExcludedFiles); - Assert.AreEqual("./package.spdx", info.ExcludedFiles[0]); - Assert.AreEqual("d6a770ba38583ed4bb4525bd96e50461655d2758", info.Value); + Assert.Single(info.ExcludedFiles); + Assert.Equal("./package.spdx", info.ExcludedFiles[0]); + Assert.Equal("d6a770ba38583ed4bb4525bd96e50461655d2758", info.Value); } /// - /// Tests the method reports bad annotators + /// Tests the Validate method reports an issue when the verification code value is invalid. /// - [TestMethod] - public void SpdxPackageVerificationCode_Validate_InvalidValue() + /// + /// Exercises the short-string boundary condition: a value shorter than 40 characters is not a valid + /// SHA1 hex digest and must produce a validation issue. + /// + [Fact] + public void SpdxPackageVerificationCode_Validate_InvalidValue_ReportsIssue() { // Arrange: Create a bad package verification code var info = new SpdxPackageVerificationCode @@ -134,6 +159,54 @@ public void SpdxPackageVerificationCode_Validate_InvalidValue() info.Validate("Test", issues); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Package 'Test' Invalid Package Verification Code Value 'BadValue'"), issues); + Assert.Contains(issues, issue => issue.Contains("Package 'Test' Invalid Package Verification Code Value 'BadValue'")); + } + + /// + /// Tests the method reports no issues for a valid value. + /// + /// + /// Verifies the happy-path: a well-formed 40-character lowercase hex SHA1 digest passes all validation + /// checks without reporting any issues. + /// + [Fact] + public void SpdxPackageVerificationCode_Validate_ValidValue_ReportsNoIssues() + { + // Arrange: Create a package verification code with a valid SHA1 value + var info = new SpdxPackageVerificationCode + { + Value = "d6a770ba38583ed4bb4525bd96e50461655d2758" + }; + + // Act: Perform validation on the SpdxPackageVerificationCode instance + var issues = new List(); + info.Validate("Test", issues); + + // Assert: Verify that the validation reports no issues + Assert.Empty(issues); + } + + /// + /// Tests the method reports an issue for non-hex characters. + /// + /// + /// Exercises the non-hex boundary condition: a string that is exactly 40 characters long but contains + /// characters outside the hexadecimal alphabet (0–9, a–f, A–F) must produce a validation issue. + /// + [Fact] + public void SpdxPackageVerificationCode_Validate_NonHexValue_ReportsIssue() + { + // Arrange: Create a package verification code with 40 chars but invalid hex + var info = new SpdxPackageVerificationCode + { + Value = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }; + + // Act: Perform validation on the SpdxPackageVerificationCode instance + var issues = new List(); + info.Validate("Test", issues); + + // Assert: Verify that the validation fails + Assert.Contains(issues, issue => issue.Contains("Package 'Test' Invalid Package Verification Code Value")); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxRelationshipTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxRelationshipTests.cs index 7e9d3c1..91941be 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxRelationshipTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxRelationshipTests.cs @@ -23,16 +23,27 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers equality comparison via and +/// comparers, deep-copy independence, field merging via +/// , validation via +/// , and round-trip text conversion via +/// . Each test exercises a single scenario or +/// boundary condition in isolation with no shared state between tests. +/// public class SpdxRelationshipTests { /// - /// Tests the comparer compares relationships correctly. + /// Tests that the comparer identifies matching relationships as equal. /// - [TestMethod] - public void SpdxRelationship_SameComparer_ComparesCorrectly() + /// + /// Verifies that two relationships with the same Id, RelationshipType, and + /// RelatedSpdxElement are considered equal even when Comment differs. + /// + [Fact] + public void SpdxRelationship_SameComparer_MatchingRelationships_ReturnsTrue() { - // Arrange: Create three relationships with different properties + // Arrange: Create two relationships that differ only in Comment var r1 = new SpdxRelationship { Id = "SPDXRef-Package1", @@ -46,6 +57,32 @@ public void SpdxRelationship_SameComparer_ComparesCorrectly() RelatedSpdxElement = "SPDXRef-Package2", Comment = "Package 1 contains Package 2" }; + + // Act: Compare the two relationships + var result = SpdxRelationship.Same.Equals(r1, r2); + + // Assert: Verify the relationships are considered equal + Assert.True(result); + Assert.True(SpdxRelationship.Same.Equals(r2, r1)); + } + + /// + /// Tests that the comparer identifies different relationships as not equal. + /// + /// + /// Verifies that two relationships with different Id, RelationshipType, or + /// RelatedSpdxElement values are considered distinct. + /// + [Fact] + public void SpdxRelationship_SameComparer_DifferentRelationships_ReturnsFalse() + { + // Arrange: Create two relationships with different key fields + var r1 = new SpdxRelationship + { + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.Contains, + RelatedSpdxElement = "SPDXRef-Package2" + }; var r3 = new SpdxRelationship { Id = "SPDXRef-Package3", @@ -53,31 +90,58 @@ public void SpdxRelationship_SameComparer_ComparesCorrectly() RelatedSpdxElement = "SPDXRef-Package4" }; - // Assert: Verify relationships compare to themselves - Assert.IsTrue(SpdxRelationship.Same.Equals(r1, r1)); - Assert.IsTrue(SpdxRelationship.Same.Equals(r2, r2)); - Assert.IsTrue(SpdxRelationship.Same.Equals(r3, r3)); - - // Assert: Verify relationships compare correctly - Assert.IsTrue(SpdxRelationship.Same.Equals(r1, r2)); - Assert.IsTrue(SpdxRelationship.Same.Equals(r2, r1)); - Assert.IsFalse(SpdxRelationship.Same.Equals(r1, r3)); - Assert.IsFalse(SpdxRelationship.Same.Equals(r3, r1)); - Assert.IsFalse(SpdxRelationship.Same.Equals(r2, r3)); - Assert.IsFalse(SpdxRelationship.Same.Equals(r3, r2)); - - // Assert: Verify same relationships have identical hashes - Assert.AreEqual(SpdxRelationship.Same.GetHashCode(r1), SpdxRelationship.Same.GetHashCode(r2)); + // Act: Compare the two relationships + var result = SpdxRelationship.Same.Equals(r1, r3); + + // Assert: Verify the relationships are considered distinct + Assert.False(result); + Assert.False(SpdxRelationship.Same.Equals(r3, r1)); + } + + /// + /// Tests that the comparer produces the same hash code for equal relationships. + /// + /// + /// Verifies that two relationships considered equal by produce + /// identical hash codes, satisfying the hash/equality contract. + /// + [Fact] + public void SpdxRelationship_SameComparer_MatchingRelationships_ReturnsSameHashCode() + { + // Arrange: Create two relationships that differ only in Comment + var r1 = new SpdxRelationship + { + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.Contains, + RelatedSpdxElement = "SPDXRef-Package2" + }; + var r2 = new SpdxRelationship + { + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.Contains, + RelatedSpdxElement = "SPDXRef-Package2", + Comment = "Package 1 contains Package 2" + }; + + // Act: Compute hash codes for both relationships + var hash1 = SpdxRelationship.Same.GetHashCode(r1); + var hash2 = SpdxRelationship.Same.GetHashCode(r2); + + // Assert: Verify the hash codes are identical + Assert.Equal(hash1, hash2); } /// - /// Tests the comparer compares relationships with the same elements - /// correctly, + /// Tests that the comparer identifies matching elements as equal. /// - [TestMethod] - public void SpdxRelationship_SameElementsComparer_ComparesCorrectly() + /// + /// Verifies that two relationships with the same Id and RelatedSpdxElement are considered equal + /// even when RelationshipType differs. + /// + [Fact] + public void SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsTrue() { - // Arrange: Create three relationships with different properties + // Arrange: Create two relationships that differ only in RelationshipType var r1 = new SpdxRelationship { Id = "SPDXRef-Package1", @@ -91,6 +155,32 @@ public void SpdxRelationship_SameElementsComparer_ComparesCorrectly() RelatedSpdxElement = "SPDXRef-Package2", Comment = "Package 1 builds Package 2" }; + + // Act: Compare the two relationships + var result = SpdxRelationship.SameElements.Equals(r1, r2); + + // Assert: Verify the relationships are considered equal + Assert.True(result); + Assert.True(SpdxRelationship.SameElements.Equals(r2, r1)); + } + + /// + /// Tests that the comparer identifies different elements as not equal. + /// + /// + /// Verifies that two relationships with different Id or RelatedSpdxElement are considered distinct, + /// regardless of their RelationshipType. + /// + [Fact] + public void SpdxRelationship_SameElementsComparer_DifferentElements_ReturnsFalse() + { + // Arrange: Create two relationships with different element IDs + var r1 = new SpdxRelationship + { + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.Contains, + RelatedSpdxElement = "SPDXRef-Package2" + }; var r3 = new SpdxRelationship { Id = "SPDXRef-Package3", @@ -98,28 +188,56 @@ public void SpdxRelationship_SameElementsComparer_ComparesCorrectly() RelatedSpdxElement = "SPDXRef-Package4" }; - // Assert: Verifies relationships compare to themselves - Assert.IsTrue(SpdxRelationship.SameElements.Equals(r1, r1)); - Assert.IsTrue(SpdxRelationship.SameElements.Equals(r2, r2)); - Assert.IsTrue(SpdxRelationship.SameElements.Equals(r3, r3)); - - // Assert: Verifies relationships compare correctly - Assert.IsTrue(SpdxRelationship.SameElements.Equals(r1, r2)); - Assert.IsTrue(SpdxRelationship.SameElements.Equals(r2, r1)); - Assert.IsFalse(SpdxRelationship.SameElements.Equals(r1, r3)); - Assert.IsFalse(SpdxRelationship.SameElements.Equals(r3, r1)); - Assert.IsFalse(SpdxRelationship.SameElements.Equals(r2, r3)); - Assert.IsFalse(SpdxRelationship.SameElements.Equals(r3, r2)); - - // Assert: Verifies same relationships have identical hashes - Assert.AreEqual(SpdxRelationship.SameElements.GetHashCode(r1), SpdxRelationship.SameElements.GetHashCode(r2)); + // Act: Compare the two relationships + var result = SpdxRelationship.SameElements.Equals(r1, r3); + + // Assert: Verify the relationships are considered distinct + Assert.False(result); + Assert.False(SpdxRelationship.SameElements.Equals(r3, r1)); + } + + /// + /// Tests that the comparer produces the same hash code for equal + /// relationships. + /// + /// + /// Verifies that two relationships considered equal by produce + /// identical hash codes, satisfying the hash/equality contract. + /// + [Fact] + public void SpdxRelationship_SameElementsComparer_MatchingElements_ReturnsSameHashCode() + { + // Arrange: Create two relationships that differ only in RelationshipType + var r1 = new SpdxRelationship + { + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.Contains, + RelatedSpdxElement = "SPDXRef-Package2" + }; + var r2 = new SpdxRelationship + { + Id = "SPDXRef-Package1", + RelationshipType = SpdxRelationshipType.BuildToolOf, + RelatedSpdxElement = "SPDXRef-Package2" + }; + + // Act: Compute hash codes for both relationships + var hash1 = SpdxRelationship.SameElements.GetHashCode(r1); + var hash2 = SpdxRelationship.SameElements.GetHashCode(r2); + + // Assert: Verify the hash codes are identical + Assert.Equal(hash1, hash2); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxRelationship_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Verifies that the returned instance has equal field values for all scalar properties and + /// is a distinct object reference from the original. + /// + [Fact] + public void SpdxRelationship_DeepCopy_FullyPopulatedRelationship_CreatesEqualButDistinctCopy() { // Arrange: Create a relationship with properties var r1 = new SpdxRelationship @@ -134,22 +252,27 @@ public void SpdxRelationship_DeepCopy_CreatesEqualButDistinctInstance() var r2 = r1.DeepCopy(); // Assert: Verifies deep-copy is equal to original - Assert.AreEqual(r1, r2, SpdxRelationship.Same); - Assert.AreEqual(r1.Id, r2.Id); - Assert.AreEqual(r1.RelationshipType, r2.RelationshipType); - Assert.AreEqual(r1.RelatedSpdxElement, r2.RelatedSpdxElement); - Assert.AreEqual(r1.Comment, r2.Comment); + Assert.Equal(r1, r2, SpdxRelationship.Same); + Assert.Equal(r1.Id, r2.Id); + Assert.Equal(r1.RelationshipType, r2.RelationshipType); + Assert.Equal(r1.RelatedSpdxElement, r2.RelatedSpdxElement); + Assert.Equal(r1.Comment, r2.Comment); // Assert: Verifies deep-copy has distinct instance - Assert.IsFalse(ReferenceEquals(r1, r2)); + Assert.False(ReferenceEquals(r1, r2)); } /// /// Tests the method adds or updates /// information correctly. /// - [TestMethod] - public void SpdxRelationship_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Verifies that a matching relationship (same id, type, and related element) is enhanced in place + /// and that a non-matching relationship from the source array is deep-copied and appended, resulting in + /// an array of length two. + /// + [Fact] + public void SpdxRelationship_Enhance_MatchingAndNewRelationships_MergesCorrectly() { // Arrange: Create an array of relationships var relationships = new[] @@ -182,21 +305,25 @@ public void SpdxRelationship_Enhance_AddsOrUpdatesInformationCorrectly() ]); // Assert: Verify the relationships array has correct information - Assert.HasCount(2, relationships); - Assert.AreEqual("SPDXRef-Package1", relationships[0].Id); - Assert.AreEqual(SpdxRelationshipType.Contains, relationships[0].RelationshipType); - Assert.AreEqual("SPDXRef-Package2", relationships[0].RelatedSpdxElement); - Assert.AreEqual("Package 1 contains Package 2", relationships[0].Comment); - Assert.AreEqual("SPDXRef-Package3", relationships[1].Id); - Assert.AreEqual(SpdxRelationshipType.DevToolOf, relationships[1].RelationshipType); - Assert.AreEqual("SPDXRef-Package4", relationships[1].RelatedSpdxElement); + Assert.Equal(2, relationships.Length); + Assert.Equal("SPDXRef-Package1", relationships[0].Id); + Assert.Equal(SpdxRelationshipType.Contains, relationships[0].RelationshipType); + Assert.Equal("SPDXRef-Package2", relationships[0].RelatedSpdxElement); + Assert.Equal("Package 1 contains Package 2", relationships[0].Comment); + Assert.Equal("SPDXRef-Package3", relationships[1].Id); + Assert.Equal(SpdxRelationshipType.DevToolOf, relationships[1].RelationshipType); + Assert.Equal("SPDXRef-Package4", relationships[1].RelatedSpdxElement); } /// /// Tests the method reports missing ids /// - [TestMethod] - public void SpdxRelationship_Validate_MissingId() + /// + /// Verifies that an empty Id causes the "Relationship Invalid SPDX Element ID Field - Empty" + /// issue to be reported. + /// + [Fact] + public void SpdxRelationship_Validate_MissingRelationshipId_ReportsIssue() { // Arrange: Create a bad relationship var relationship = new SpdxRelationship() @@ -211,14 +338,18 @@ public void SpdxRelationship_Validate_MissingId() relationship.Validate(issues, null); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Relationship Invalid SPDX Element ID Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Relationship Invalid SPDX Element ID Field - Empty")); } /// - /// Tests the method reports missing ids + /// Tests the method reports missing or empty related element IDs. /// - [TestMethod] - public void SpdxRelationship_Validate_MissingRelatedId() + /// + /// Verifies that an empty RelatedSpdxElement causes the "Relationship Invalid Related SPDX Element + /// Field - Empty" issue to be reported. + /// + [Fact] + public void SpdxRelationship_Validate_MissingRelatedElementId_ReportsIssue() { // Arrange: Create a bad relationship var relationship = new SpdxRelationship() @@ -233,14 +364,18 @@ public void SpdxRelationship_Validate_MissingRelatedId() relationship.Validate(issues, null); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Relationship Invalid Related SPDX Element Field - Empty"), issues); + Assert.Contains(issues, issue => issue.Contains("Relationship Invalid Related SPDX Element Field - Empty")); } /// /// Tests the method reports missing relationships /// - [TestMethod] - public void SpdxRelationship_Validate_MissingRelationship() + /// + /// Verifies that a RelationshipType of causes the + /// "Relationship Invalid Relationship Type Field - Missing" issue to be reported. + /// + [Fact] + public void SpdxRelationship_Validate_MissingRelationshipType_ReportsIssue() { // Arrange: Create a bad relationship var relationship = new SpdxRelationship() @@ -255,154 +390,205 @@ public void SpdxRelationship_Validate_MissingRelationship() relationship.Validate(issues, null); // Assert: Verify that the validation fails and the error message includes the description - Assert.Contains(issue => issue.Contains("Relationship Invalid Relationship Type Field - Missing"), issues); + Assert.Contains(issues, issue => issue.Contains("Relationship Invalid Relationship Type Field - Missing")); } /// /// Tests the method for the "CONTAINS" relationship /// type. /// - [TestMethod] - public void SpdxRelationshipTypeExtensions_FromText_Valid() + /// + /// Verifies that all 45 recognized SPDX relationship type tokens (including case-insensitive variants) are + /// correctly parsed to their corresponding enum values, and that an empty + /// string maps to . + /// + [Fact] + public void SpdxRelationshipTypeExtensions_FromText_KnownText_ReturnsMappedEnum() { - Assert.AreEqual(SpdxRelationshipType.Missing, SpdxRelationshipTypeExtensions.FromText("")); - Assert.AreEqual(SpdxRelationshipType.Describes, SpdxRelationshipTypeExtensions.FromText("DESCRIBES")); - Assert.AreEqual(SpdxRelationshipType.Describes, SpdxRelationshipTypeExtensions.FromText("describes")); - Assert.AreEqual(SpdxRelationshipType.Describes, SpdxRelationshipTypeExtensions.FromText("Describes")); - Assert.AreEqual(SpdxRelationshipType.DescribedBy, SpdxRelationshipTypeExtensions.FromText("DESCRIBED_BY")); - Assert.AreEqual(SpdxRelationshipType.Contains, SpdxRelationshipTypeExtensions.FromText("CONTAINS")); - Assert.AreEqual(SpdxRelationshipType.ContainedBy, SpdxRelationshipTypeExtensions.FromText("CONTAINED_BY")); - Assert.AreEqual(SpdxRelationshipType.DependsOn, SpdxRelationshipTypeExtensions.FromText("DEPENDS_ON")); - Assert.AreEqual(SpdxRelationshipType.DependencyOf, SpdxRelationshipTypeExtensions.FromText("DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.DependencyManifestOf, + // Arrange: (none required) + + // Act / Assert: Verify all recognized relationship type strings parse to their enum values + Assert.Equal(SpdxRelationshipType.Missing, SpdxRelationshipTypeExtensions.FromText("")); + Assert.Equal(SpdxRelationshipType.Describes, SpdxRelationshipTypeExtensions.FromText("DESCRIBES")); + Assert.Equal(SpdxRelationshipType.Describes, SpdxRelationshipTypeExtensions.FromText("describes")); + Assert.Equal(SpdxRelationshipType.Describes, SpdxRelationshipTypeExtensions.FromText("Describes")); + Assert.Equal(SpdxRelationshipType.DescribedBy, SpdxRelationshipTypeExtensions.FromText("DESCRIBED_BY")); + Assert.Equal(SpdxRelationshipType.Contains, SpdxRelationshipTypeExtensions.FromText("CONTAINS")); + Assert.Equal(SpdxRelationshipType.ContainedBy, SpdxRelationshipTypeExtensions.FromText("CONTAINED_BY")); + Assert.Equal(SpdxRelationshipType.DependsOn, SpdxRelationshipTypeExtensions.FromText("DEPENDS_ON")); + Assert.Equal(SpdxRelationshipType.DependencyOf, SpdxRelationshipTypeExtensions.FromText("DEPENDENCY_OF")); + Assert.Equal(SpdxRelationshipType.DependencyManifestOf, SpdxRelationshipTypeExtensions.FromText("DEPENDENCY_MANIFEST_OF")); - Assert.AreEqual(SpdxRelationshipType.BuildDependencyOf, + Assert.Equal(SpdxRelationshipType.BuildDependencyOf, SpdxRelationshipTypeExtensions.FromText("BUILD_DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.DevDependencyOf, + Assert.Equal(SpdxRelationshipType.DevDependencyOf, SpdxRelationshipTypeExtensions.FromText("DEV_DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.OptionalDependencyOf, + Assert.Equal(SpdxRelationshipType.OptionalDependencyOf, SpdxRelationshipTypeExtensions.FromText("OPTIONAL_DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.ProvidedDependencyOf, + Assert.Equal(SpdxRelationshipType.ProvidedDependencyOf, SpdxRelationshipTypeExtensions.FromText("PROVIDED_DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.TestDependencyOf, + Assert.Equal(SpdxRelationshipType.TestDependencyOf, SpdxRelationshipTypeExtensions.FromText("TEST_DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.RuntimeDependencyOf, + Assert.Equal(SpdxRelationshipType.RuntimeDependencyOf, SpdxRelationshipTypeExtensions.FromText("RUNTIME_DEPENDENCY_OF")); - Assert.AreEqual(SpdxRelationshipType.ExampleOf, SpdxRelationshipTypeExtensions.FromText("EXAMPLE_OF")); - Assert.AreEqual(SpdxRelationshipType.Generates, SpdxRelationshipTypeExtensions.FromText("GENERATES")); - Assert.AreEqual(SpdxRelationshipType.GeneratedFrom, SpdxRelationshipTypeExtensions.FromText("GENERATED_FROM")); - Assert.AreEqual(SpdxRelationshipType.AncestorOf, SpdxRelationshipTypeExtensions.FromText("ANCESTOR_OF")); - Assert.AreEqual(SpdxRelationshipType.DescendantOf, SpdxRelationshipTypeExtensions.FromText("DESCENDANT_OF")); - Assert.AreEqual(SpdxRelationshipType.VariantOf, SpdxRelationshipTypeExtensions.FromText("VARIANT_OF")); - Assert.AreEqual(SpdxRelationshipType.DistributionArtifact, + Assert.Equal(SpdxRelationshipType.ExampleOf, SpdxRelationshipTypeExtensions.FromText("EXAMPLE_OF")); + Assert.Equal(SpdxRelationshipType.Generates, SpdxRelationshipTypeExtensions.FromText("GENERATES")); + Assert.Equal(SpdxRelationshipType.GeneratedFrom, SpdxRelationshipTypeExtensions.FromText("GENERATED_FROM")); + Assert.Equal(SpdxRelationshipType.AncestorOf, SpdxRelationshipTypeExtensions.FromText("ANCESTOR_OF")); + Assert.Equal(SpdxRelationshipType.DescendantOf, SpdxRelationshipTypeExtensions.FromText("DESCENDANT_OF")); + Assert.Equal(SpdxRelationshipType.VariantOf, SpdxRelationshipTypeExtensions.FromText("VARIANT_OF")); + Assert.Equal(SpdxRelationshipType.DistributionArtifact, SpdxRelationshipTypeExtensions.FromText("DISTRIBUTION_ARTIFACT")); - Assert.AreEqual(SpdxRelationshipType.PatchFor, SpdxRelationshipTypeExtensions.FromText("PATCH_FOR")); - Assert.AreEqual(SpdxRelationshipType.PatchApplied, SpdxRelationshipTypeExtensions.FromText("PATCH_APPLIED")); - Assert.AreEqual(SpdxRelationshipType.CopyOf, SpdxRelationshipTypeExtensions.FromText("COPY_OF")); - Assert.AreEqual(SpdxRelationshipType.FileAdded, SpdxRelationshipTypeExtensions.FromText("FILE_ADDED")); - Assert.AreEqual(SpdxRelationshipType.FileDeleted, SpdxRelationshipTypeExtensions.FromText("FILE_DELETED")); - Assert.AreEqual(SpdxRelationshipType.FileModified, SpdxRelationshipTypeExtensions.FromText("FILE_MODIFIED")); - Assert.AreEqual(SpdxRelationshipType.ExpandedFromArchive, + Assert.Equal(SpdxRelationshipType.PatchFor, SpdxRelationshipTypeExtensions.FromText("PATCH_FOR")); + Assert.Equal(SpdxRelationshipType.PatchApplied, SpdxRelationshipTypeExtensions.FromText("PATCH_APPLIED")); + Assert.Equal(SpdxRelationshipType.CopyOf, SpdxRelationshipTypeExtensions.FromText("COPY_OF")); + Assert.Equal(SpdxRelationshipType.FileAdded, SpdxRelationshipTypeExtensions.FromText("FILE_ADDED")); + Assert.Equal(SpdxRelationshipType.FileDeleted, SpdxRelationshipTypeExtensions.FromText("FILE_DELETED")); + Assert.Equal(SpdxRelationshipType.FileModified, SpdxRelationshipTypeExtensions.FromText("FILE_MODIFIED")); + Assert.Equal(SpdxRelationshipType.ExpandedFromArchive, SpdxRelationshipTypeExtensions.FromText("EXPANDED_FROM_ARCHIVE")); - Assert.AreEqual(SpdxRelationshipType.DynamicLink, SpdxRelationshipTypeExtensions.FromText("DYNAMIC_LINK")); - Assert.AreEqual(SpdxRelationshipType.StaticLink, SpdxRelationshipTypeExtensions.FromText("STATIC_LINK")); - Assert.AreEqual(SpdxRelationshipType.DataFileOf, SpdxRelationshipTypeExtensions.FromText("DATA_FILE_OF")); - Assert.AreEqual(SpdxRelationshipType.TestCaseOf, SpdxRelationshipTypeExtensions.FromText("TEST_CASE_OF")); - Assert.AreEqual(SpdxRelationshipType.BuildToolOf, SpdxRelationshipTypeExtensions.FromText("BUILD_TOOL_OF")); - Assert.AreEqual(SpdxRelationshipType.DevToolOf, SpdxRelationshipTypeExtensions.FromText("DEV_TOOL_OF")); - Assert.AreEqual(SpdxRelationshipType.TestOf, SpdxRelationshipTypeExtensions.FromText("TEST_OF")); - Assert.AreEqual(SpdxRelationshipType.TestToolOf, SpdxRelationshipTypeExtensions.FromText("TEST_TOOL_OF")); - Assert.AreEqual(SpdxRelationshipType.DocumentationOf, + Assert.Equal(SpdxRelationshipType.DynamicLink, SpdxRelationshipTypeExtensions.FromText("DYNAMIC_LINK")); + Assert.Equal(SpdxRelationshipType.StaticLink, SpdxRelationshipTypeExtensions.FromText("STATIC_LINK")); + Assert.Equal(SpdxRelationshipType.DataFileOf, SpdxRelationshipTypeExtensions.FromText("DATA_FILE_OF")); + Assert.Equal(SpdxRelationshipType.TestCaseOf, SpdxRelationshipTypeExtensions.FromText("TEST_CASE_OF")); + Assert.Equal(SpdxRelationshipType.BuildToolOf, SpdxRelationshipTypeExtensions.FromText("BUILD_TOOL_OF")); + Assert.Equal(SpdxRelationshipType.DevToolOf, SpdxRelationshipTypeExtensions.FromText("DEV_TOOL_OF")); + Assert.Equal(SpdxRelationshipType.TestOf, SpdxRelationshipTypeExtensions.FromText("TEST_OF")); + Assert.Equal(SpdxRelationshipType.TestToolOf, SpdxRelationshipTypeExtensions.FromText("TEST_TOOL_OF")); + Assert.Equal(SpdxRelationshipType.DocumentationOf, SpdxRelationshipTypeExtensions.FromText("DOCUMENTATION_OF")); - Assert.AreEqual(SpdxRelationshipType.OptionalComponentOf, + Assert.Equal(SpdxRelationshipType.OptionalComponentOf, SpdxRelationshipTypeExtensions.FromText("OPTIONAL_COMPONENT_OF")); - Assert.AreEqual(SpdxRelationshipType.MetafileOf, SpdxRelationshipTypeExtensions.FromText("METAFILE_OF")); - Assert.AreEqual(SpdxRelationshipType.PackageOf, SpdxRelationshipTypeExtensions.FromText("PACKAGE_OF")); - Assert.AreEqual(SpdxRelationshipType.Amends, SpdxRelationshipTypeExtensions.FromText("AMENDS")); - Assert.AreEqual(SpdxRelationshipType.PrerequisiteFor, + Assert.Equal(SpdxRelationshipType.MetafileOf, SpdxRelationshipTypeExtensions.FromText("METAFILE_OF")); + Assert.Equal(SpdxRelationshipType.PackageOf, SpdxRelationshipTypeExtensions.FromText("PACKAGE_OF")); + Assert.Equal(SpdxRelationshipType.Amends, SpdxRelationshipTypeExtensions.FromText("AMENDS")); + Assert.Equal(SpdxRelationshipType.PrerequisiteFor, SpdxRelationshipTypeExtensions.FromText("PREREQUISITE_FOR")); - Assert.AreEqual(SpdxRelationshipType.HasPrerequisite, + Assert.Equal(SpdxRelationshipType.HasPrerequisite, SpdxRelationshipTypeExtensions.FromText("HAS_PREREQUISITE")); - Assert.AreEqual(SpdxRelationshipType.RequirementDescriptionFor, + Assert.Equal(SpdxRelationshipType.RequirementDescriptionFor, SpdxRelationshipTypeExtensions.FromText("REQUIREMENT_DESCRIPTION_FOR")); - Assert.AreEqual(SpdxRelationshipType.SpecificationFor, + Assert.Equal(SpdxRelationshipType.SpecificationFor, SpdxRelationshipTypeExtensions.FromText("SPECIFICATION_FOR")); - Assert.AreEqual(SpdxRelationshipType.Other, SpdxRelationshipTypeExtensions.FromText("OTHER")); + Assert.Equal(SpdxRelationshipType.Other, SpdxRelationshipTypeExtensions.FromText("OTHER")); } /// /// Tests the method for an invalid relationship type. /// - [TestMethod] - public void SpdxRelationshipTypeExtensions_FromText_Invalid() + /// + /// Verifies that an unrecognized relationship type string throws an + /// with a descriptive error message. + /// + [Fact] + public void SpdxRelationshipTypeExtensions_FromText_UnknownText_ThrowsInvalidOperationException() { + // Arrange: (none required) + + // Act / Assert: Verify that an unknown type throws InvalidOperationException var exception = - Assert.ThrowsExactly(() => SpdxRelationshipTypeExtensions.FromText("invalid")); - Assert.AreEqual("Unsupported SPDX Relationship Type 'invalid'", exception.Message); + Assert.Throws(() => SpdxRelationshipTypeExtensions.FromText("invalid")); + Assert.Equal("Unsupported SPDX Relationship Type 'invalid'", exception.Message); } /// /// Tests the method for the "CONTAINS" /// relationship type. /// - [TestMethod] - public void SpdxRelationshipTypeExtensions_ToText_Valid() + /// + /// Verifies that all 45 recognized enum values are correctly serialized to + /// their canonical SPDX text representations (uppercase, underscore-separated tokens). + /// + [Fact] + public void SpdxRelationshipTypeExtensions_ToText_KnownEnum_ReturnsMappedText() { - Assert.AreEqual("DESCRIBES", SpdxRelationshipType.Describes.ToText()); - Assert.AreEqual("DESCRIBED_BY", SpdxRelationshipType.DescribedBy.ToText()); - Assert.AreEqual("CONTAINS", SpdxRelationshipType.Contains.ToText()); - Assert.AreEqual("CONTAINED_BY", SpdxRelationshipType.ContainedBy.ToText()); - Assert.AreEqual("DEPENDS_ON", SpdxRelationshipType.DependsOn.ToText()); - Assert.AreEqual("DEPENDENCY_OF", SpdxRelationshipType.DependencyOf.ToText()); - Assert.AreEqual("DEPENDENCY_MANIFEST_OF", SpdxRelationshipType.DependencyManifestOf.ToText()); - Assert.AreEqual("BUILD_DEPENDENCY_OF", SpdxRelationshipType.BuildDependencyOf.ToText()); - Assert.AreEqual("DEV_DEPENDENCY_OF", SpdxRelationshipType.DevDependencyOf.ToText()); - Assert.AreEqual("OPTIONAL_DEPENDENCY_OF", SpdxRelationshipType.OptionalDependencyOf.ToText()); - Assert.AreEqual("PROVIDED_DEPENDENCY_OF", SpdxRelationshipType.ProvidedDependencyOf.ToText()); - Assert.AreEqual("TEST_DEPENDENCY_OF", SpdxRelationshipType.TestDependencyOf.ToText()); - Assert.AreEqual("RUNTIME_DEPENDENCY_OF", SpdxRelationshipType.RuntimeDependencyOf.ToText()); - Assert.AreEqual("EXAMPLE_OF", SpdxRelationshipType.ExampleOf.ToText()); - Assert.AreEqual("GENERATES", SpdxRelationshipType.Generates.ToText()); - Assert.AreEqual("GENERATED_FROM", SpdxRelationshipType.GeneratedFrom.ToText()); - Assert.AreEqual("ANCESTOR_OF", SpdxRelationshipType.AncestorOf.ToText()); - Assert.AreEqual("DESCENDANT_OF", SpdxRelationshipType.DescendantOf.ToText()); - Assert.AreEqual("VARIANT_OF", SpdxRelationshipType.VariantOf.ToText()); - Assert.AreEqual("DISTRIBUTION_ARTIFACT", SpdxRelationshipType.DistributionArtifact.ToText()); - Assert.AreEqual("PATCH_FOR", SpdxRelationshipType.PatchFor.ToText()); - Assert.AreEqual("PATCH_APPLIED", SpdxRelationshipType.PatchApplied.ToText()); - Assert.AreEqual("COPY_OF", SpdxRelationshipType.CopyOf.ToText()); - Assert.AreEqual("FILE_ADDED", SpdxRelationshipType.FileAdded.ToText()); - Assert.AreEqual("FILE_DELETED", SpdxRelationshipType.FileDeleted.ToText()); - Assert.AreEqual("FILE_MODIFIED", SpdxRelationshipType.FileModified.ToText()); - Assert.AreEqual("EXPANDED_FROM_ARCHIVE", SpdxRelationshipType.ExpandedFromArchive.ToText()); - Assert.AreEqual("DYNAMIC_LINK", SpdxRelationshipType.DynamicLink.ToText()); - Assert.AreEqual("STATIC_LINK", SpdxRelationshipType.StaticLink.ToText()); - Assert.AreEqual("DATA_FILE_OF", SpdxRelationshipType.DataFileOf.ToText()); - Assert.AreEqual("TEST_CASE_OF", SpdxRelationshipType.TestCaseOf.ToText()); - Assert.AreEqual("BUILD_TOOL_OF", SpdxRelationshipType.BuildToolOf.ToText()); - Assert.AreEqual("DEV_TOOL_OF", SpdxRelationshipType.DevToolOf.ToText()); - Assert.AreEqual("TEST_OF", SpdxRelationshipType.TestOf.ToText()); - Assert.AreEqual("TEST_TOOL_OF", SpdxRelationshipType.TestToolOf.ToText()); - Assert.AreEqual("DOCUMENTATION_OF", SpdxRelationshipType.DocumentationOf.ToText()); - Assert.AreEqual("OPTIONAL_COMPONENT_OF", SpdxRelationshipType.OptionalComponentOf.ToText()); - Assert.AreEqual("METAFILE_OF", SpdxRelationshipType.MetafileOf.ToText()); - Assert.AreEqual("PACKAGE_OF", SpdxRelationshipType.PackageOf.ToText()); - Assert.AreEqual("AMENDS", SpdxRelationshipType.Amends.ToText()); - Assert.AreEqual("PREREQUISITE_FOR", SpdxRelationshipType.PrerequisiteFor.ToText()); - Assert.AreEqual("HAS_PREREQUISITE", SpdxRelationshipType.HasPrerequisite.ToText()); - Assert.AreEqual("REQUIREMENT_DESCRIPTION_FOR", SpdxRelationshipType.RequirementDescriptionFor.ToText()); - Assert.AreEqual("SPECIFICATION_FOR", SpdxRelationshipType.SpecificationFor.ToText()); - Assert.AreEqual("OTHER", SpdxRelationshipType.Other.ToText()); + // Arrange: (none required) + + // Act / Assert: Verify all relationship type enum values serialize to their SPDX text representations + Assert.Equal("DESCRIBES", SpdxRelationshipType.Describes.ToText()); + Assert.Equal("DESCRIBED_BY", SpdxRelationshipType.DescribedBy.ToText()); + Assert.Equal("CONTAINS", SpdxRelationshipType.Contains.ToText()); + Assert.Equal("CONTAINED_BY", SpdxRelationshipType.ContainedBy.ToText()); + Assert.Equal("DEPENDS_ON", SpdxRelationshipType.DependsOn.ToText()); + Assert.Equal("DEPENDENCY_OF", SpdxRelationshipType.DependencyOf.ToText()); + Assert.Equal("DEPENDENCY_MANIFEST_OF", SpdxRelationshipType.DependencyManifestOf.ToText()); + Assert.Equal("BUILD_DEPENDENCY_OF", SpdxRelationshipType.BuildDependencyOf.ToText()); + Assert.Equal("DEV_DEPENDENCY_OF", SpdxRelationshipType.DevDependencyOf.ToText()); + Assert.Equal("OPTIONAL_DEPENDENCY_OF", SpdxRelationshipType.OptionalDependencyOf.ToText()); + Assert.Equal("PROVIDED_DEPENDENCY_OF", SpdxRelationshipType.ProvidedDependencyOf.ToText()); + Assert.Equal("TEST_DEPENDENCY_OF", SpdxRelationshipType.TestDependencyOf.ToText()); + Assert.Equal("RUNTIME_DEPENDENCY_OF", SpdxRelationshipType.RuntimeDependencyOf.ToText()); + Assert.Equal("EXAMPLE_OF", SpdxRelationshipType.ExampleOf.ToText()); + Assert.Equal("GENERATES", SpdxRelationshipType.Generates.ToText()); + Assert.Equal("GENERATED_FROM", SpdxRelationshipType.GeneratedFrom.ToText()); + Assert.Equal("ANCESTOR_OF", SpdxRelationshipType.AncestorOf.ToText()); + Assert.Equal("DESCENDANT_OF", SpdxRelationshipType.DescendantOf.ToText()); + Assert.Equal("VARIANT_OF", SpdxRelationshipType.VariantOf.ToText()); + Assert.Equal("DISTRIBUTION_ARTIFACT", SpdxRelationshipType.DistributionArtifact.ToText()); + Assert.Equal("PATCH_FOR", SpdxRelationshipType.PatchFor.ToText()); + Assert.Equal("PATCH_APPLIED", SpdxRelationshipType.PatchApplied.ToText()); + Assert.Equal("COPY_OF", SpdxRelationshipType.CopyOf.ToText()); + Assert.Equal("FILE_ADDED", SpdxRelationshipType.FileAdded.ToText()); + Assert.Equal("FILE_DELETED", SpdxRelationshipType.FileDeleted.ToText()); + Assert.Equal("FILE_MODIFIED", SpdxRelationshipType.FileModified.ToText()); + Assert.Equal("EXPANDED_FROM_ARCHIVE", SpdxRelationshipType.ExpandedFromArchive.ToText()); + Assert.Equal("DYNAMIC_LINK", SpdxRelationshipType.DynamicLink.ToText()); + Assert.Equal("STATIC_LINK", SpdxRelationshipType.StaticLink.ToText()); + Assert.Equal("DATA_FILE_OF", SpdxRelationshipType.DataFileOf.ToText()); + Assert.Equal("TEST_CASE_OF", SpdxRelationshipType.TestCaseOf.ToText()); + Assert.Equal("BUILD_TOOL_OF", SpdxRelationshipType.BuildToolOf.ToText()); + Assert.Equal("DEV_TOOL_OF", SpdxRelationshipType.DevToolOf.ToText()); + Assert.Equal("TEST_OF", SpdxRelationshipType.TestOf.ToText()); + Assert.Equal("TEST_TOOL_OF", SpdxRelationshipType.TestToolOf.ToText()); + Assert.Equal("DOCUMENTATION_OF", SpdxRelationshipType.DocumentationOf.ToText()); + Assert.Equal("OPTIONAL_COMPONENT_OF", SpdxRelationshipType.OptionalComponentOf.ToText()); + Assert.Equal("METAFILE_OF", SpdxRelationshipType.MetafileOf.ToText()); + Assert.Equal("PACKAGE_OF", SpdxRelationshipType.PackageOf.ToText()); + Assert.Equal("AMENDS", SpdxRelationshipType.Amends.ToText()); + Assert.Equal("PREREQUISITE_FOR", SpdxRelationshipType.PrerequisiteFor.ToText()); + Assert.Equal("HAS_PREREQUISITE", SpdxRelationshipType.HasPrerequisite.ToText()); + Assert.Equal("REQUIREMENT_DESCRIPTION_FOR", SpdxRelationshipType.RequirementDescriptionFor.ToText()); + Assert.Equal("SPECIFICATION_FOR", SpdxRelationshipType.SpecificationFor.ToText()); + Assert.Equal("OTHER", SpdxRelationshipType.Other.ToText()); + } + + /// + /// Tests the method for the + /// sentinel value. + /// + /// + /// Verifies that attempting to serialize the sentinel throws an + /// with a descriptive error message, since the sentinel is not a valid + /// SPDX relationship type token. + /// + [Fact] + public void SpdxRelationshipTypeExtensions_ToText_MissingSentinel_ThrowsInvalidOperationException() + { + // Arrange: (none required) + + // Act: Attempt to convert the Missing sentinel to text + var exception = Assert.Throws(() => SpdxRelationshipType.Missing.ToText()); + + // Assert: Verify the exception has the expected message + Assert.Equal("Attempt to serialize missing SPDX Relationship Type", exception.Message); } /// /// Tests the method for an invalid /// relationship type. /// - [TestMethod] - public void SpdxRelationshipTypeExtensions_ToText_Invalid() + /// + /// Verifies that an out-of-range value (including the + /// sentinel) throws an + /// with a descriptive error message. + /// + [Fact] + public void SpdxRelationshipTypeExtensions_ToText_UnknownEnum_ThrowsInvalidOperationException() { - var exception = Assert.ThrowsExactly(() => ((SpdxRelationshipType)1000).ToText()); - Assert.AreEqual("Unsupported SPDX Relationship Type '1000'", exception.Message); + // Arrange: (none required) + + // Act / Assert: Verify that an unknown type throws InvalidOperationException + var exception = Assert.Throws(() => ((SpdxRelationshipType)1000).ToText()); + Assert.Equal("Unsupported SPDX Relationship Type '1000'", exception.Message); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/SpdxSnippetTests.cs b/test/DemaConsulting.SpdxModel.Tests/SpdxSnippetTests.cs index 9827482..aa4e593 100644 --- a/test/DemaConsulting.SpdxModel.Tests/SpdxSnippetTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/SpdxSnippetTests.cs @@ -23,15 +23,26 @@ namespace DemaConsulting.SpdxModel.Tests; /// /// Tests for the class. /// -[TestClass] +/// +/// Covers equality comparison via the comparer, deep-copy independence, +/// field merging via , and full +/// validation of required fields and byte range constraints. Each test exercises a single scenario or +/// boundary condition in isolation with no shared state between tests. +/// public class SpdxSnippetTests { /// /// Tests the comparer compares snippets correctly. /// - [TestMethod] - public void SpdxSnippet_SameComparer_ComparesCorrectly() + /// + /// Verifies that two snippets with the same SnippetFromFile, SnippetByteStart, and + /// SnippetByteEnd are considered equal even when other fields differ, and that snippets + /// with different file or byte range are distinct. + /// + [Fact] + public void SpdxSnippet_SameComparer_SameFileAndByteRange_ReturnsEqual() { + // Arrange: Create two snippets with the same byte range and one distinct snippet var s1 = new SpdxSnippet { SnippetFromFile = "SPDXRef-File1", @@ -55,60 +66,98 @@ public void SpdxSnippet_SameComparer_ComparesCorrectly() SnippetByteEnd = 40 }; - // Assert snippets compare to themselves - Assert.IsTrue(SpdxSnippet.Same.Equals(s1, s1)); - Assert.IsTrue(SpdxSnippet.Same.Equals(s2, s2)); - Assert.IsTrue(SpdxSnippet.Same.Equals(s3, s3)); - - // Assert snippets compare correctly - Assert.IsTrue(SpdxSnippet.Same.Equals(s1, s2)); - Assert.IsTrue(SpdxSnippet.Same.Equals(s2, s1)); - Assert.IsFalse(SpdxSnippet.Same.Equals(s1, s3)); - Assert.IsFalse(SpdxSnippet.Same.Equals(s3, s1)); - Assert.IsFalse(SpdxSnippet.Same.Equals(s2, s3)); - Assert.IsFalse(SpdxSnippet.Same.Equals(s3, s2)); - - // Assert same snippets have identical hashes - Assert.AreEqual(SpdxSnippet.Same.GetHashCode(s1), SpdxSnippet.Same.GetHashCode(s2)); + // Act / Assert: Verify snippets compare to themselves + Assert.True(SpdxSnippet.Same.Equals(s1, s1)); + Assert.True(SpdxSnippet.Same.Equals(s2, s2)); + Assert.True(SpdxSnippet.Same.Equals(s3, s3)); + + // Assert: snippets compare correctly + Assert.True(SpdxSnippet.Same.Equals(s1, s2)); + Assert.True(SpdxSnippet.Same.Equals(s2, s1)); + Assert.False(SpdxSnippet.Same.Equals(s1, s3)); + Assert.False(SpdxSnippet.Same.Equals(s3, s1)); + Assert.False(SpdxSnippet.Same.Equals(s2, s3)); + Assert.False(SpdxSnippet.Same.Equals(s3, s2)); + + // Assert: same snippets have identical hashes + Assert.Equal(SpdxSnippet.Same.GetHashCode(s1), SpdxSnippet.Same.GetHashCode(s2)); } /// /// Tests the method successfully creates a deep copy. /// - [TestMethod] - public void SpdxSnippet_DeepCopy_CreatesEqualButDistinctInstance() + /// + /// Verifies that the returned instance has equal field values for all properties including + /// array fields, is a distinct object reference from the original, and that all array fields + /// are independent instances so that mutating the copy does not affect the original. + /// + [Fact] + public void SpdxSnippet_DeepCopy_FullyPopulatedSnippet_CreatesEqualButDistinctCopy() { - // Arrange: Create a SpdxSnippet instance with various properties + // Arrange: Create a fully-populated SpdxSnippet instance with all fields set var s1 = new SpdxSnippet { + Id = "SPDXRef-Snippet", SnippetFromFile = "SPDXRef-File1", SnippetByteStart = 100, SnippetByteEnd = 200, + SnippetLineStart = 5, + SnippetLineEnd = 10, + ConcludedLicense = "MIT", + LicenseInfoInSnippet = ["MIT", "Apache-2.0"], + LicenseComments = "License comment", + CopyrightText = "Copyright(c) 2024 DEMA Consulting", Comment = "Found snippet", - ConcludedLicense = "MIT" + Name = "MySnippet", + AttributionText = ["Attribution text"], + Annotations = + [ + new SpdxAnnotation + { + Annotator = "Tool: test-tool", + Date = "2024-05-28T01:30:00Z", + Type = SpdxAnnotationType.Review, + Comment = "Reviewed" + } + ] }; // Act: Create a deep copy of the SpdxSnippet instance var s2 = s1.DeepCopy(); - // Assert: Verify the deep-copy is equal to the original - Assert.AreEqual(s1, s2, SpdxSnippet.Same); - Assert.AreEqual(s1.SnippetFromFile, s2.SnippetFromFile); - Assert.AreEqual(s1.SnippetByteStart, s2.SnippetByteStart); - Assert.AreEqual(s1.SnippetByteEnd, s2.SnippetByteEnd); - Assert.AreEqual(s1.Comment, s2.Comment); - Assert.AreEqual(s1.ConcludedLicense, s2.ConcludedLicense); + // Assert: Verify the deep-copy has equal field values to the original + Assert.Equal(s1, s2, SpdxSnippet.Same); + Assert.Equal(s1.Id, s2.Id); + Assert.Equal(s1.SnippetFromFile, s2.SnippetFromFile); + Assert.Equal(s1.SnippetByteStart, s2.SnippetByteStart); + Assert.Equal(s1.SnippetByteEnd, s2.SnippetByteEnd); + Assert.Equal(s1.SnippetLineStart, s2.SnippetLineStart); + Assert.Equal(s1.SnippetLineEnd, s2.SnippetLineEnd); + Assert.Equal(s1.ConcludedLicense, s2.ConcludedLicense); + Assert.Equal(s1.LicenseInfoInSnippet, s2.LicenseInfoInSnippet); + Assert.Equal(s1.LicenseComments, s2.LicenseComments); + Assert.Equal(s1.CopyrightText, s2.CopyrightText); + Assert.Equal(s1.Comment, s2.Comment); + Assert.Equal(s1.Name, s2.Name); + Assert.Equal(s1.AttributionText, s2.AttributionText); - // Assert: Verify the deep-copy is a distinct instance - Assert.IsFalse(ReferenceEquals(s1, s2)); + // Assert: Verify the deep-copy is a distinct instance with independent array references + Assert.False(ReferenceEquals(s1, s2)); + Assert.False(ReferenceEquals(s1.LicenseInfoInSnippet, s2.LicenseInfoInSnippet)); + Assert.False(ReferenceEquals(s1.AttributionText, s2.AttributionText)); + Assert.False(ReferenceEquals(s1.Annotations, s2.Annotations)); } /// /// Tests the method adds or updates information /// correctly. /// - [TestMethod] - public void SpdxSnippet_Enhance_AddsOrUpdatesInformationCorrectly() + /// + /// Verifies that a matching snippet (same file and byte range) is enhanced in place and that a non-matching + /// snippet from the source array is deep-copied and appended, resulting in an array of length two. + /// + [Fact] + public void SpdxSnippet_Enhance_MatchingAndNewSnippets_MergesCorrectly() { // Arrange: Create an array of SpdxSnippet objects var snippets = new[] @@ -142,22 +191,26 @@ public void SpdxSnippet_Enhance_AddsOrUpdatesInformationCorrectly() ]); // Assert: Check that the snippets array has been enhanced correctly - Assert.HasCount(2, snippets); - Assert.AreEqual("SPDXRef-File1", snippets[0].SnippetFromFile); - Assert.AreEqual(100, snippets[0].SnippetByteStart); - Assert.AreEqual(200, snippets[0].SnippetByteEnd); - Assert.AreEqual("Found snippet", snippets[0].Comment); - Assert.AreEqual("MIT", snippets[0].ConcludedLicense); - Assert.AreEqual("SPDXRef-File2", snippets[1].SnippetFromFile); - Assert.AreEqual(10, snippets[1].SnippetByteStart); - Assert.AreEqual(40, snippets[1].SnippetByteEnd); + Assert.Equal(2, snippets.Length); + Assert.Equal("SPDXRef-File1", snippets[0].SnippetFromFile); + Assert.Equal(100, snippets[0].SnippetByteStart); + Assert.Equal(200, snippets[0].SnippetByteEnd); + Assert.Equal("Found snippet", snippets[0].Comment); + Assert.Equal("MIT", snippets[0].ConcludedLicense); + Assert.Equal("SPDXRef-File2", snippets[1].SnippetFromFile); + Assert.Equal(10, snippets[1].SnippetByteStart); + Assert.Equal(40, snippets[1].SnippetByteEnd); } /// /// Tests that an invalid snippet ID fails validation. /// - [TestMethod] - public void SpdxSnippet_Validate_ReportsInvalidSnippetId() + /// + /// Verifies that an Id not matching the SPDXRef- prefix format causes the + /// "Snippet Invalid SPDX Identifier Field" issue to be reported. + /// + [Fact] + public void SpdxSnippet_Validate_InvalidSnippetId_ReportsIssue() { // Arrange: Create a SpdxSnippet with an invalid ID var snippet = new SpdxSnippet @@ -165,7 +218,9 @@ public void SpdxSnippet_Validate_ReportsInvalidSnippetId() Id = "Invalid_ID", SnippetFromFile = "SPDXRef-File1", SnippetByteStart = 100, - SnippetByteEnd = 200 + SnippetByteEnd = 200, + ConcludedLicense = "MIT", + CopyrightText = "Copyright(c) 2024 DEMA Consulting" }; // Act: Validate the snippet @@ -173,14 +228,19 @@ public void SpdxSnippet_Validate_ReportsInvalidSnippetId() snippet.Validate(issues); // Assert: Check that the issues list contains the expected error message - Assert.Contains(issue => issue.Contains("Snippet Invalid SPDX Identifier Field 'Invalid_ID'"), issues); + Assert.Contains(issues, issue => issue.Contains("Snippet Invalid SPDX Identifier Field 'Invalid_ID'")); } /// /// Tests that a valid snippet passes validation. /// - [TestMethod] - public void SpdxSnippet_Validate_Success() + /// + /// Exercises the happy-path: a snippet with all required fields populated (valid SPDX ID, + /// non-empty SnippetFromFile, byte range ≥ 1, non-empty license, and copyright) + /// passes all validation checks without reporting any issues. + /// + [Fact] + public void SpdxSnippet_Validate_AllRequiredFieldsPresent_ReturnsNoIssues() { // Arrange: Create a valid SpdxSnippet var snippet = new SpdxSnippet @@ -198,14 +258,18 @@ public void SpdxSnippet_Validate_Success() snippet.Validate(issues); // Assert: Verify that the validation reports no issues. - Assert.IsEmpty(issues); + Assert.Empty(issues); } /// /// Tests the method validates annotations. /// - [TestMethod] - public void SpdxSnippet_Validate_InvalidAnnotation() + /// + /// Verifies that an annotation with an empty Annotator field causes the + /// "Invalid Annotator Field - Empty" issue to be reported with the correct snippet prefix. + /// + [Fact] + public void SpdxSnippet_Validate_InvalidAnnotation_ReportsIssue() { // Arrange: Create a valid snippet with an invalid annotation var snippet = new SpdxSnippet @@ -233,6 +297,151 @@ public void SpdxSnippet_Validate_InvalidAnnotation() snippet.Validate(issues); // Assert: Verify the annotation issue is reported with the correct prefix - Assert.Contains("Snippet 'SPDXRef-Snippet' Invalid Annotator Field - Empty", issues); + Assert.Contains(issues, issue => issue.Contains("Snippet 'SPDXRef-Snippet' Invalid Annotator Field - Empty")); + } + + /// + /// Tests the method reports an empty snippet-from-file field. + /// + /// + /// Verifies the boundary condition where SnippetFromFile is empty: validation must report + /// the "Invalid Snippet From File Field - Empty" issue. + /// + [Fact] + public void SpdxSnippet_Validate_EmptySnippetFromFile_ReportsIssue() + { + // Arrange: Create a snippet with an empty SnippetFromFile + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "", + SnippetByteStart = 100, + SnippetByteEnd = 200, + ConcludedLicense = "MIT", + CopyrightText = "Copyright(c) 2024 DEMA Consulting" + }; + + // Act: Validate the snippet + var issues = new List(); + snippet.Validate(issues); + + // Assert: Verify the empty SnippetFromFile issue is reported + Assert.Contains(issues, issue => issue.Contains("Snippet 'SPDXRef-Snippet' Invalid Snippet From File Field - Empty")); + } + + /// + /// Tests the method reports an invalid byte start value. + /// + /// + /// Verifies the lower boundary condition: a SnippetByteStart value of 0 (less than the + /// required minimum of 1) causes the "Invalid Snippet Byte Range Start Field" issue to be reported. + /// + [Fact] + public void SpdxSnippet_Validate_InvalidByteStart_ReportsIssue() + { + // Arrange: Create a snippet with SnippetByteStart < 1 + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "SPDXRef-File1", + SnippetByteStart = 0, + SnippetByteEnd = 200, + ConcludedLicense = "MIT", + CopyrightText = "Copyright(c) 2024 DEMA Consulting" + }; + + // Act: Validate the snippet + var issues = new List(); + snippet.Validate(issues); + + // Assert: Verify the invalid byte start issue is reported + Assert.Contains(issues, issue => issue.Contains("Snippet 'SPDXRef-Snippet' Invalid Snippet Byte Range Start Field '0'")); + } + + /// + /// Tests the method reports an invalid byte end value. + /// + /// + /// Verifies the range boundary condition: a SnippetByteEnd less than SnippetByteStart + /// causes the "Invalid Snippet Byte Range End Field" issue to be reported. + /// + [Fact] + public void SpdxSnippet_Validate_InvalidByteEnd_ReportsIssue() + { + // Arrange: Create a snippet where SnippetByteEnd is less than SnippetByteStart + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "SPDXRef-File1", + SnippetByteStart = 100, + SnippetByteEnd = 50, + ConcludedLicense = "MIT", + CopyrightText = "Copyright(c) 2024 DEMA Consulting" + }; + + // Act: Validate the snippet + var issues = new List(); + snippet.Validate(issues); + + // Assert: Verify the invalid byte end issue is reported + Assert.Contains(issues, issue => issue.Contains("Snippet 'SPDXRef-Snippet' Invalid Snippet Byte Range End Field '50' < '100'")); + } + + /// + /// Tests the method reports an empty concluded license field. + /// + /// + /// Verifies that an empty ConcludedLicense causes the "Invalid Concluded License Field - Empty" + /// issue to be reported. + /// + [Fact] + public void SpdxSnippet_Validate_EmptyConcludedLicense_ReportsIssue() + { + // Arrange: Create a snippet with an empty ConcludedLicense + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "SPDXRef-File1", + SnippetByteStart = 100, + SnippetByteEnd = 200, + ConcludedLicense = "", + CopyrightText = "Copyright(c) 2024 DEMA Consulting" + }; + + // Act: Validate the snippet + var issues = new List(); + snippet.Validate(issues); + + // Assert: Verify the empty ConcludedLicense issue is reported + Assert.Contains(issues, issue => issue.Contains("Snippet 'SPDXRef-Snippet' Invalid Concluded License Field - Empty")); + } + + /// + /// Tests the method reports an empty copyright text field. + /// + /// + /// Verifies that an empty CopyrightText causes the "Invalid Copyright Text Field - Empty" + /// issue to be reported. + /// + [Fact] + public void SpdxSnippet_Validate_EmptyCopyrightText_ReportsIssue() + { + // Arrange: Create a snippet with an empty CopyrightText + var snippet = new SpdxSnippet + { + Id = "SPDXRef-Snippet", + SnippetFromFile = "SPDXRef-File1", + SnippetByteStart = 100, + SnippetByteEnd = 200, + ConcludedLicense = "MIT", + CopyrightText = "" + }; + + // Act: Validate the snippet + var issues = new List(); + snippet.Validate(issues); + + // Assert: Verify the empty CopyrightText issue is reported + Assert.Contains(issues, issue => issue.Contains("Snippet 'SPDXRef-Snippet' Invalid Copyright Text Field - Empty")); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/TestHelpers.cs b/test/DemaConsulting.SpdxModel.Tests/TestHelpers.cs index 3d01c43..e306cbc 100644 --- a/test/DemaConsulting.SpdxModel.Tests/TestHelpers.cs +++ b/test/DemaConsulting.SpdxModel.Tests/TestHelpers.cs @@ -43,4 +43,27 @@ public static string GetEmbeddedResource(string resourceName) using var reader = new StreamReader(stream); return reader.ReadToEnd().ReplaceLineEndings(); } + + /// + /// Asserts that two collections contain the same elements in any order, + /// using the provided equality comparer for element matching. + /// + public static void AssertEquivalent( + IEnumerable expected, + IEnumerable actual, + IEqualityComparer comparer) + { + var expectedList = expected.ToList(); + var actualList = actual.ToList(); + Assert.Equal(expectedList.Count, actualList.Count); + foreach (var item in expectedList) + { + Assert.Contains(actualList, x => comparer.Equals(x, item)); + } + + foreach (var item in actualList) + { + Assert.Contains(expectedList, x => comparer.Equals(x, item)); + } + } } diff --git a/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxModelTransformTests.cs b/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxModelTransformTests.cs index 1842928..c6dab48 100644 --- a/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxModelTransformTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxModelTransformTests.cs @@ -26,13 +26,22 @@ namespace DemaConsulting.SpdxModel.Tests.Transforms; /// /// Integration tests for the SpdxModel Transform subsystem. /// -[TestClass] +/// +/// Integration-scope tests: each test deserializes a real SPDX 2.3 JSON document from +/// an embedded resource and exercises the transform +/// against that document. xUnit v3 is the test framework. +/// public class SpdxModelTransformTests { /// /// Tests that a relationship added to an SPDX document persists in the document. /// - [TestMethod] + /// + /// Happy path: a well-formed relationship whose source and target both exist in the + /// document is added, the relationship count increases by one, and the document + /// remains valid after the transform. + /// + [Fact] public void SpdxModelTransform_AddRelationship_ToDocument_RelationshipPersists() { // Arrange: Load the SPDX 2.3 JSON example as a real document to transform @@ -52,14 +61,275 @@ public void SpdxModelTransform_AddRelationship_ToDocument_RelationshipPersists() }); // Assert: Verify the relationship was added and the document remains valid - Assert.AreEqual(initialCount + 1, document.Relationships.Length); - Assert.IsTrue(Array.Exists( + Assert.Equal(initialCount + 1, document.Relationships.Length); + Assert.True(Array.Exists( document.Relationships, r => r.Id == "SPDXRef-Package" && r.RelatedSpdxElement == "SPDXRef-fromDoap-0" && r.RelationshipType == SpdxRelationshipType.DependsOn)); var issues = new List(); document.Validate(issues); - Assert.IsEmpty(issues); + Assert.Empty(issues); + } + + /// + /// Tests that adding a relationship with an invalid source element ID throws . + /// + /// + /// Error path: a source element ID that does not exist in the document causes an + /// to be thrown. The document is expected to remain + /// unmodified because the validation happens before any mutation. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_InvalidSourceId_ThrowsArgumentException() + { + // Arrange: Load the SPDX 2.3 JSON example as a real document to transform + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + + // Act / Assert: Adding with a non-existent source ID throws ArgumentException + Assert.Throws(() => + { + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-NonExistent", + RelatedSpdxElement = "SPDXRef-fromDoap-0", + RelationshipType = SpdxRelationshipType.DependsOn + }); + }); + } + + /// + /// Tests that adding a relationship with an invalid target element ID throws . + /// + /// + /// Error path: a target element ID that does not exist in the document, and is neither + /// NOASSERTION nor prefixed with DocumentRef-, causes an + /// to be thrown. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_InvalidTargetId_ThrowsArgumentException() + { + // Arrange: Load the SPDX 2.3 JSON example as a real document to transform + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + + // Act / Assert: Adding with a non-existent target that is neither NOASSERTION nor DocumentRef- throws + Assert.Throws(() => + { + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-NonExistent", + RelationshipType = SpdxRelationshipType.DependsOn + }); + }); + } + + /// + /// Tests that adding a duplicate relationship enhances the existing entry rather than duplicating it. + /// + /// + /// Idempotency path: adding the same relationship a second time merges (enhances) the + /// existing entry rather than appending a duplicate. The relationship count after two + /// adds must equal the count after one add. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_Duplicate_EnhancesExistingRelationship() + { + // Arrange: Load the SPDX 2.3 JSON example and add an initial relationship + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + var initialCount = document.Relationships.Length; + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-fromDoap-0", + RelationshipType = SpdxRelationshipType.DependsOn + }); + + // Act: Add the same relationship again + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-fromDoap-0", + RelationshipType = SpdxRelationshipType.DependsOn + }); + + // Assert: Only one new relationship was added (duplicate was merged, not appended) + Assert.Equal(initialCount + 1, document.Relationships.Length); + } + + /// + /// Tests that the batch Add with replace=true removes pre-existing matching relationships. + /// + /// + /// Replace-mode path: the batch overload with replace: true removes any + /// existing relationships between the same pair of elements before adding the new ones. + /// This allows changing the relationship type between a fixed pair of elements. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_Replace_RemovesPreExistingRelationships() + { + // Arrange: Load the SPDX 2.3 JSON example and add an initial relationship + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-fromDoap-0", + RelationshipType = SpdxRelationshipType.DependsOn + }); + var countAfterFirstAdd = document.Relationships.Length; + + // Act: Replace the relationship with a different type using the batch overload + SpdxRelationships.Add( + document, + [ + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-fromDoap-0", + RelationshipType = SpdxRelationshipType.BuildToolOf + } + ], + replace: true); + + // Assert: The count is unchanged (old removed, new added) and the type changed + Assert.Equal(countAfterFirstAdd, document.Relationships.Length); + Assert.True(Array.Exists( + document.Relationships, + r => r.Id == "SPDXRef-Package" && + r.RelatedSpdxElement == "SPDXRef-fromDoap-0" && + r.RelationshipType == SpdxRelationshipType.BuildToolOf)); + Assert.False(Array.Exists( + document.Relationships, + r => r.Id == "SPDXRef-Package" && + r.RelatedSpdxElement == "SPDXRef-fromDoap-0" && + r.RelationshipType == SpdxRelationshipType.DependsOn)); + } + + /// + /// Tests that the batch Add with multiple relationships adds all of them. + /// + /// + /// Batch path: the batch overload with two distinct relationships adds both in a single + /// call, increasing the relationship count by exactly two. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_BatchMultiple_AddsAllRelationships() + { + // Arrange: Load the SPDX 2.3 JSON example + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + var initialCount = document.Relationships.Length; + + // Act: Add two distinct relationships in a single batch call + SpdxRelationships.Add( + document, + [ + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-fromDoap-0", + RelationshipType = SpdxRelationshipType.DependsOn + }, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "SPDXRef-fromDoap-1", + RelationshipType = SpdxRelationshipType.DependsOn + } + ]); + + // Assert: Both relationships were added + Assert.Equal(initialCount + 2, document.Relationships.Length); + } + + /// + /// Tests that a relationship with NOASSERTION as the target element is accepted as valid. + /// + /// + /// Boundary path: NOASSERTION is a valid target element value (it means the + /// related element is intentionally unspecified). The transform must accept it without + /// throwing, regardless of whether any element with ID "NOASSERTION" exists. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_NoAssertionTarget_AddsRelationship() + { + // Arrange: Load the SPDX 2.3 JSON example + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + var initialCount = document.Relationships.Length; + + // Act: Add a relationship where the target is NOASSERTION + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = SpdxElement.NoAssertion, + RelationshipType = SpdxRelationshipType.DependsOn + }); + + // Assert: Relationship was added without an exception + Assert.Equal(initialCount + 1, document.Relationships.Length); + Assert.True(Array.Exists( + document.Relationships, + r => r.Id == "SPDXRef-Package" && + r.RelatedSpdxElement == SpdxElement.NoAssertion && + r.RelationshipType == SpdxRelationshipType.DependsOn)); + } + + /// + /// Tests that a relationship with a DocumentRef- prefixed target is accepted as valid. + /// + /// + /// Boundary path: a target element prefixed with DocumentRef- refers to an + /// element in an external document. The transform must accept it without throwing, + /// regardless of whether the external document reference resolves locally. + /// + [Fact] + public void SpdxModelTransform_AddRelationship_DocumentRefTarget_AddsRelationship() + { + // Arrange: Load the SPDX 2.3 JSON example + var json = SpdxTestHelpers.GetEmbeddedResource( + "DemaConsulting.SpdxModel.Tests.IO.Examples.SPDXJSONExample-v2.3.spdx.json"); + var document = Spdx2JsonDeserializer.Deserialize(json); + var initialCount = document.Relationships.Length; + + // Act: Add a relationship where the target uses the DocumentRef- prefix + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package", + RelatedSpdxElement = "DocumentRef-spdx-tool-1.2:SPDXRef-Package", + RelationshipType = SpdxRelationshipType.DependsOn + }); + + // Assert: Relationship was added without an exception + Assert.Equal(initialCount + 1, document.Relationships.Length); + Assert.True(Array.Exists( + document.Relationships, + r => r.Id == "SPDXRef-Package" && + r.RelatedSpdxElement == "DocumentRef-spdx-tool-1.2:SPDXRef-Package" && + r.RelationshipType == SpdxRelationshipType.DependsOn)); } } diff --git a/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxRelationshipsTests.cs b/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxRelationshipsTests.cs index 930d7b1..e6b7e26 100644 --- a/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxRelationshipsTests.cs +++ b/test/DemaConsulting.SpdxModel.Tests/Transforms/SpdxRelationshipsTests.cs @@ -4,9 +4,15 @@ namespace DemaConsulting.SpdxModel.Tests.Transforms; /// -/// Tests for the transforms. +/// Tests for the transforms. /// -[TestClass] +/// +/// Uses xUnit v3 as the test framework. +/// Every test deserializes a fresh copy of to prevent +/// inter-test state leakage. The class covers the full scope of +/// operations: adding single and multiple relationships, deduplication, replacement, and +/// atomicity on failure. +/// public class SpdxRelationshipsTests { /// @@ -47,16 +53,22 @@ public class SpdxRelationshipsTests """; /// - /// Tests adding a relationship with a missing ID. + /// Tests that adding a relationship with a missing source ID throws . /// - [TestMethod] - public void SpdxRelationships_AddSingle_MissingId() + /// + /// Verifies the error path where the source element ID does not exist in the document. + /// A fresh document is deserialized to ensure baseline state contains no relationships. + /// Confirms the exception carries the correct parameter name and that the document + /// relationships collection remains empty after the failed call. + /// + [Fact] + public void SpdxRelationships_AddSingle_MissingId_ThrowsArgumentException() { // Arrange: Deserialize the test document contents var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); // Act: Attempt to add a relationship with a non-existent ID - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => { SpdxRelationships.Add( document, @@ -70,21 +82,27 @@ public void SpdxRelationships_AddSingle_MissingId() // Assert: Verify the exception message and that no relationships were added Assert.StartsWith("Element SPDXRef-Package-Missing not found in SPDX document", ex.Message); - Assert.AreEqual("relationship", ex.ParamName); - Assert.IsEmpty(document.Relationships); + Assert.Equal("relationship", ex.ParamName); + Assert.Empty(document.Relationships); } /// - /// Tests adding a relationship with a missing related element. + /// Tests that adding a relationship with a missing related element throws . /// - [TestMethod] - public void SpdxRelationships_AddSingle_MissingRelatedElement() + /// + /// Verifies the error path where the target element ID does not exist in the document. + /// A fresh document is deserialized to ensure baseline state contains no relationships. + /// Confirms the exception carries the correct parameter name and that the document + /// relationships collection remains empty after the failed call. + /// + [Fact] + public void SpdxRelationships_AddSingle_MissingRelatedElement_ThrowsArgumentException() { // Arrange: Deserialize the test document contents var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); // Act: Attempt to add a relationship with a missing related element - var ex = Assert.ThrowsExactly(() => + var ex = Assert.Throws(() => { SpdxRelationships.Add( document, @@ -98,15 +116,20 @@ public void SpdxRelationships_AddSingle_MissingRelatedElement() // Assert: Verify the exception message and that no relationships were added Assert.StartsWith("Element SPDXRef-Package-Missing not found in SPDX document", ex.Message); - Assert.AreEqual("relationship", ex.ParamName); - Assert.IsEmpty(document.Relationships); + Assert.Equal("relationship", ex.ParamName); + Assert.Empty(document.Relationships); } /// - /// Tests adding a relationship. + /// Tests that adding a valid relationship appends it to the document. /// - [TestMethod] - public void SpdxRelationships_AddSingle_Success() + /// + /// Verifies the happy path where both source and target elements exist in the document. + /// A fresh document is deserialized so the initial relationships collection is empty, + /// making the single post-add entry the definitive proof of a successful append. + /// + [Fact] + public void SpdxRelationships_AddSingle_ValidRelationship_AddsRelationship() { // Arrange: Deserialize the test document contents var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); @@ -122,17 +145,23 @@ public void SpdxRelationships_AddSingle_Success() }); // Assert: Verify the relationship was added correctly - Assert.HasCount(1, document.Relationships); - Assert.AreEqual("SPDXRef-Package-1", document.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); } /// - /// Tests adding a duplicate relationship. + /// Tests that adding a duplicate relationship enhances the existing entry rather than duplicating it. /// - [TestMethod] - public void SpdxRelationships_AddSingle_Duplicate() + /// + /// Verifies the deduplication behaviour: a second call with an identical relationship + /// must not create a second entry. A fresh document is deserialized to isolate the + /// count assertion; the final count of one proves that the second add merged into the + /// existing entry rather than appending a new one. + /// + [Fact] + public void SpdxRelationships_AddSingle_DuplicateRelationship_EnhancesExistingRelationship() { // Arrange: Deserialize the test document contents var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); @@ -156,17 +185,86 @@ public void SpdxRelationships_AddSingle_Duplicate() }); // Assert: Verify the relationship was added only once - Assert.HasCount(1, document.Relationships); - Assert.AreEqual("SPDXRef-Package-1", document.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); } /// - /// Tests adding multiple relationships. + /// Tests that a relationship with a NOASSERTION target is accepted as valid. /// - [TestMethod] - public void SpdxRelationships_AddMultiple_Success() + /// + /// Verifies that is treated as a sentinel value + /// that bypasses the element-existence check on the target. A fresh document is + /// deserialized so the single resulting entry proves the add succeeded without requiring + /// the target to be present in the document's element collections. + /// + [Fact] + public void SpdxRelationships_AddSingle_NoAssertionTarget_AddsRelationship() + { + // Arrange: Deserialize the test document contents + var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); + + // Act: Add a relationship where the target is NOASSERTION + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package-1", + RelatedSpdxElement = SpdxElement.NoAssertion, + RelationshipType = SpdxRelationshipType.DependsOn + }); + + // Assert: Verify the relationship was added correctly + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal(SpdxElement.NoAssertion, document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); + } + + /// + /// Tests that a relationship with a DocumentRef- prefixed target is accepted as valid. + /// + /// + /// Verifies that a target ID beginning with DocumentRef- is treated as an + /// external-document reference and bypasses the local element-existence check. A fresh + /// document is deserialized so the single resulting entry proves the add succeeded + /// without requiring the external element to appear in the local document. + /// + [Fact] + public void SpdxRelationships_AddSingle_DocumentRefTarget_AddsRelationship() + { + // Arrange: Deserialize the test document contents + var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); + + // Act: Add a relationship where the target uses the DocumentRef- external-reference prefix + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package-1", + RelatedSpdxElement = "DocumentRef-external:SPDXRef-Package-3", + RelationshipType = SpdxRelationshipType.DependsOn + }); + + // Assert: Verify the relationship was added correctly without requiring the element in the document + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("DocumentRef-external:SPDXRef-Package-3", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); + } + + /// + /// Tests that the batch Add with a single relationship appends it to the document. + /// + /// + /// Verifies the batch overload behaves identically to the single-item overload for a + /// one-element array. A fresh document is deserialized so the count assertion unambiguously + /// reflects the result of the batch call rather than any pre-existing state. + /// + [Fact] + public void SpdxRelationships_AddMultiple_SingleRelationship_AddsRelationship() { // Arrange: Deserialize the test document contents var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); @@ -184,17 +282,22 @@ public void SpdxRelationships_AddMultiple_Success() ]); // Assert: Verify the relationship was added correctly - Assert.HasCount(1, document.Relationships); - Assert.AreEqual("SPDXRef-Package-1", document.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); } /// - /// Tests adding multiple relationships with a duplicate. + /// Tests that adding multiple duplicate relationships deduplicates them. /// - [TestMethod] - public void SpdxRelationships_AddMultiple_Duplicate() + /// + /// Verifies that a batch containing two identical entries results in only one relationship + /// in the document. A fresh document is deserialized so the final count of one is solely + /// the result of the batch call; no pre-existing entries could mask a deduplication failure. + /// + [Fact] + public void SpdxRelationships_AddMultiple_DuplicateRelationships_DeduplicatesRelationships() { // Arrange: Deserialize the test document contents var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); @@ -218,17 +321,23 @@ public void SpdxRelationships_AddMultiple_Duplicate() ]); // Assert: Verify the relationship was added only once - Assert.HasCount(1, document.Relationships); - Assert.AreEqual("SPDXRef-Package-1", document.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); } /// - /// Tests adding multiple relationships with a duplicate and replace. + /// Tests that the batch Add with replace=true removes pre-existing matching relationships. /// - [TestMethod] - public void SpdxRelationships_AddMultiple_Replace() + /// + /// Verifies the replace flag: when replace: true is passed, any pre-existing + /// relationships whose source-target pair matches an entry in the batch are removed + /// before the new relationships are inserted. A fresh document with a single pre-seeded + /// relationship is used so the type change from the replacement is unambiguous. + /// + [Fact] + public void SpdxRelationships_AddMultiple_Replace_RemovesAndReplacesExistingRelationships() { // Arrange: Deserialize the test document contents and add an initial relationship var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); @@ -253,9 +362,62 @@ public void SpdxRelationships_AddMultiple_Replace() true); // Assert: Verify the relationship was replaced with the new type - Assert.HasCount(1, document.Relationships); - Assert.AreEqual("SPDXRef-Package-1", document.Relationships[0].Id); - Assert.AreEqual("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); - Assert.AreEqual(SpdxRelationshipType.BuildToolOf, document.Relationships[0].RelationshipType); + Assert.Single(document.Relationships); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.BuildToolOf, document.Relationships[0].RelationshipType); + } + + /// + /// Tests that a batch Add with replace=true and an invalid relationship leaves the document unmodified. + /// + /// + /// Verifies atomicity: when the batch contains an invalid relationship (missing source ID), + /// the entire operation is rolled back and the document's relationships are unchanged. + /// A pre-seeded relationship is added before the batch call so the assertion on the + /// unchanged collection proves that even the valid first entry in the batch was not committed. + /// + [Fact] + public void SpdxRelationships_AddMultiple_InvalidRelationship_LeavesDocumentUnmodified() + { + // Arrange: Deserialize the test document and add an initial relationship + var document = Spdx2JsonDeserializer.Deserialize(TestDocumentContents); + SpdxRelationships.Add( + document, + new SpdxRelationship + { + Id = "SPDXRef-Package-1", + RelatedSpdxElement = "SPDXRef-Package-2", + RelationshipType = SpdxRelationshipType.DependsOn + }); + var initialRelationships = document.Relationships.ToArray(); + + // Act: Attempt a batch-add with replace=true where the second relationship has an invalid source ID + Assert.Throws(() => + { + SpdxRelationships.Add( + document, + [ + new SpdxRelationship + { + Id = "SPDXRef-Package-1", + RelatedSpdxElement = "SPDXRef-Package-2", + RelationshipType = SpdxRelationshipType.BuildToolOf + }, + new SpdxRelationship + { + Id = "SPDXRef-Package-Missing", + RelatedSpdxElement = "SPDXRef-Package-2", + RelationshipType = SpdxRelationshipType.DependsOn + } + ], + replace: true); + }); + + // Assert: Document relationships are unchanged after the failed batch-add + Assert.Equal(initialRelationships.Length, document.Relationships.Length); + Assert.Equal("SPDXRef-Package-1", document.Relationships[0].Id); + Assert.Equal("SPDXRef-Package-2", document.Relationships[0].RelatedSpdxElement); + Assert.Equal(SpdxRelationshipType.DependsOn, document.Relationships[0].RelationshipType); } }