Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .reviewmark.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,16 @@ reviews:
- "src/**/Modeling/FileAssertJsonAssert.cs" # implementation
- "test/**/Modeling/FileAssertJsonAssertTests.cs" # unit tests

# FileAssert-Modeling-FileAssertZipAssert Review (one per unit)
- id: FileAssert-Modeling-FileAssertZipAssert
title: Review that FileAssert Modeling FileAssertZipAssert Implementation is Correct
paths:
- "docs/reqstream/file-assert/modeling/file-assert-zip-assert.yaml" # requirements
- "docs/design/file-assert/modeling/file-assert-zip-assert.md" # design
- "docs/verification/file-assert/modeling/file-assert-zip-assert.md" # verification
- "src/**/Modeling/FileAssertZipAssert.cs" # implementation
- "test/**/Modeling/FileAssertZipAssertTests.cs" # unit tests

# FileAssert-OTS-BuildMark Review
- id: FileAssert-OTS-BuildMark
title: Review FileAssert OTS BuildMark Requirements and Verification
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ tests:
json:
- query: "ConnectionStrings"
count: 1

- name: TestProject_PackageValid
# Distribution zip contains required entries
tags: [package]
files:
- pattern: "output/package.zip"
zip:
entries:
- pattern: 'lib/net8.0/MyLib.dll'
min: 1
max: 1
- pattern: 'lib/**/*.dll'
min: 1
```

### Acceptance Criteria Reference
Expand Down Expand Up @@ -219,6 +232,10 @@ tests:
| `json[].count` | Exact number of matched JSON nodes |
| `json[].min` | Minimum number of matched JSON nodes |
| `json[].max` | Maximum number of matched JSON nodes |
| `zip:` | Zip archive entry assertions (fails if not a valid zip) |
| `zip.entries[].pattern` | Glob pattern selecting zip archive entry names |
| `zip.entries[].min` | Minimum number of matching zip entries |
| `zip.entries[].max` | Maximum number of matching zip entries |

## Self Validation

Expand Down
9 changes: 8 additions & 1 deletion docs/design/file-assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ one or more units:
| Configuration | Subsystem | FileAssertConfig, FileAssertData |
| Modeling | Subsystem | FileAssertTest, FileAssertFile, FileAssertRule, |
| | | FileAssertTextAssert, FileAssertPdfAssert, FileAssertXmlAssert, |
| | | FileAssertHtmlAssert, FileAssertYamlAssert, FileAssertJsonAssert |
| | | FileAssertHtmlAssert, FileAssertYamlAssert, FileAssertJsonAssert, |
| | | FileAssertZipAssert |
| Utilities | Subsystem | PathHelpers |
| SelfTest | Subsystem | Validation |

Expand Down Expand Up @@ -73,6 +74,10 @@ The following sequence describes the normal execution path:
immediate error if parsing fails, otherwise applies dot-notation path count assertions.
g. If a `json:` block is defined, attempts to parse the file using `System.Text.Json`; reports
an immediate error if parsing fails, otherwise applies dot-notation path count assertions.
h. If a `zip:` block is defined, attempts to open the file as a zip archive using
`System.IO.Compression.ZipFile`; reports an immediate error if the archive cannot be
opened, otherwise matches entry names against each configured glob pattern and enforces
the declared count constraints.
8. Rule violations and parse failures are recorded via `context.WriteError`.
9. After all tests complete, if `context.ResultsFile` is set, `FileAssertConfig.Run` writes
TRX or JUnit XML results (format determined by the file extension) to the specified path.
Expand Down Expand Up @@ -107,3 +112,5 @@ The following sequence describes the normal execution path:
HtmlAgilityPack is chosen for HTML because it is the de-facto standard for lenient HTML parsing
in .NET. YamlDotNet is already a project dependency and is reused for YAML parsing.
`System.Text.Json` is part of the .NET BCL and is used for JSON parsing.
- **Zip archive inspection**: `System.IO.Compression.ZipFile` is part of the .NET BCL and is used
to open zip archives and enumerate their entries, requiring no additional dependencies.
5 changes: 4 additions & 1 deletion docs/design/file-assert/modeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ executable domain objects and drives the assertion logic.
| `FileAssertHtmlAssert` | `FileAssertHtmlAssert.cs` | Parses HTML; applies XPath node count assertions. |
| `FileAssertYamlAssert` | `FileAssertYamlAssert.cs` | Parses YAML; applies dot-notation path assertions. |
| `FileAssertJsonAssert` | `FileAssertJsonAssert.cs` | Parses JSON; applies dot-notation path assertions. |
| `FileAssertZipAssert` | `FileAssertZipAssert.cs` | Opens zip archive; applies entry glob count checks. |

### Subsystem Responsibilities

Expand All @@ -30,6 +31,7 @@ executable domain objects and drives the assertion logic.
- Parse matched files as PDF, XML, HTML, YAML, or JSON documents when the corresponding assertion block is declared.
- Report an immediate error when a file cannot be parsed as the declared format.
- Apply structured-document query assertions (XPath or dot-notation) to parsed document nodes.
- Open zip archives and match entry names against glob patterns, enforcing count constraints.
- Report assertion failures via the `Context` from the Cli subsystem.

### Object Hierarchy
Expand All @@ -47,7 +49,8 @@ FileAssertTest
├── FileAssertXmlAssert? (zero or one)
├── FileAssertHtmlAssert? (zero or one)
├── FileAssertYamlAssert? (zero or one)
└── FileAssertJsonAssert? (zero or one)
├── FileAssertJsonAssert? (zero or one)
└── FileAssertZipAssert? (zero or one)
```

