Separate optimization capabilities by effect #2995
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| branches: [main, master] | |
| # release.yml calls this via `uses: ./.github/workflows/ci.yml` so tag | |
| # releases run the exact same test matrix as per-PR CI. | |
| workflow_call: {} | |
| workflow_dispatch: {} | |
| # Pushing three times to a branch used to queue three full matrices and run all of them. A newer | |
| # pull-request run supersedes the older one; a push to main is not cancelled, because main wants a | |
| # recorded result for every commit rather than only for the newest. | |
| concurrency: | |
| group: ci-${{ github.event_name }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| # Force JavaScript-based actions to run on Node 24 instead of the deprecated Node 20 ahead of | |
| # the 2026-06-16 hard cutover, until we bump each action to a Node-24-native major version. | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' | |
| jobs: | |
| test: | |
| name: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 90 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # version.pl needs full history | |
| # FileFormat.PngCrushAdapters has sibling-conditional ProjectReferences | |
| # to ..\..\PNGCrushCS\FileFormat.{Png,Apng,Jpeg,...}. Without the sibling | |
| # those types disappear and any test that imports them fails to compile. | |
| # Clone the sibling alongside CompressionWorkbench so the conditional | |
| # resolves to true and the full image-format surface is available on CI. | |
| - name: Clone PNGCrushCS sibling | |
| shell: bash | |
| run: git clone --depth 1 https://github.com/Hawkynt/PNGCrushCS.git "$GITHUB_WORKSPACE/../PNGCrushCS" | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| dotnet-quality: 'preview' | |
| # MSBuild does not reliably propagate $(Configuration) across solution-tree | |
| # boundaries; without this pre-build the sibling builds to bin/Debug/net8.0/ | |
| # and the publish step then can't find Release/net8.0/ during bundling. | |
| - name: Pre-build PNGCrushCS sibling (Release) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cd "$GITHUB_WORKSPACE/../PNGCrushCS" | |
| REQUIRED="Compression.Core FileFormat.Core FileFormat.Bmp FileFormat.Apng \ | |
| FileFormat.Tiff FileFormat.BigTiff FileFormat.Mng FileFormat.Fli \ | |
| FileFormat.Dcx FileFormat.Icns FileFormat.Mpo FileFormat.Jpeg \ | |
| FileFormat.Png" | |
| # Sibling reorganised most format projects into a FileFormats/ subdir | |
| # but kept Compression.Core / FileFormat.Core at the root. Probe both | |
| # locations per project so the script tolerates the layout move; also | |
| # tolerate transient sibling-mid-refactor commits where a project is | |
| # missing entirely — FileFormat.PngCrushAdapters.csproj has a matching | |
| # SiblingPresent gate that falls back to an empty assembly. | |
| locate() { | |
| local proj="$1" | |
| if [ -f "$proj/$proj.csproj" ]; then echo "$proj"; return 0; fi | |
| if [ -f "FileFormats/$proj/$proj.csproj" ]; then echo "FileFormats/$proj"; return 0; fi | |
| return 1 | |
| } | |
| MISSING="" | |
| for proj in $REQUIRED; do | |
| if ! locate "$proj" >/dev/null; then | |
| MISSING="$MISSING $proj" | |
| fi | |
| done | |
| if [ -n "$MISSING" ]; then | |
| echo "::warning::PNGCrushCS sibling incomplete (missing:$MISSING). Skipping pre-build; consumer csproj SiblingPresent gate handles the fallback." | |
| exit 0 | |
| fi | |
| for proj in $REQUIRED; do | |
| dir="$(locate "$proj")" | |
| echo "::group::Building $proj ($dir)" | |
| dotnet build "$dir/$proj.csproj" --configuration Release --nologo | |
| echo "::endgroup::" | |
| test -f "$dir/bin/Release/net8.0/$proj.dll" || { | |
| echo "::error::Pre-build did not produce $dir/bin/Release/net8.0/$proj.dll" | |
| exit 1 | |
| } | |
| done | |
| echo "All sibling Release DLLs verified." | |
| - name: Restore | |
| run: dotnet restore CompressionWorkbench.slnx | |
| - name: Build | |
| run: dotnet build CompressionWorkbench.slnx --configuration Release --no-restore | |
| # Verify the four NuGet meta-packages still pack on every PR / push. | |
| # This catches regressions in the bundling target (filter excludes for | |
| # Compression.Core / Registry, ProjectReference path resolution, etc.) | |
| # before they reach release / nightly. We don't push or upload — the | |
| # _build.yml workflow does the real packing and artifact upload for the | |
| # nightly / release workflows that consume them. | |
| # | |
| # NB: --no-build is intentionally omitted. The meta csprojs use a custom | |
| # `_AddBundledAssembliesToPackage` target with `DependsOnTargets="ResolveReferences"` | |
| # which the SDK refuses to invoke under --no-build (NETSDK1085). The | |
| # prior `dotnet build` step already produced up-to-date outputs so pack's | |
| # build phase no-ops. | |
| - name: Verify NuGet meta-packages pack | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| rm -rf dist-nuget | |
| dotnet pack Compression.Core/Hawkynt.Compression.Core.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci | |
| dotnet pack Hawkynt.FileFormats.Audio/Hawkynt.FileFormats.Audio.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci | |
| dotnet pack Hawkynt.FileFormats.Archives/Hawkynt.FileFormats.Archives.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci | |
| dotnet pack Hawkynt.FileFormats.FileSystems/Hawkynt.FileFormats.FileSystems.csproj --configuration Release --output dist-nuget -p:Version=0.0.0-ci | |
| ls -la dist-nuget/ | |
| - name: Install interop tools (Linux) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y p7zip-full gzip bzip2 xz-utils zstd lz4 tar cpio genisoimage mtools qemu-utils gfs2-utils bcachefs-tools xfsprogs | |
| # cabextract and wimlib-imagex are the tools that OWN the CAB and WIM formats. | |
| # Both reference tests fall back to 7-Zip when they are absent, so without these | |
| # two packages the owning readers were never asked -- which is what #85 and #86 | |
| # each reported. Installed so those cases run instead of silently degrading. | |
| sudo apt-get install -y cabextract wimtools | |
| sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true | |
| sudo modprobe gfs2 || true | |
| # Core tests -- always run (no external tools required). | |
| # | |
| # NOTE the two similar category names, which are NOT spellings of each other: | |
| # End2End (95 uses) in-process create/extract round trips - Ace, Arj, Arc and friends. | |
| # No external process, so it belongs here and GATES the pull request. | |
| # EndToEnd (10 uses) oracle comparisons against real 7-Zip / RAR / SFX binaries. Needs | |
| # the tools installed below, hence its own advisory step. | |
| # Renaming one to the other would move 95 gating tests into an advisory tier that installs | |
| # tools they never use. Leave them distinct. | |
| - name: Core tests | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "TestCategory!=EndToEnd&TestCategory!=OsIntegration&TestCategory!=ExternalInterop&TestCategory!=ExternalFsInterop&TestCategory!=PolyglotInterop&TestCategory!=Performance" | |
| --logger "trx;LogFileName=core-tests.trx" | |
| # Interop tools for E2E tests. gfs2-utils is a mandatory oracle for the | |
| # ExHash gate below; linux-modules-extra supplies gfs2.ko on Ubuntu runner | |
| # kernels where it is not built in. | |
| - name: Install 7-Zip (Windows) | |
| if: matrix.os == 'windows-latest' | |
| run: choco install 7zip -y | |
| # Unlike the broad external tier below, this is a hard gate: real | |
| # fsck.gfs2 must accept our ExHash metadata and the Linux driver must mount | |
| # the image and resolve/read the emitted namespace. | |
| - name: GFS2 ExHash oracle | |
| if: matrix.os == 'ubuntu-latest' | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "FullyQualifiedName~Compression.Tests.Gfs2.Gfs2NestedDirectoryExternalTests" | |
| --logger "trx;LogFileName=gfs2-exhash-tests.trx" | |
| # Bucket generation handling is a cross-tree invariant: alloc_v4, | |
| # bucket_gens, extent/b-tree pointers and backpointers must all carry the | |
| # same generation. Keep the real checker as a hard gate rather than relying | |
| # on our own reader to agree with our own writer. | |
| # | |
| # The tests gate themselves on the checker's version. Ubuntu's archive | |
| # currently ships bcachefs-tools 1.3.4, which refuses the superblock this | |
| # package writes (metadata version 1.38) before it reads any bucket, so on | |
| # this runner they skip and say so. They run in full wherever the tool is | |
| # new enough. A checker that does read the image and then objects fails the | |
| # step; that is never downgraded to a skip. | |
| - name: bcachefs bucket generation oracle | |
| if: matrix.os == 'ubuntu-latest' | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "FullyQualifiedName~Compression.Tests.BcacheFs.BcacheFsBucketGenerationExternalTests" | |
| --logger "trx;LogFileName=bcachefs-generation-tests.trx" | |
| # python-msgpack is the one oracle below that is not already on both runner images. Advisory | |
| # on its own: without it the MessagePack case skips and says so, and the rest still run. | |
| - name: Install structured-format oracles | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| python3 -m pip install --user --break-system-packages msgpack \ | |
| || python -m pip install --user msgpack \ | |
| || true | |
| # The frozen vectors in the core tier pin bytes that CPython, Perl and reg.exe accepted once. | |
| # This asks the tools themselves, against live output, and it is a hard gate rather than | |
| # advisory because the tools it needs ship with both runner images: CPython with pickle, json | |
| # and ElementTree, Perl with Storable, and reg.exe on Windows. A red here means our output | |
| # stopped being readable by the implementation that owns the format. | |
| # | |
| # Individual cases still Assert.Ignore when their own tool is absent, so a runner without one | |
| # reports "not validated" for that case instead of failing the build for it. That is the only | |
| # thing a missing tool is allowed to do; a tool that IS there and rejects our bytes fails. | |
| - name: Structured pseudo-archive writer oracles | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "FullyQualifiedName~Compression.Tests.StructuredPseudoArchives.StructuredPseudoArchiveExternalToolTests" | |
| --logger "trx;LogFileName=structured-writer-tests.trx" | |
| - name: End-to-end interop tests | |
| continue-on-error: true | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "Category=EndToEnd" | |
| --logger "trx;LogFileName=e2e-tests.trx" | |
| # These exercise our formats against the real external tools installed | |
| # above (7-Zip, zstd, lz4, flac) and the post-mutation filesystem checks. | |
| # They were excluded from the core filter but had no step of their own, so | |
| # nothing ran them at all; advisory like the other interop tiers, because a | |
| # runner without one of the tools must not fail the build. | |
| - name: External interop tests | |
| continue-on-error: true | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "Category=ExternalInterop" | |
| --logger "trx;LogFileName=external-tests.trx" | |
| # mkfs.*, 7z and friends. Advisory for the same reason the neighbours are: a runner without | |
| # one of the tools must not fail the build. These used to sit in the CORE tier, which runs | |
| # BEFORE the tools above are installed -- so they were reached with nothing to interoperate | |
| # with and skipped themselves, every run, invisibly. | |
| - name: External filesystem interop tests | |
| continue-on-error: true | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "Category=ExternalFsInterop" | |
| --logger "trx;LogFileName=external-fs-tests.trx" | |
| - name: OS integration tests | |
| continue-on-error: true | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "Category=OsIntegration" | |
| --logger "trx;LogFileName=os-tests.trx" | |
| # Polyglot tests verify our formats round-trip through other language | |
| # ecosystems. Tests skip individually if the language package isn't | |
| # available; never call `pip install` etc. from inside a test. | |
| - name: Install polyglot test dependencies (Linux) | |
| if: matrix.os == 'ubuntu-latest' | |
| continue-on-error: true | |
| run: | | |
| python3 -m pip install --user --break-system-packages py7zr zstandard brotli lz4 rarfile || true | |
| sudo cpanm --notest Archive::Zip IO::Compress::Lzma IO::Compress::Lzop || true | |
| gem install --user-install minitar rubyzip zstd-ruby || true | |
| npm install --prefix /tmp/polyglot-node adm-zip tar-stream || true | |
| echo "NODE_PATH=/tmp/polyglot-node/node_modules" >> $GITHUB_ENV | |
| - name: Polyglot interop tests | |
| continue-on-error: true | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "Category=PolyglotInterop" | |
| --logger "trx;LogFileName=polyglot-tests.trx" | |
| # Wall-clock performance tests are advisory — a slow shared runner must not | |
| # fail the build on a timing assertion. | |
| - name: Performance tests | |
| continue-on-error: true | |
| run: > | |
| dotnet test Compression.Tests | |
| --configuration Release --no-build | |
| --filter "Category=Performance" | |
| --logger "trx;LogFileName=perf-tests.trx" | |
| # AFTER every test leg, and it runs even when one of them failed. Ordered before them it | |
| # masks the suite completely: this step going red stopped the job, no test ran at all, and | |
| # the pull request looked merely blocked rather than failing. That is how twenty-five failing | |
| # tests once reached main. The tests are the primary signal and must always produce one. | |
| # | |
| # The package READMEs must follow the house template, and their API reference must still match | |
| # the assemblies just built. The template itself lives in RepositoryTemplate -- nothing about | |
| # it is copied into this repo. | |
| - name: Check package READMEs | |
| if: ${{ !cancelled() && matrix.os == 'ubuntu-latest' }} | |
| uses: Hawkynt/RepositoryTemplate/package-readme@v1 | |
| - name: Upload test results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-${{ matrix.os }} | |
| path: '**/TestResults/*.trx' | |
| # A text lint over README.md against the house convention — no build, no toolchain — so it | |
| # runs beside the rest rather than inside it and cannot mask them. | |
| repo-readme: | |
| name: repo README | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: Hawkynt/RepositoryTemplate/repo-readme@v1 | |
| with: | |
| gui: true |