### Interactions with Other Subsystems
Expand Down
120 changes: 120 additions & 0 deletions docs/design/file-assert/modeling/file-assert-zip-assert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
### FileAssertZipAssert Design

#### Overview

The `FileAssertZipAssert` class validates the contents of a zip archive by matching entry names
against glob patterns and enforcing minimum and maximum count constraints. It is created from a
`FileAssertZipData` DTO and is invoked by `FileAssertFile` when a `zip:` assertion block is
declared. Wrapping zip entry validation in a dedicated unit keeps `FileAssertFile` free of
archive-inspection logic and makes the zip assertion pattern consistent with all other file-type
assert units.

#### Class Structure

##### Nested Class: Entry

The `Entry` nested class holds the compiled state for a single entry constraint:

| Property | Type | Description |
| :-------- | :------- | :------------------------------------------------------------------ |
| `Pattern` | `string` | Glob pattern used to match zip entry names. |
| `Min` | `int?` | Minimum number of entries that must match, or null for no bound. |
| `Max` | `int?` | Maximum number of entries that may match, or null for no bound. |

##### Properties

| Property | Type | Description |
| :-------- | :-------------------------- | :---------------------------------------------------- |
| `Entries` | `IReadOnlyList<Entry>` | Entry constraints applied to the zip archive. |

##### Factory Method

```csharp
internal static FileAssertZipAssert Create(FileAssertZipData data)
```

Converts each `FileAssertZipEntryData` DTO into an `Entry` instance after validating that a
pattern is specified.

| Parameter | Type | Description |
| :-------- | :-------------------- | :------------------------------------------------------- |
| `data` | `FileAssertZipData` | Zip assertion block data from YAML configuration. |

| Return / Exception | Description |
| :--------------------------- | :---------------------------------------------------------- |
| Returns | A new `FileAssertZipAssert` instance. |
| `ArgumentNullException` | Thrown when `data` is null. |
| `InvalidOperationException` | Thrown when any entry does not specify a pattern. |

##### Run Method

```csharp
internal void Run(Context context, string fileName)
```

Opens the zip archive, collects all file entry names, and evaluates each entry constraint.

Execution proceeds in the following steps:

1. Attempts to open the zip archive with `ZipFile.OpenRead(fileName)`.
2. If an `IOException`, `InvalidDataException`, or `UnauthorizedAccessException` is thrown,
writes the error below and returns immediately.
3. Enumerates all archive entries, normalizing separators to forward slashes and excluding
directory entries (names ending with `/`).
4. For each configured `Entry`, uses `Matcher.Match(".", allEntries)` from
`Microsoft.Extensions.FileSystemGlobbing` to count matched entries.
5. Writes an error if the match count is below `Min` or above `Max`.

###### Run Error Messages

```text
File '<fileName>' could not be read as a zip archive
```

```text
Zip '<fileName>' entry pattern '<pattern>' matched <count> entry(s),
but expected at least <min>
```

```text
Zip '<fileName>' entry pattern '<pattern>' matched <count> entry(s),
but expected at most <max>
```

| Parameter | Type | Description |
| :--------- | :-------- | :------------------------------------- |
| `context` | `Context` | Reporting sink used to record errors. |
| `fileName` | `string` | Full path to the zip file to validate. |

#### YAML Configuration

Zip entry constraints are declared under the `zip:` key of a file entry:

```yaml
files:
- pattern: "output/package.zip"
zip:
entries:
- pattern: 'lib/net8.0/MyLib.dll'
min: 1
max: 1
- pattern: 'lib/**/*.dll'
min: 1
```

#### Design Decisions

- **Dedicated unit for zip validation**: Wrapping zip archive inspection in `FileAssertZipAssert`
keeps `FileAssertFile` free of archive-handling logic and makes the pattern consistent with all
other file-type assert units (`FileAssertTextAssert`, `FileAssertXmlAssert`, etc.).
- **Forward-slash normalization**: Zip entry names are normalized to forward slashes before
matching so that glob patterns work consistently regardless of the creating platform.
- **Directory entry exclusion**: Entries whose names end with `/` are directory markers and are
excluded from matching to avoid false counts from container entries.
- **Virtual root for Matcher**: `Matcher.Match(".", allEntries)` applies the glob
pattern directly to the normalized entry name list without any filesystem path manipulation,
because zip entry names are self-contained paths rather than paths relative to a directory root.
The `"."` root is required because `InMemoryDirectoryInfo` rejects empty or null root paths.
- **Immediate failure on parse error**: If the file cannot be opened as a zip archive, an error
is written immediately and no entry constraints are evaluated, consistent with the behavior of
all other file-type assert units.
7 changes: 5 additions & 2 deletions docs/design/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This document covers the detailed design of the following software units:
- **FileAssertHtmlAssert** — HTML document assertions (`FileAssertHtmlAssert.cs`)
- **FileAssertYamlAssert** — YAML document assertions (`FileAssertYamlAssert.cs`)
- **FileAssertJsonAssert** — JSON document assertions (`FileAssertJsonAssert.cs`)
- **FileAssertZipAssert** — zip archive entry assertions (`FileAssertZipAssert.cs`)
- **PathHelpers** — safe path-combination utility (`PathHelpers.cs`)
- **Validation** — self-validation test runner (`Validation.cs`)

Expand Down Expand Up @@ -63,7 +64,8 @@ FileAssert (System)
│ ├── FileAssertXmlAssert (Unit)
│ ├── FileAssertHtmlAssert (Unit)
│ ├── FileAssertYamlAssert (Unit)
│ └── FileAssertJsonAssert (Unit)
│ ├── FileAssertJsonAssert (Unit)
│ └── FileAssertZipAssert (Unit)
├── Utilities (Subsystem)
│ └── PathHelpers (Unit)
└── SelfTest (Subsystem)
Expand Down Expand Up @@ -94,7 +96,8 @@ src/DemaConsulting.FileAssert/
│ ├── FileAssertXmlAssert.cs — XML document assertions (System.Xml.Linq/XPath)
│ ├── FileAssertHtmlAssert.cs — HTML document assertions (HtmlAgilityPack)
│ ├── FileAssertYamlAssert.cs — YAML document assertions (YamlDotNet)
│ └── FileAssertJsonAssert.cs — JSON document assertions (System.Text.Json)
│ ├── FileAssertJsonAssert.cs — JSON document assertions (System.Text.Json)
│ └── FileAssertZipAssert.cs — zip archive entry assertions (System.IO.Compression)
├── Utilities/
│ └── PathHelpers.cs — safe path-combination utility
└── SelfTest/
Expand Down
17 changes: 17 additions & 0 deletions docs/reqstream/file-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,20 @@ sections:
- FileAssert-Modeling-QueryAssertions
tests:
- IntegrationTest_JsonAssert_PassingQuery_ReturnsZero

- id: FileAssert-System-ZipAssertions
title: |
The FileAssert tool shall evaluate zip archive entry assertions declared in a `zip:` block
and return a non-zero exit code when any entry count constraint fails or when the file
cannot be opened as a valid zip archive.
justification: |
Zip archives are a common packaging format for build outputs, distribution packages, and
compliance artifacts. Entry count constraints allow users to assert that required files
are present in the archive without unpacking it. An immediate failure on an invalid zip
archive prevents misleading partial results.
children:
- FileAssert-Modeling-FileTypeParsing
- FileAssert-FileAssertZipAssert-EntryMatching
tests:
- FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError
- FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError
2 changes: 2 additions & 0 deletions docs/reqstream/file-assert/modeling.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ sections:
- FileAssert-FileAssertYamlAssert-ParseError
- FileAssert-FileAssertJsonAssert-Creation
- FileAssert-FileAssertJsonAssert-ParseError
- FileAssert-FileAssertZipAssert-Creation
- FileAssert-FileAssertZipAssert-ParseError
tests:
- Modeling_FileTypeParsing_InvalidXml_ReportsParseError

Expand Down
50 changes: 50 additions & 0 deletions docs/reqstream/file-assert/modeling/file-assert-zip-assert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
# Software Unit Requirements for the FileAssertZipAssert Class
#
# The FileAssertZipAssert class validates zip archive contents by matching entry names against
# glob patterns and enforcing count constraints. It is invoked by FileAssertFile when a
# `zip:` assertion block is declared.

sections:
- title: FileAssertZipAssert Unit Requirements
requirements:
- id: FileAssert-FileAssertZipAssert-Creation
title: |
The FileAssertZipAssert class shall be constructed from a zip assertion data object
containing a list of entry pattern constraints.
justification: |
Constructing entry constraints at creation time rather than at evaluation time avoids
repeated per-file construction overhead and allows validation errors (such as missing
patterns) to be reported before any file system operations are attempted.
tests:
- FileAssertZipAssert_Create_ValidData_CreatesZipAssert
- FileAssertZipAssert_Create_NullData_ThrowsArgumentNullException
- FileAssertZipAssert_Create_EntryMissingPattern_ThrowsInvalidOperationException

- id: FileAssert-FileAssertZipAssert-EntryMatching
title: |
The FileAssertZipAssert class shall match zip archive entry names against each
configured glob pattern and report an error when the match count violates the
declared minimum or maximum constraint.
justification: |
Count constraints on zip entry patterns allow users to assert that required
artifacts are present in a package archive (minimum) or that unexpected extra
entries have not been included (maximum). Both bounds must be checked and reported
independently so that all violations are visible in a single pass.
tests:
- FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError
- FileAssertZipAssert_Run_GlobPatternMatchesMultipleEntries_NoError
- FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError
- FileAssertZipAssert_Run_TooManyMatchingEntries_WritesError

- id: FileAssert-FileAssertZipAssert-ParseError
title: |
The FileAssertZipAssert class shall report an immediate error when the target file
cannot be opened as a valid zip archive.
justification: |
I/O failures (file not found, access denied) and format errors (invalid zip data)
should be reported as clean assertion errors rather than unhandled exceptions,
consistent with the behavior of all other file-type assert units.
tests:
- FileAssertZipAssert_Run_InvalidZipFile_WritesError
- FileAssertZipAssert_Run_NonExistentFile_WritesError
16 changes: 16 additions & 0 deletions docs/verification/file-assert.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,20 @@ a positional filter argument.

**Expected**: Exit code non-zero.

### FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError

**Scenario**: A zip assertion is configured and the archive contains entries that satisfy
the declared minimum and maximum count constraints.

**Expected**: Exit code 0.

### FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError

**Scenario**: A zip assertion is configured with a minimum count but the archive contains
fewer matching entries than required.

**Expected**: Exit code non-zero.

## Requirements Coverage

- **Version display**: IntegrationTest_VersionFlag_OutputsVersion
Expand Down Expand Up @@ -253,3 +267,5 @@ a positional filter argument.
IntegrationTest_YamlAssert_PassingQuery_ReturnsZero,
IntegrationTest_JsonAssert_PassingQuery_ReturnsZero,
IntegrationTest_PdfAssert_InvalidFile_ReturnsNonZero
- **Zip archive assertions**: FileAssertZipAssert_Run_MatchingEntriesMeetConstraints_NoError,
FileAssertZipAssert_Run_TooFewMatchingEntries_WritesError
2 changes: 2 additions & 0 deletions docs/verification/file-assert/modeling.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ satisfying the query and count constraints is provided.
Modeling_ExecuteChain_ReportsFailuresThroughContext
- **XML parsing error reporting**: Modeling_FileTypeParsing_InvalidXml_ReportsParseError
- **XML query assertion**: Modeling_QueryAssertions_XmlQueryMeetsCount_NoError
- **Zip assert creation and parse error**: FileAssertZipAssert_Create_ValidData_CreatesZipAssert,
FileAssertZipAssert_Run_InvalidZipFile_WritesError
Loading