From b60e43fe2ba0e5030dcf8aec7eb861706bbba978 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 00:51:25 +0100 Subject: [PATCH 01/11] feat: docker + wsl support --- .github/copilot-instructions.md | 2 +- AGENTS.md | 73 +++- Directory.Packages.props | 1 + Justfile | 16 + README.md | 124 ++++-- docs/wiki/Architecture.md | 68 ++- docs/wiki/Backends.md | 392 ++++++++++++++++++ docs/wiki/Consumer-Requirements.md | 74 ++-- docs/wiki/Contributing-Modules.md | 20 +- docs/wiki/Contributing.md | 10 +- docs/wiki/Getting-Started.md | 64 ++- docs/wiki/Home.md | 81 ++-- docs/wiki/Lifecycle.md | 2 +- docs/wiki/Modules.md | 20 +- docs/wiki/Networking.md | 2 +- docs/wiki/Packaging.md | 35 +- docs/wiki/Release-Flow.md | 8 +- docs/wiki/Testing.md | 22 +- docs/wiki/Wait-Strategies.md | 4 +- docs/wiki/Wslc-Capability-Matrix.md | 2 +- docs/wiki/_Sidebar.md | 1 + docs/wiki/index.md | 8 +- purview-build.json | 67 +-- .../DockerSample/DockerSample.csproj | 18 + .../getting-started/DockerSample/Program.cs | 28 ++ samples/getting-started/README.md | 32 ++ samples/getting-started/WslSample/Program.cs | 28 ++ .../WslSample/WslSample.csproj | 25 ++ scripts/verify-consumers.ps1 | 201 +++++++-- src/Directory.Build.props | 2 +- src/WSLTestContainers.slnx | 16 +- src/src/Azurite/Azurite.csproj | 4 +- src/src/Azurite/AzuriteAccount.cs | 2 +- src/src/Azurite/AzuriteBuilder.cs | 10 +- src/src/Azurite/AzuriteConfiguration.cs | 2 +- src/src/Azurite/AzuriteContainer.cs | 8 +- src/src/Azurite/Sdk/README.md | 21 +- src/src/Containers/Container.cs | 13 + src/src/Containers/ContainerBackendInfo.cs | 22 + .../Containers/ContainerBackendSelection.cs | 53 +++ src/src/Containers/ContainerBackends.cs | 293 +++++++++++++ src/src/Containers/ContainerBase.cs | 133 ++++++ .../ContainerBuilder.cs | 82 ++-- .../ContainerConfiguration.cs | 12 +- .../Containers/ContainerLogEntry.cs | 2 +- src/src/Containers/ContainerName.cs | 41 ++ .../Containers/ContainerState.cs | 2 +- src/src/Containers/Containers.csproj | 9 + .../Diagnostics/ContainerActivity.cs | 13 + .../Diagnostics/Secret.cs | 2 +- .../Diagnostics/SecretRedactor.cs | 2 +- .../Containers/ExecOptions.cs | 2 +- .../Containers/ExecResult.cs | 2 +- .../IContainer.cs | 4 +- src/src/Containers/IContainerBackend.cs | 18 + .../IContainerBuilder.cs | 2 +- .../IContainerConfiguration.cs | 10 +- .../Images/Image.cs | 12 +- .../Images/ImagePullProgress.cs | 2 +- .../Images/ImageSummary.cs | 2 +- .../Images/PullPolicy.cs | 2 +- .../Containers/LogStream.cs | 2 +- .../Mounts/BindMount.cs | 2 +- .../Mounts/NamedVolume.cs | 2 +- .../Networking/ContainerNetworkingMode.cs | 2 +- .../Networking/PortBinding.cs | 2 +- .../Networking/PortProtocol.cs | 2 +- .../RegistryCredentials.cs | 4 +- .../Containers/Runtime/ContainerException.cs | 41 ++ src/src/Containers/Sdk/README.md | 68 +++ .../buildTransitive/Purview.Containers.props | 16 + .../Purview.Containers.targets | 50 +++ .../Waiting/AllWaitStrategy.cs | 2 +- .../Waiting/AnyWaitStrategy.cs | 2 +- .../Waiting/CommandWaitStrategy.cs | 2 +- .../Waiting/ContainerRunningWaitStrategy.cs | 4 +- .../Waiting/CustomWaitStrategy.cs | 2 +- .../Waiting/HttpWaitStrategy.cs | 2 +- .../Waiting/IWaitStrategy.cs | 2 +- .../Waiting/LogMessageWaitStrategy.cs | 2 +- .../Waiting/TcpPortWaitStrategy.cs | 2 +- .../Waiting/Wait.cs | 2 +- .../Waiting/WaitContext.cs | 5 +- .../Waiting/WaitStrategy.cs | 2 +- .../Waiting/WaitStrategyRunner.cs | 19 +- src/src/Directory.Build.props | 23 + src/src/Docker/Docker.csproj | 14 + src/src/Docker/DockerContainer.cs | 218 ++++++++++ src/src/Docker/DockerContainerBackend.cs | 123 ++++++ src/src/Docker/Sdk/README.md | 56 +++ .../Purview.Containers.Docker.props | 15 + src/src/MsSql/MsSql.csproj | 4 +- src/src/MsSql/MsSqlBuilder.cs | 20 +- src/src/MsSql/MsSqlConfiguration.cs | 4 +- src/src/MsSql/MsSqlContainer.cs | 8 +- src/src/MsSql/Sdk/README.md | 25 +- src/src/MySql/MySql.csproj | 4 +- src/src/MySql/MySqlBuilder.cs | 18 +- src/src/MySql/MySqlConfiguration.cs | 4 +- src/src/MySql/MySqlContainer.cs | 8 +- src/src/MySql/Sdk/README.md | 25 +- src/src/Nats/Nats.csproj | 4 +- src/src/Nats/NatsBuilder.cs | 10 +- src/src/Nats/NatsConfiguration.cs | 2 +- src/src/Nats/NatsContainer.cs | 8 +- src/src/Nats/Sdk/README.md | 23 +- src/src/PostgreSql/PostgreSql.csproj | 4 +- src/src/PostgreSql/PostgreSqlBuilder.cs | 20 +- src/src/PostgreSql/PostgreSqlConfiguration.cs | 4 +- src/src/PostgreSql/PostgreSqlContainer.cs | 8 +- src/src/PostgreSql/Sdk/README.md | 25 +- src/src/RabbitMq/RabbitMq.csproj | 4 +- src/src/RabbitMq/RabbitMqBuilder.cs | 20 +- src/src/RabbitMq/RabbitMqConfiguration.cs | 4 +- src/src/RabbitMq/RabbitMqContainer.cs | 8 +- src/src/RabbitMq/Sdk/README.md | 25 +- src/src/Redis/Redis.csproj | 4 +- src/src/Redis/RedisBuilder.cs | 10 +- src/src/Redis/RedisConfiguration.cs | 2 +- src/src/Redis/RedisContainer.cs | 8 +- src/src/Redis/Sdk/README.md | 23 +- .../Wsl/Diagnostics/WslContainerActivity.cs | 10 + .../IContainerRuntime.cs | 2 +- .../IContainerSession.cs | 4 +- src/src/{WslContainers => Wsl}/LogBuffer.cs | 3 +- src/src/{WslContainers => Wsl}/Sdk/README.md | 35 +- .../Purview.Containers.Wsl.props} | 10 +- .../Purview.Containers.Wsl.targets} | 37 +- src/src/{WslContainers => Wsl}/StorageMode.cs | 2 +- src/src/Wsl/Wsl.csproj | 21 + .../{WslContainers => Wsl}/WslContainer.cs | 15 +- src/src/Wsl/WslContainerBackend.cs | 68 +++ src/src/Wsl/WslContainerException.cs | 25 ++ .../WslContainerRuntime.cs | 27 +- .../WslContainerRuntimeInfo.cs | 2 +- .../WslContainerRuntimeOptions.cs | 8 +- .../WslContainerSession.cs | 35 +- .../WslInspectParser.cs | 2 +- .../WslSettingsMapper.cs | 6 +- .../Diagnostics/WslContainersActivity.cs | 10 - .../Runtime/WslContainerException.cs | 39 -- src/src/WslContainers/WslContainers.csproj | 13 - .../AzuriteIntegrationTests.cs | 2 +- .../Azurite.UnitTests/AzuriteBuilderTests.cs | 4 +- .../Docker.IntegrationTests.csproj | 5 + .../DockerBackendSelectionTests.cs | 60 +++ .../DockerContainerTests.cs | 105 +++++ .../Docker.IntegrationTests/DockerTest.cs | 33 ++ .../Docker.UnitTests/Docker.UnitTests.csproj | 5 + .../DockerContainerBackendTests.cs | 42 ++ .../ModuleDockerTests.cs | 130 ++++++ .../Modules.DockerIntegrationTests.csproj | 12 + .../MsSqlIntegrationTests.cs | 2 +- .../MsSql.UnitTests/MsSqlBuilderTests.cs | 8 +- .../MySqlIntegrationTests.cs | 2 +- .../MySql.UnitTests/MySqlBuilderTests.cs | 6 +- .../NatsIntegrationTests.cs | 2 +- src/tests/Nats.UnitTests/NatsBuilderTests.cs | 4 +- .../PostgreSqlIntegrationTests.cs | 4 +- .../PostgreSqlBuilderTests.cs | 10 +- .../RabbitMqIntegrationTests.cs | 2 +- .../RabbitMqBuilderTests.cs | 8 +- .../RedisIntegrationTests.cs | 2 +- .../Redis.UnitTests/RedisBuilderTests.cs | 4 +- .../SharedTestingFramework.csproj | 2 +- src/tests/SharedTestingFramework/WslcTest.cs | 19 +- .../AlpineLifecycleTests.cs | 4 +- .../CleanupTests.cs | 8 +- .../ConcurrencyStressTests.cs | 6 +- .../ExecTests.cs | 4 +- .../PerformanceTests.cs | 2 +- .../PortMappingTests.cs | 3 +- .../RuntimeInfoTests.cs | 2 +- .../SharedStorageTests.cs | 6 +- .../VolumeTests.cs | 2 +- .../WaitStrategyTests.cs | 11 +- .../Wsl.IntegrationTests.csproj} | 0 .../ConsumerRequirementsTests.cs | 2 +- .../Wsl.UnitTests/ContainerBackendsTests.cs | 265 ++++++++++++ .../ContainerBuilderTests.cs | 44 +- .../FakeContainer.cs | 4 +- .../HttpWaitStrategyTests.cs | 4 +- .../ImageTests.cs | 4 +- .../SecretTests.cs | 4 +- .../StorageModeTests.cs | 2 +- .../WaitContextTests.cs | 4 +- .../WaitStrategyRunnerTests.cs | 8 +- .../WaitStrategyTests.cs | 5 +- .../Wsl.UnitTests.csproj} | 0 .../WslContainerRuntimeTests.cs | 2 +- .../WslInspectParserTests.cs | 2 +- 191 files changed, 3708 insertions(+), 789 deletions(-) create mode 100644 docs/wiki/Backends.md create mode 100644 samples/getting-started/DockerSample/DockerSample.csproj create mode 100644 samples/getting-started/DockerSample/Program.cs create mode 100644 samples/getting-started/README.md create mode 100644 samples/getting-started/WslSample/Program.cs create mode 100644 samples/getting-started/WslSample/WslSample.csproj create mode 100644 src/src/Containers/Container.cs create mode 100644 src/src/Containers/ContainerBackendInfo.cs create mode 100644 src/src/Containers/ContainerBackendSelection.cs create mode 100644 src/src/Containers/ContainerBackends.cs create mode 100644 src/src/Containers/ContainerBase.cs rename src/src/{WslContainers => Containers}/ContainerBuilder.cs (80%) rename src/src/{WslContainers => Containers}/ContainerConfiguration.cs (94%) rename src/src/{WslContainers => }/Containers/ContainerLogEntry.cs (79%) create mode 100644 src/src/Containers/ContainerName.cs rename src/src/{WslContainers => }/Containers/ContainerState.cs (91%) create mode 100644 src/src/Containers/Containers.csproj create mode 100644 src/src/Containers/Diagnostics/ContainerActivity.cs rename src/src/{WslContainers => Containers}/Diagnostics/Secret.cs (93%) rename src/src/{WslContainers => Containers}/Diagnostics/SecretRedactor.cs (94%) rename src/src/{WslContainers => }/Containers/ExecOptions.cs (94%) rename src/src/{WslContainers => }/Containers/ExecResult.cs (85%) rename src/src/{WslContainers => Containers}/IContainer.cs (95%) create mode 100644 src/src/Containers/IContainerBackend.cs rename src/src/{WslContainers => Containers}/IContainerBuilder.cs (89%) rename src/src/{WslContainers => Containers}/IContainerConfiguration.cs (92%) rename src/src/{WslContainers => Containers}/Images/Image.cs (92%) rename src/src/{WslContainers => Containers}/Images/ImagePullProgress.cs (81%) rename src/src/{WslContainers => Containers}/Images/ImageSummary.cs (80%) rename src/src/{WslContainers => Containers}/Images/PullPolicy.cs (89%) rename src/src/{WslContainers => }/Containers/LogStream.cs (82%) rename src/src/{WslContainers => Containers}/Mounts/BindMount.cs (81%) rename src/src/{WslContainers => Containers}/Mounts/NamedVolume.cs (83%) rename src/src/{WslContainers => Containers}/Networking/ContainerNetworkingMode.cs (89%) rename src/src/{WslContainers => Containers}/Networking/PortBinding.cs (94%) rename src/src/{WslContainers => Containers}/Networking/PortProtocol.cs (81%) rename src/src/{WslContainers => Containers}/RegistryCredentials.cs (90%) create mode 100644 src/src/Containers/Runtime/ContainerException.cs create mode 100644 src/src/Containers/Sdk/README.md create mode 100644 src/src/Containers/Sdk/buildTransitive/Purview.Containers.props create mode 100644 src/src/Containers/Sdk/buildTransitive/Purview.Containers.targets rename src/src/{WslContainers => Containers}/Waiting/AllWaitStrategy.cs (94%) rename src/src/{WslContainers => Containers}/Waiting/AnyWaitStrategy.cs (94%) rename src/src/{WslContainers => Containers}/Waiting/CommandWaitStrategy.cs (96%) rename src/src/{WslContainers => Containers}/Waiting/ContainerRunningWaitStrategy.cs (81%) rename src/src/{WslContainers => Containers}/Waiting/CustomWaitStrategy.cs (93%) rename src/src/{WslContainers => Containers}/Waiting/HttpWaitStrategy.cs (98%) rename src/src/{WslContainers => Containers}/Waiting/IWaitStrategy.cs (94%) rename src/src/{WslContainers => Containers}/Waiting/LogMessageWaitStrategy.cs (95%) rename src/src/{WslContainers => Containers}/Waiting/TcpPortWaitStrategy.cs (96%) rename src/src/{WslContainers => Containers}/Waiting/Wait.cs (98%) rename src/src/{WslContainers => Containers}/Waiting/WaitContext.cs (80%) rename src/src/{WslContainers => Containers}/Waiting/WaitStrategy.cs (96%) rename src/src/{WslContainers => Containers}/Waiting/WaitStrategyRunner.cs (85%) create mode 100644 src/src/Directory.Build.props create mode 100644 src/src/Docker/Docker.csproj create mode 100644 src/src/Docker/DockerContainer.cs create mode 100644 src/src/Docker/DockerContainerBackend.cs create mode 100644 src/src/Docker/Sdk/README.md create mode 100644 src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props create mode 100644 src/src/Wsl/Diagnostics/WslContainerActivity.cs rename src/src/{WslContainers => Wsl}/IContainerRuntime.cs (93%) rename src/src/{WslContainers => Wsl}/IContainerSession.cs (92%) rename src/src/{WslContainers => Wsl}/LogBuffer.cs (97%) rename src/src/{WslContainers => Wsl}/Sdk/README.md (79%) rename src/src/{WslContainers/Sdk/buildTransitive/Purview.WslContainers.props => Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props} (62%) rename src/src/{WslContainers/Sdk/buildTransitive/Purview.WslContainers.targets => Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets} (58%) rename src/src/{WslContainers => Wsl}/StorageMode.cs (93%) create mode 100644 src/src/Wsl/Wsl.csproj rename src/src/{WslContainers => Wsl}/WslContainer.cs (96%) create mode 100644 src/src/Wsl/WslContainerBackend.cs create mode 100644 src/src/Wsl/WslContainerException.cs rename src/src/{WslContainers => Wsl}/WslContainerRuntime.cs (86%) rename src/src/{WslContainers => Wsl}/WslContainerRuntimeInfo.cs (86%) rename src/src/{WslContainers => Wsl}/WslContainerRuntimeOptions.cs (83%) rename src/src/{WslContainers => Wsl}/WslContainerSession.cs (89%) rename src/src/{WslContainers => Wsl}/WslInspectParser.cs (98%) rename src/src/{WslContainers => Wsl}/WslSettingsMapper.cs (95%) delete mode 100644 src/src/WslContainers/Diagnostics/WslContainersActivity.cs delete mode 100644 src/src/WslContainers/Runtime/WslContainerException.cs delete mode 100644 src/src/WslContainers/WslContainers.csproj create mode 100644 src/tests/Docker.IntegrationTests/Docker.IntegrationTests.csproj create mode 100644 src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs create mode 100644 src/tests/Docker.IntegrationTests/DockerContainerTests.cs create mode 100644 src/tests/Docker.IntegrationTests/DockerTest.cs create mode 100644 src/tests/Docker.UnitTests/Docker.UnitTests.csproj create mode 100644 src/tests/Docker.UnitTests/DockerContainerBackendTests.cs create mode 100644 src/tests/Modules.DockerIntegrationTests/ModuleDockerTests.cs create mode 100644 src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/AlpineLifecycleTests.cs (96%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/CleanupTests.cs (88%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/ConcurrencyStressTests.cs (89%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/ExecTests.cs (96%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/PerformanceTests.cs (96%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/PortMappingTests.cs (96%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/RuntimeInfoTests.cs (91%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/SharedStorageTests.cs (91%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/VolumeTests.cs (98%) rename src/tests/{WslContainers.IntegrationTests => Wsl.IntegrationTests}/WaitStrategyTests.cs (92%) rename src/tests/{WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj => Wsl.IntegrationTests/Wsl.IntegrationTests.csproj} (100%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/ConsumerRequirementsTests.cs (97%) create mode 100644 src/tests/Wsl.UnitTests/ContainerBackendsTests.cs rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/ContainerBuilderTests.cs (72%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/FakeContainer.cs (95%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/HttpWaitStrategyTests.cs (98%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/ImageTests.cs (97%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/SecretTests.cs (88%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/StorageModeTests.cs (95%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/WaitContextTests.cs (94%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/WaitStrategyRunnerTests.cs (91%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/WaitStrategyTests.cs (97%) rename src/tests/{WslContainers.UnitTests/WslContainers.UnitTests.csproj => Wsl.UnitTests/Wsl.UnitTests.csproj} (100%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/WslContainerRuntimeTests.cs (97%) rename src/tests/{WslContainers.UnitTests => Wsl.UnitTests}/WslInspectParserTests.cs (97%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 35aa381..396adea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -21,7 +21,7 @@ Keep product, architecture, and general engineering standards centralized in `AG - Never weaken the WSLC session/store invariants (single shared session, serialised session mutations, the shared-store fallback, `Secret` redaction) to make a test pass. - Treat the published consumer contract as an invariant too: `.NET 11` + a Windows target framework, - the `buildTransitive` defaults, and the `PWC0001`/`PWC0002` guards documented in + the `buildTransitive` defaults, and the `PCC0001`/`PCC0002` guards documented in `docs/wiki/Consumer-Requirements.md`. `just verify-consumers` must pass before a consumer-facing change is complete. - Consult the repository `.agents/` folder for additional skills/workflows that may improve execution diff --git a/AGENTS.md b/AGENTS.md index c3403d5..bd3a5e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,10 +2,12 @@ ## Purpose and authority -This repository contains `Purview.WslContainers`, a **WSLC-native** Testcontainers-style library for .NET: -throwaway Linux containers for integration testing on **Microsoft WSL Containers (WSLC)**, with no Docker -installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats`, -`MySql`). +This repository contains `Purview.Containers`, a Testcontainers-style library for .NET that runs throwaway +Linux containers for integration testing on **Microsoft WSL Containers (WSLC)** — the Windows runtime with +no Docker installation — or on **Docker** through Testcontainers, plus the service modules (`PostgreSql`, +`Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats`, `MySql`). +`docs/wiki/Backends.md` is the consumer guide to choosing and configuring a backend; keep it up to date +whenever a target framework, a backend package or the `buildTransitive` assets change. - This file is the repository-wide source of truth for AI agents. A more-specific `AGENTS.md` in a subtree, if one is ever added, takes precedence for that subtree. @@ -21,12 +23,15 @@ installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitM | Path | Purpose | | --- | --- | | `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack | -| `src/src/WslContainers` | Core runtime (`Purview.WslContainers`): `ContainerBuilder`, `WslContainer`, runtime, sessions, images, networking, mounts, wait strategies, diagnostics | -| `src/src/` | Service modules; each is a thin layer over the core and carries a bespoke `Sdk/README.md` | -| `src/src//Sdk` | Package-only assets. `Sdk/README.md` is packed as the package README (suppressing the repo-root README); `Sdk/buildTransitive/**` ships MSBuild assets to consumers (the core package's consumer defaults and guards) | -| `src/tests` | TUnit unit (`*.UnitTests`) and WSLC integration (`*.IntegrationTests`) projects | +| `src/src/Containers` | Backend-neutral abstractions (`Purview.Containers`, `net10.0`): `IContainer`/`ContainerConfiguration`, `ContainerBuilder`, `ContainerBase`, `IContainerBackend`/`ContainerBackends`, wait strategies, images, networking, mounts, diagnostics | +| `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`, `net11.0-windows10.0.19041.0`): `WslContainerBackend`, the shared session runtime, `WslContainer`, `WslContainerSession` | +| `src/src/Docker` | Docker backend (`Purview.Containers.Docker`, `net10.0`): `DockerContainerBackend` + `DockerContainer` over Testcontainers | +| `src/src/` | Service modules (`Purview.Containers.`, `net10.0`); each is a thin layer over the abstractions and carries a bespoke `Sdk/README.md` | +| `src/src//Sdk` | Package-only assets. `Sdk/README.md` is packed as the package README (suppressing the repo-root README); `Sdk/buildTransitive/**` ships MSBuild assets to consumers (the abstractions' backend registration, each backend package's own registration entry, and the WSL backend's consumer defaults and guards) | +| `src/tests` | TUnit unit (`*.UnitTests`) and container integration (`*.IntegrationTests`) projects: the WSLC suites need a WSLC host, `Docker.IntegrationTests` and `Modules.DockerIntegrationTests` need a Docker daemon | | `spikes/WslcSpikes` | Phase 0 investigation harness (`s1`..`s14` probes); not part of the test run | -| `docs/wiki` | User-facing documentation wiki, aggregated by the purview-dev website | +| `docs/wiki` | User-facing documentation wiki, aggregated by the purview-dev website. `Backends.md` is the consumer guide to choosing and configuring WSLC or Docker | +| `samples/getting-started` | Runnable consumer-shaped samples (WSLC and Docker) built with the solution; `just sample-wsl` / `just sample-docker` | | `mkdocs.yml` | Wiki site configuration (`docs_dir: docs/wiki`) | | `src/Directory.Build.props` / `src/Directory.Build.targets` | Solution-wide SDK import, package metadata and the shared package icon | | `purview-build.json` | Shared `Purview.Build` pipeline configuration: solution, test discovery/filter and the **exhaustive** pack-validation manifest | @@ -57,23 +62,37 @@ installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitM - **Random host ports are native** (`windowsPort=0`) and read back from the container's mapped ports — never probe for a free port. - **Default networking is `Bridged`**; only IPv4 loopback is mapped, and UDP is unsupported - (`WslContainerNotSupportedException`). + (`ContainerNotSupportedException`). +- **One backend registry per process** (`ContainerBackends`): backend packages register themselves through + the generated module initializer, `ResolveAsync()` returns the pinned backend, the + `PURVIEW_CONTAINERS_BACKEND`-selected backend, or the first *usable* registered backend (probing each + one), and an empty/unusable registry throws `ContainerBackendUnavailableException` naming the fix. A + named or pinned backend never silently falls back. A module must never reference a backend package — it + resolves through the registry. - **`DisposeAsync` is idempotent** and never terminates the shared session; a process-exit hook disposes the session so its name is released. - **Secrets use `Secret`** and must never reach logs, names, `ToString()` or diagnostics (they are redacted). ## Consumer requirements and compatibility -- The packages are **.NET 11, Windows-only**: every project under `src/` targets - `net11.0-windows10.0.19041.0` (`src/Directory.Build.props`) and a consumer must match. The contract, - the failures that enforce it and the CI workarounds are documented in - [Consumer Requirements](docs/wiki/Consumer-Requirements.md); update that page whenever the target - framework, the `buildTransitive` defaults or the `PWC0001`/`PWC0002` guards change. -- `Purview.WslContainers` ships `Sdk/buildTransitive/Purview.WslContainers.{props,targets}` so consumers +- The packages split by framework: `Purview.Containers` (abstractions) and the service modules target + **`net10.0`** on any platform, while `Purview.Containers.Wsl` is the **.NET 11, Windows-only** backend + (`net11.0-windows10.0.19041.0`). `src/src/Directory.Build.props` sets the `net10.0` subtree default and + `Wsl.csproj` overrides it; `src/tests` keeps the Windows target because the tests drive WSLC. The + contract, the failures that enforce it and the CI workarounds are documented in + [Consumer Requirements](docs/wiki/Consumer-Requirements.md); update that page whenever a target + framework, the `buildTransitive` defaults or the `PCC0001`/`PCC0002` guards change. +- `Purview.Containers.Wsl` ships `Sdk/buildTransitive/Purview.Containers.Wsl.{props,targets}` so consumers inherit `WindowsSdkPackageVersion`/`PlatformTarget` defaults and a clear error for an unsupported - target framework (`PWC0001`) or a 32-bit consumer (`PWC0002`). Those assets are framework-agnostic, so + target framework (`PCC0001`) or a 32-bit consumer (`PCC0002`). Those assets are framework-agnostic, so NuGet no longer raises `NU1202` at restore time — the guards are the fail-fast path. Keep them, and - keep the `RequiredContent` entries that declare them. + keep the `RequiredContent` entries that declare them. **These assets must never flow to a `net10.0` + consumer** (a Docker-backed one) or `PCC0001` fires on Linux; they belong to the WSL backend package only. +- `Purview.Containers` ships `Sdk/buildTransitive/Purview.Containers.{props,targets}`, which turn + the `PurviewContainersBackends` list (each backend package appends its own entry) into a generated + module initializer in the consuming assembly. That is how a package consumer's process discovers its + backend without reflection or assembly scanning. A project-reference consumer (this repository's own + tests) registers explicitly instead — `WslcTest.EnsureBackendRegistered()`. - `src/Directory.Build.props` sets `EnableWindowsTargeting=true` because the shared CI agent is `ubuntu-latest`; removing it fails the pipeline with `NETSDK1100`. - The project is **experimental**: keep the experimental notice in `README.md`, `docs/wiki/Home.md` and @@ -85,7 +104,11 @@ installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitM strategy, and connection-string/endpoint accessors. It must not duplicate runtime infrastructure. - New modules follow the shape in [Contributing Modules](docs/wiki/Contributing-Modules.md): a `ContainerConfiguration`-derived record, a `ContainerBuilder` - subclass, and a container exposing `GetConnectionString()`/endpoints built from `GetMappedPublicPort`. + subclass, and a container deriving from `ContainerBase` that exposes + `GetConnectionString()`/endpoints built from `GetMappedPublicPort`. Take an `IContainerBackend` in the + builder and container constructors and pass it through as `new MyContainer(configuration, Backend)` — a `null` backend is resolved on start via `ContainerBackends.ResolveAsync()`; a + module package must never reference `Purview.Containers.Wsl` (that is what keeps it runnable on any + backend and restorable on Linux). - Prefer verifying the service for readiness (exec a readiness command or open a host client connection) over a bare TCP check. Where an image reports readiness too early (MySQL), a log match is wrong. - SQL Server requires an explicit `AcceptLicense()` call; never accept licensing terms on the caller's behalf. @@ -96,9 +119,13 @@ installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitM resolves `IsPackable` by scanning the project file, and the package metadata/icon conditions in `src/Directory.Build.props` depend on it. - Each package ships `lib/$(TFM)/.{dll,xml}`, `README.md` (from `Sdk/README.md`) and - `purview-logo-light.png`. PDBs ship only in the `.snupkg`. The core package additionally ships - `buildTransitive/Purview.WslContainers.{props,targets}` (see - [Consumer requirements and compatibility](#consumer-requirements-and-compatibility)). + `purview-logo-light.png`. PDBs ship only in the `.snupkg`. MSBuild assets are per package and must be + named after their own package id for NuGet to import them: `Purview.Containers` ships + `buildTransitive/Purview.Containers.{props,targets}` (backend registration), + `Purview.Containers.Wsl` ships `buildTransitive/Purview.Containers.Wsl.{props,targets}` (consumer + defaults and guards) and `Purview.Containers.Docker` ships + `buildTransitive/Purview.Containers.Docker.props` (its registration entry). See + [Consumer requirements and compatibility](#consumer-requirements-and-compatibility). - `PackValidation.RequireExplicitContent` defaults to `true`, so `purview-build.json`'s `RequiredContent` is the **exhaustive** manifest: a produced package with no rule, or a packed entry matched by no glob, fails validation. Adding or removing packaged content means updating that map. @@ -113,7 +140,7 @@ installation, plus the service modules (`PostgreSql`, `Redis`, `MsSql`, `RabbitM exclusively locks its image-store VHD, so parallel modules fail with `0x80070020`. - Use the shared `WslcTest` helper in `src/tests/SharedTestingFramework` for integration skip/availability checks. Unit tests may use the modules' `BuildConfigurationForTesting()` internal hook. -- `WslContainers.UnitTests/ConsumerRequirementsTests.cs` guards the published target framework +- `Wsl.UnitTests/ConsumerRequirementsTests.cs` guards the published target framework (`.NETCoreApp,Version=v11.0` plus `Windows10.0.19041.0`); keep it in step with [Consumer Requirements](docs/wiki/Consumer-Requirements.md). - `just verify-consumers` packs and then builds throwaway consumer projects to assert the documented diff --git a/Directory.Packages.props b/Directory.Packages.props index 610dc72..621c8fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,7 @@ + diff --git a/Justfile b/Justfile index d222b55..1008e65 100644 --- a/Justfile +++ b/Justfile @@ -121,6 +121,22 @@ verify-consumers *args: just pack pwsh -NoProfile -File scripts/verify-consumers.ps1 {{ args }} +# ----------------------------------------------------------------------------- +# Samples +# ----------------------------------------------------------------------------- + +# Runs the WSLC getting-started sample: needs WSL Containers, starts a real container +[group('Samples')] +sample-wsl: + echo "Running {{ BLUE }}samples/getting-started/WslSample{{ NORMAL }} (needs WSL Containers)..." + dotnet run --project samples/getting-started/WslSample/WslSample.csproj + +# Runs the Docker getting-started sample: needs a reachable Docker daemon, starts a real container +[group('Samples')] +sample-docker: + echo "Running {{ BLUE }}samples/getting-started/DockerSample{{ NORMAL }} (needs a Docker daemon)..." + dotnet run --project samples/getting-started/DockerSample/DockerSample.csproj + # ----------------------------------------------------------------------------- # Formatting # ----------------------------------------------------------------------------- diff --git a/README.md b/README.md index 61a5e0d..4116269 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,51 @@ -# Purview.WslContainers +# Purview.Containers -[![NuGet version](https://img.shields.io/nuget/v/Purview.WslContainers.svg)](https://www.nuget.org/packages/Purview.WslContainers) +[![NuGet version](https://img.shields.io/nuget/v/Purview.Containers.Wsl.svg)](https://www.nuget.org/packages/Purview.Containers.Wsl) [![Release](https://github.com/purview-dev/wsl-containers/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/wsl-containers/actions/workflows/release.yml) -A **WSLC-native** Testcontainers-style library for .NET that runs throwaway Linux containers for integration testing on **Microsoft WSL Containers (WSLC)** — with no Docker installation. +A Testcontainers-style library for .NET that runs throwaway Linux containers for integration testing — on **Microsoft WSL Containers (WSLC)** with no Docker installation, or on **Docker** through Testcontainers. -Built directly against the `Microsoft.WSL.Containers` NuGet package (the WSLC managed C# API). No `wslc.exe`/`wsl.exe`/`docker` CLI, no Docker.DotNet, no Testcontainers internally. +> **Two backends, one API.** Containers are created through the backend-neutral `Purview.Containers` +> abstractions, so the same test suite runs on **WSL Containers** (`Purview.Containers.Wsl`) or +> **Docker** (`Purview.Containers.Docker`, driven by Testcontainers). Selection is automatic by default +> and can be pinned with `PURVIEW_CONTAINERS_BACKEND` — which is how a developer machine uses WSLC and a +> Linux CI runner uses Docker without changing a line of test code. +> +> **[Backends: WSLC or Docker](docs/wiki/Backends.md)** — comparison, side-by-side project setup, CI +> example and troubleshooting. + +The WSLC backend is built directly against the `Microsoft.WSL.Containers` NuGet package (the WSLC managed C# API) — no `wslc.exe`/`wsl.exe`/`docker` CLI. The Docker backend is built on [Testcontainers for .NET](https://dotnet.testcontainers.org/). > **Experimental.** This project is an experiment in running throwaway containers through Microsoft > WSL Containers. The public API, defaults and packaging rules can change between prereleases, and > there is no production support guarantee. Pin the exact package version you build against, and read > [Consumer Requirements](docs/wiki/Consumer-Requirements.md) before adopting it. -> **Status: Phase 8 in progress.** Core runtime, wait strategies, the `Image`/`Tag` parser, registry auth, -> observability, hardening, and **seven service modules** (PostgreSQL, Redis, SQL Server, RabbitMQ, Azurite, -> NATS, MySQL). **Images are shared by default** (`StorageMode.Shared`): sessions reuse a stable -> image store (`%LOCALAPPDATA%\Purview\WslContainers\images`) so images are pulled once, not per session; -> `StorageMode.PerSession` provides isolation. 76 unit tests pass across all modules. +> **Status: preview.** Backend-neutral abstractions, two backends (WSL Containers and Docker), wait +> strategies, the `Image`/`Tag` parser, registry auth, observability, hardening, and **seven service +> modules** (PostgreSQL, Redis, SQL Server, RabbitMQ, Azurite, NATS, MySQL). On WSLC, **images are shared +> by default** (`StorageMode.Shared`): sessions reuse a stable image store +> (`%LOCALAPPDATA%\Purview\WslContainers\images`) so images are pulled once, not per session; +> `StorageMode.PerSession` provides isolation. Runnable samples live in `samples/getting-started`. ## Prerequisites -- Windows 10/11. -- **WSL Containers**, installed via `wsl --install --no-distribution` (verified against WSL 3.0.1.0). -- .NET SDK 11 (the repo pins `11.0.100-rc.1`). -- A **consuming project must be a .NET 11 project that targets Windows specifically** — - `net11.0-windows10.0.19041.0`, x64 or arm64. The packages ship MSBuild defaults for the supporting - settings; the full contract, the exact errors raised when it is not met, and the - `EnableWindowsTargeting` workaround for non-Windows CI agents are documented in - [Consumer Requirements](docs/wiki/Consumer-Requirements.md). +Pick a backend — see [Backends: WSLC or Docker](docs/wiki/Backends.md) for the comparison. + +**WSL Containers backend** + +- Windows 10/11 with **WSL Containers**, installed via `wsl --install --no-distribution` (verified against + WSL 3.0.1.0). +- A consuming project that is a **.NET 11 project targeting Windows specifically** — + `net11.0-windows10.0.19041.0`, x64 or arm64. The `Purview.Containers.Wsl` package ships MSBuild defaults + for the supporting settings. + +**Docker backend** + +- Any reachable Docker daemon, and a `net10.0` (or later) project on any platform. + +.NET SDK 11 is required to build this repository (it pins `11.0.100-rc.1`); SDK 10 is enough for a Docker +consumer. Verify: @@ -36,11 +54,21 @@ wsl --version # WSL Containers installed (verified against 3.0.1.0) wslc version # e.g. 3.0.1.0 ``` -The library reports missing prerequisites via `WslContainerRuntime.GetInfoAsync()`; it never installs or updates WSL on its own. +```bash +docker info # a Docker daemon the tests can reach +``` + +The library reports missing prerequisites via `WslContainerRuntime.GetInfoAsync()` (WSLC) or +`DockerContainerBackend.GetInfoAsync()` (Docker); it never installs a runtime on its own. + +The full consumer contract, the exact errors raised when it is not met, and the `EnableWindowsTargeting` +workaround for non-Windows CI agents are documented in +[Consumer Requirements](docs/wiki/Consumer-Requirements.md). ## Target developer experience -The generic container API below is **implemented and working**: +The generic container API below is **implemented and working** — and the identical code runs on either +backend (see [Backends: WSLC or Docker](docs/wiki/Backends.md)): ```csharp await using var container = new ContainerBuilder() @@ -104,28 +132,28 @@ string amqp = rabbitMq.GetConnectionString(); ``` src/ - WslContainers/ core runtime (Containers, Images, Runtime, Networking, Mounts, Diagnostics) - WslContainers.PostgreSql/ PostgreSQL module (builder, container, Npgsql connection string) - WslContainers.Redis/ Redis module (builder, container, StackExchange.Redis connection string) - WslContainers.MsSql/ SQL Server module (builder, container, SqlClient connection string) - WslContainers.RabbitMq/ RabbitMQ module (builder, container, AMQP + management endpoints) - WslContainers.Azurite/ Azurite module (builder, container, blob/queue/table endpoints) - WslContainers.Nats/ NATS module (builder, container, client + monitoring endpoints) - WslContainers.MySql/ MySQL module (builder, container, MySqlConnector connection string) + Wsl/ core runtime + WSL Containers backend (package Purview.Containers.Wsl) + PostgreSql/ PostgreSQL module (builder, container, Npgsql connection string) + Redis/ Redis module (builder, container, StackExchange.Redis connection string) + MsSql/ SQL Server module (builder, container, SqlClient connection string) + RabbitMq/ RabbitMQ module (builder, container, AMQP + management endpoints) + Azurite/ Azurite module (builder, container, blob/queue/table endpoints) + Nats/ NATS module (builder, container, client + monitoring endpoints) + MySql/ MySQL module (builder, container, MySqlConnector connection string) tests/ SharedTestingFramework/ shared WSLC skip/helper for integration tests - WslContainers.UnitTests/ - WslContainers.IntegrationTests/ - WslContainers.PostgreSql.UnitTests/ - WslContainers.PostgreSql.IntegrationTests/ - WslContainers.Redis.UnitTests/ - WslContainers.Redis.IntegrationTests/ - WslContainers.MsSql.UnitTests/ - WslContainers.MsSql.IntegrationTests/ - WslContainers.RabbitMq.UnitTests/ - WslContainers.RabbitMq.IntegrationTests/ - WslContainers.Azurite.UnitTests/ - WslContainers.Azurite.IntegrationTests/ + Wsl.UnitTests/ + Wsl.IntegrationTests/ + PostgreSql.UnitTests/ + PostgreSql.IntegrationTests/ + Redis.UnitTests/ + Redis.IntegrationTests/ + MsSql.UnitTests/ + MsSql.IntegrationTests/ + RabbitMq.UnitTests/ + RabbitMq.IntegrationTests/ + Azurite.UnitTests/ + Azurite.IntegrationTests/ spikes/ WslcSpikes/ Phase 0 investigation harness (run: see below) docs/ @@ -142,7 +170,8 @@ The project documentation lives in [`docs/wiki`](docs/wiki/Home.md) and is publi (`mkdocs.yml`, `docs_dir: docs/wiki`, aggregated by the purview-dev website): - [Getting Started](docs/wiki/Getting-Started.md) — prerequisites, first container, first module. -- [Consumer Requirements](docs/wiki/Consumer-Requirements.md) — the .NET 11 + Windows target framework contract, the `PWC0001`/`PWC0002` guards, and the CI workarounds. +- [Backends: WSLC or Docker](docs/wiki/Backends.md) — how to choose, side-by-side project setup, CI example, troubleshooting. +- [Consumer Requirements](docs/wiki/Consumer-Requirements.md) — the .NET 11 + Windows target framework contract, the `PCC0001`/`PCC0002` guards, and the CI workarounds. - [Architecture](docs/wiki/Architecture.md) — the shared session model, concurrency and cleanup decisions. - [Lifecycle](docs/wiki/Lifecycle.md), [Networking](docs/wiki/Networking.md), [Wait Strategies](docs/wiki/Wait-Strategies.md). - [Modules](docs/wiki/Modules.md) — the module contract and every shipped module. @@ -151,7 +180,7 @@ The project documentation lives in [`docs/wiki`](docs/wiki/Home.md) and is publi - [Contributing](docs/wiki/Contributing.md) and [Contributing Modules](docs/wiki/Contributing-Modules.md). Every package also ships its own `README.md` (from `src/src//Sdk/README.md`), so -`dotnet add package Purview.WslContainers.` brings documentation specific to that package. +`dotnet add package Purview.Containers.` brings documentation specific to that package. The PostgreSQL, Redis, SQL Server and RabbitMQ modules work today: @@ -233,18 +262,25 @@ just test # dotnet test, one test module at a time # "Maximum Parallel Test Projects" to 1) before running the WSLC integration tests. ``` -> The `WslContainers.IntegrationTests` module runs 27 real containers in one session and takes ~4 +To watch a container start end to end, run a sample — the same code, on either backend: + +```powershell +just sample-wsl # needs WSL Containers +just sample-docker # needs a Docker daemon +``` + +> The `Wsl.IntegrationTests` module runs 27 real containers in one session and takes ~4 > minutes because WSLC serialises container operations; expect slow-test warnings while it runs. ### Verifying the consumer contract ```powershell -just verify-consumers # pack, then build 12 throwaway consumer projects against ./artifacts +just verify-consumers # pack, then build 16 throwaway consumer projects against ./artifacts just verify-consumers -Keep # same, keeping the generated projects for inspection ``` `just verify-consumers` asserts every claim in [Consumer Requirements](docs/wiki/Consumer-Requirements.md): -the happy path, the shipped `buildTransitive` defaults, the `PWC0001`/`PWC0002` guards, and the +the happy path, the shipped `buildTransitive` defaults, the `PCC0001`/`PCC0002` guards, and the non-.NET-11 escape hatch that deliberately does not work. It needs network access and is therefore a local step rather than part of the `[Category=Unit]` pipeline filter. diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md index 572d7fe..f216c8a 100644 --- a/docs/wiki/Architecture.md +++ b/docs/wiki/Architecture.md @@ -1,29 +1,63 @@ # Architecture -Design for a WSLC-native Testcontainers-style .NET library: `Purview.WslContainers`. +Design for a Testcontainers-style .NET library with pluggable container backends: `Purview.Containers`. ## Core model +The library is split into a backend-neutral abstraction assembly and one assembly per backend: + ``` -WslContainerRuntime (process singleton, IAsyncDisposable) - ├─ SessionSettings (name, storagePath, cpu/mem, gpu, timeout) - ├─ SemaphoreSlim -> serialises session-mutating and container-lifecycle ops - ├─ ImageCatalog -> pull policies + keyed dedup of concurrent pulls - ├─ PortAllocator -> native random host ports (windowsPort=0) - └─ SessionHandle -> Microsoft.WSL.Containers.Session (internal) - -IContainerRuntime - Task GetInfoAsync(CancellationToken ct = default) - Task GetSessionAsync(CancellationToken ct = default) - Task InitializeAsync(...) +Purview.Containers (net10.0, portable) + ├─ IContainer / IContainerConfiguration / ContainerConfiguration the container contract + ├─ ContainerBuilder fluent configuration + validation + ├─ ContainerBase typed module container (delegates to the backend) + ├─ ContainerBackends + IContainerBackend backend registry and selection + ├─ Waiting / Images / Mounts / Networking / Diagnostics readiness, model, secrets + └─ Runtime/ContainerException neutral error taxonomy + +Purview.Containers.Wsl (net11.0-windows10.0.19041.0) + ├─ WslContainerBackend : IContainerBackend registers itself as "wsl" + ├─ WslContainerRuntime : IContainerRuntime process singleton, owns the shared session + │ ├─ SessionSettings (name, storagePath, cpu/mem, gpu, timeout) + │ ├─ SemaphoreSlim -> serialises session-mutating and container-lifecycle ops + │ ├─ ImageCatalog -> pull policies + keyed dedup of concurrent pulls + │ ├─ PortAllocator -> native random host ports (windowsPort=0) + │ └─ SessionHandle -> Microsoft.WSL.Containers.Session (internal) + ├─ WslContainer : IContainer WSLC-backed container + └─ WslContainerSession : IContainerSession image pull + container create/start/stop/delete/exec IContainer : IAsyncDisposable StartAsync / StopAsync / DisposeAsync / ExecAsync / GetMappedPublicPort / GetLogsAsync / tailing IAsyncEnumerable ``` +A module (`Purview.Containers.PostgreSql`, `Purview.Containers.Redis`, …) derives its container from +`ContainerBase` and references `Purview.Containers` only. The backend is resolved at run time through +`ContainerBackends`, so the same module package works on WSLC or Docker. + Microsoft types (`Session`, `Container`, `Process`, `ContainerSettings`, …) are **runtime implementation details** kept behind the public interfaces. They are not exposed through the public API surface (an opt-in accessor is the only escape hatch). +## Backend selection + +`ContainerBackends` is the process-wide registry; `ResolveAsync()` chooses the backend with this +precedence: + +1. **Pinned instance** — `ContainerBackends.Use(new WslContainerBackend())`, or `WithBackend(...)` on a + single builder. Used as-is: never probed, never substituted. +2. **Named backend** — `PURVIEW_CONTAINERS_BACKEND=wsl|docker|` (or + `Use(ContainerBackendSelection.Named(...))`). The named backend is probed: an unknown name lists what is + registered, and an unusable one fails with its own diagnostics. There is no fallback. +3. **Automatic detection** — every registered backend is probed in registration order and the first + *available and compatible* one wins. When none is usable the exception lists each backend's + availability, version and missing components, plus the `PURVIEW_CONTAINERS_BACKEND` values that would + work. + +Resolution is cached per process, so probes run once; `Reset()` (tests) and `Register`/`Use` invalidate the +cache. Package consumers get registration from the generated module initializer in +`Purview.Containers.targets`; project-reference consumers register explicitly with +`Register(...)`. Consumer-facing guidance (including the CI example) is in +[Backends: WSLC or Docker](Backends.md). + ## Session lifetime model (chosen after spikes) **One shared, process-wide session.** Findings that drove this: @@ -37,7 +71,7 @@ Design rules: 1. **One lazily-started session per process** with a deterministic name `wslc-{processId}-{8 hex}` (unique per machine; session names are reserved until the session is disposed — EXP S1/S14) and a **stable storage path** (default `%LOCALAPPDATA%\Purview.WslContainers\sessions\{name}\`), configurable. 2. The session VM is capped at **4096 MB by default** (`WslContainerRuntimeOptions.Default`). This is required for SQL Server (which refuses to start below 2000 MB — `sqlservr: This program requires a machine with at least 2000 megabytes of memory`) and harmless for lighter containers. Override via `WslContainerRuntimeOptions.MemorySizeInMB`. -2. `IContainerRuntime` is the seam so advanced users/tests can substitute a per-container-session runtime for isolation experiments. +2. `IContainerBackend` is the seam for backends: a backend package (starting with `Purview.Containers.Wsl`) supplies `IContainer` instances, and `ContainerBackends` resolves which one runs. `IContainerRuntime` remains the WSLC-internal seam so advanced users/tests can substitute a per-container-session runtime for isolation experiments. 3. Container `DisposeAsync` never terminates the shared session; it deletes the container only. 4. The runtime registers a **process-exit handler** and `IAsyncDisposable` to `Terminate()`+`Dispose()` the session at shutdown. @@ -46,7 +80,7 @@ Design rules: - Name: `wslc-{pid}-{random8}`. Never place secrets/credentials in names, paths, or logs. - **Storage is shared by default** (`StorageMode.Shared`): all sessions use `%LOCALAPPDATA%\Purview.WslContainers\images`, so the image store is pulled once and reused across - process runs. Set `StorageMode.PerSession` (or an explicit `StoragePath` / `WSL_CONTAINERS_STORAGE_PATH`) + process runs. Set `StorageMode.PerSession` (or an explicit `StoragePath` / `PURVIEW_CONTAINERS_STORAGE_PATH`) for isolation. Session names stay unique per process; only the path is shared. - **Concurrent sharing is not possible**: a running session exclusively locks its `storage.vhdx` (a second session on the same path fails with `0x80070020` — EXP S18). The lock is taken lazily on the @@ -68,7 +102,7 @@ Design rules: - **Random host port = native `windowsPort=0`** (race-free, EXP S4). The assigned port is read from `Inspect().Ports`. - Explicit host ports are bound by WSLC at `Start`; a conflict throws `0x80072740` (surface as a clear `WslContainerPortInUseException`). - Default host bind is IPv4 loopback only; IPv6 is not mapped (EXP S12). -- UDP → `WslContainerNotSupportedException`. +- UDP → `ContainerNotSupportedException`. ## Concurrency @@ -83,7 +117,7 @@ on the same path can start its session successfully and only fail later on its f (`GetImages()`) with `0x80070020`. The runtime therefore verifies the store once, under a gate, on the first `GetSessionAsync`; when a concurrent process holds the default shared store, that session is discarded and the runtime transparently switches to an isolated per-process store. Explicit -`StoragePath`/`WSL_CONTAINERS_STORAGE_PATH`/`StorageMode.PerSession` configuration opts out of the +`StoragePath`/`PURVIEW_CONTAINERS_STORAGE_PATH`/`StorageMode.PerSession` configuration opts out of the fallback. Isolated stores are transient and are removed when their session terminates. ## Cleanup & reaper decision @@ -97,7 +131,7 @@ fallback. Isolated stores are transient and are removed when their session termi ## Observability -- `System.Diagnostics.ActivitySource("Purview.WslContainers")` emits `wslcontainer.session.start`, `wslcontainer.image.pull`, `wslcontainer.container.create/start/stop/delete`, `wslcontainer.exec.create`, and `wslcontainer.wait`. Tags carry container name/id/image and strategy — never credentials. +- `System.Diagnostics.ActivitySource("Purview.Containers")` emits `wslcontainer.session.start`, `wslcontainer.image.pull`, `wslcontainer.container.create/start/stop/delete`, `wslcontainer.exec.create`, and `wslcontainer.wait`. Tags carry container name/id/image and strategy — never credentials. - Tailing `GetLogsAsync(CancellationToken)` ends when the container's init process exits: the log buffer is flushed and its channel completed on process exit (and on container disposal), so an `await foreach` over the stream terminates instead of waiting forever. The string overload (`GetLogsAsync(stream, ct)`) reads the accumulated buffer only. - `Microsoft.Extensions.Logging` integration is optional/future; the core works without a host or DI. diff --git a/docs/wiki/Backends.md b/docs/wiki/Backends.md new file mode 100644 index 0000000..6b8bb38 --- /dev/null +++ b/docs/wiki/Backends.md @@ -0,0 +1,392 @@ +# Backends: WSL Containers or Docker + +`Purview.Containers` has **one API and two interchangeable backends**. The same test code, the same service +modules (`Purview.Containers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats`, `MySql`), and +the same connection-string accessors run on either runtime — you choose which one, in code or from the +environment. + +- `**Purview.Containers.Wsl**` — throwaway containers on **Microsoft WSL Containers (WSLC)**, the Windows + runtime with no Docker installation. +- `**Purview.Containers.Docker**` — throwaway containers on **any reachable Docker daemon**, driven by + Testcontainers. + +## At a glance + +| | WSL Containers | Docker | +| --- | --- | --- | +| **Package** | `Purview.Containers.Wsl` | `Purview.Containers.Docker` | +| **Prerequisite** | Windows 10/11 with WSL Containers (`wsl --install --no-distribution`) | any reachable Docker daemon (Docker Desktop, Docker Engine in WSL2, a VM, or a CI runner) | +| **Host OS** | Windows only | Windows, Linux, macOS | +| **Project target framework** | `net11.0-windows10.0.19041.0`, x64 or arm64 | `net10.0` or later, any platform | +| **How containers run** | the `Microsoft.WSL.Containers` managed API (daemonless) | the Docker Engine API via Testcontainers | +| **Images** | a shared store (`%LOCALAPPDATA%\Purview\WslContainers\images`) reused across runs | the daemon's own image store | +| **Leak protection** | session disposal plus a process-exit hook | the Testcontainers resource reaper (Ryuk) | +| **Check the host** | `wsl --version`, `wslc version` | `docker info` | +| **Typical fit** | local Windows development without Docker Desktop, fastest cold start | CI runners, non-Windows hosts, teams already running Docker | + +Switch between them without touching test code: + +```bash +# auto (the default) | wsl | docker | +export PURVIEW_CONTAINERS_BACKEND=docker # bash / zsh / CI +``` + +```powershell +$env:PURVIEW_CONTAINERS_BACKEND = 'wsl' # PowerShell +``` + +## Verify the host before you rely on it + +Each backend reports its own readiness, so a missing runtime is a clear message instead of a timeout deep +inside a test. + +WSL Containers: + +```powershell +wsl --version # WSL itself +wslc version # the WSLC runtime +``` + +```csharp +using Purview.Containers.Wsl; + +var info = await new WslContainerBackend().GetInfoAsync(); +Console.WriteLine($"{info.Name}: usable={info.IsUsable} version={info.Version}"); + +if (!info.IsUsable) +{ + Console.WriteLine(string.Join("; ", info.MissingComponents)); +} +``` + +Docker: + +```bash +docker info +``` + +```csharp +using Purview.Containers.Docker; + +var info = await new DockerContainerBackend().GetInfoAsync(); +Console.WriteLine($"{info.Name}: usable={info.IsUsable} version={info.Version}"); + +if (!info.IsUsable) +{ + Console.WriteLine(string.Join("; ", info.MissingComponents)); +} +``` + +`ContainerBackends.ProbeAllAsync()` reports every registered backend at once, which is the quickest way to +answer "what can this machine run?": + +```csharp +foreach (var backend in await ContainerBackends.ProbeAllAsync()) +{ + Console.WriteLine($"{backend.Name}: usable={backend.IsUsable} version={backend.Version}"); +} +``` + +## The same test, either backend + +The test body never names a backend, so it is identical on both runtimes: + +```csharp +using Purview.Containers; +using Purview.Containers.Waiting; + +public class CacheTests +{ + [Test] + public async Task Cache_IsReachableOnItsMappedPort() + { + await using var container = new ContainerBuilder() + .WithImage("redis:7") + .WithPortBinding(6379, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForTcpPort(6379)) + .Build(); + + await container.StartAsync(); + + ushort port = container.GetMappedPublicPort(6379); + + await Assert.That(port).IsGreaterThan((ushort)0); + } +} +``` + +The **package reference** decides which runtime executes it. Three project shapes cover every case. + +### Option 1 — WSLC on a Windows machine + +```bash +dotnet add package Purview.Containers.Wsl +``` + +```xml + + + net11.0-windows10.0.19041.0 + x64 + enable + enable + + + + + +``` + +`WindowsSdkPackageVersion` is supplied by the package. A project that targets anything other than a .NET 11 +Windows framework fails the build with `PCC0001`, and a 32-bit consumer with `PCC0002`; see +[Consumer Requirements](Consumer-Requirements.md). + +### Option 2 — Docker anywhere + +```bash +dotnet add package Purview.Containers.Docker +``` + +```xml + + + net10.0 + enable + enable + + + + + +``` + +That project restores and builds on Linux, macOS and Windows — no Windows target framework, no +`WindowsSdkPackageVersion`, no Docker Desktop licence requirement beyond the daemon you already run. + +### Option 3 — one project, both backends + +Multi-target, and reference each backend only for the framework it supports: + +```bash +dotnet add package Purview.Containers.Wsl +dotnet add package Purview.Containers.Docker +``` + +```xml + + + net11.0-windows10.0.19041.0;net10.0 + enable + enable + + + + + + + + +``` + +On a Windows host that also runs Docker, reference **both** packages for the same framework and let the +selection policy choose (WSLC first, Docker next): + +```xml + + + + +``` + +If your code needs a backend-specific API (for example `WslContainerRuntime`, or +`WslContainerBackend(runtime)` to pin a specific WSLC session) guard it so the portable target still +compiles: + +```csharp +#if WINDOWS +using Purview.Containers.Wsl; +#endif + +// ... +#if WINDOWS +var backend = new WslContainerBackend(runtimeForIsolation); +#else +var backend = new DockerContainerBackend(); +#endif +``` + +### Typed modules work the same way + +A module is backend-neutral, so only the backend package line changes: + +```bash +dotnet add package Purview.Containers.PostgreSql # the module +dotnet add package Purview.Containers.Wsl # ...or Purview.Containers.Docker +``` + +```csharp +using Npgsql; +using Purview.Containers.PostgreSql; + +await using var postgres = new PostgreSqlBuilder() + .WithDatabase("tests") + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + +await postgres.StartAsync(); // ready when pg_isready succeeds, on either backend + +await using var connection = new NpgsqlConnection(postgres.GetConnectionString()); +await connection.OpenAsync(); +``` + +## Choosing at run time + +Selection is resolved once per process, in this order: + +| # | Rule | How to set it | Behaviour | +| --- | --- | --- | --- | +| 1 | Pinned instance | `ContainerBackends.Use(new DockerContainerBackend())`, or `WithBackend(...)` on one builder | used as-is: never probed, never substituted | +| 2 | Named backend | `PURVIEW_CONTAINERS_BACKEND=wsl\|docker\|`, or `ContainerBackends.Use(ContainerBackendSelection.Named("docker"))` | probed; a missing or unusable backend fails with its own diagnostics and **no fallback** | +| 3 | Automatic detection | the default (`auto`) | every registered backend is probed in registration order; the first *available and compatible* one wins (WSLC before Docker) | + +```csharp +using Purview.Containers; + +// Automatic detection (the default)... +var container = new ContainerBuilder().WithImage("alpine:3.19").Build(); + +// ...or pin it for this builder only. +var pinned = new ContainerBuilder() + .WithBackend(new DockerContainerBackend()) + .WithImage("alpine:3.19") + .Build(); + +// ...or pin it for the process. +ContainerBackends.Use(ContainerBackendSelection.Named("docker")); +``` + +`Build()` never needs a backend: it validates the configuration and returns a container that resolves the +backend when it starts. That is what lets the same test suite run on whichever runtime the machine has. + +To fail loudly instead of falling back — the usual choice in CI — name the backend: + +``` +PURVIEW_CONTAINERS_BACKEND=docker # if Docker is not reachable, the test fails and says why +``` + +## In CI + +A hosted Linux runner already has a Docker daemon, so nothing has to be started alongside your tests — +reference `Purview.Containers.Docker` in a `net10.0` test project and run `dotnet test`: + +```yaml +name: tests + +on: [push, pull_request] + +jobs: + integration-linux: + name: Integration tests (Docker) + runs-on: ubuntu-latest + env: + # Optional. "auto" (the default) also selects Docker when WSL Containers is absent. + PURVIEW_CONTAINERS_BACKEND: docker + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - run: dotnet restore + - run: dotnet build --configuration Release --no-restore + - run: dotnet test --configuration Release --no-build +``` + +- Target `net10.0` or later and never `net11.0-windows…` on a Linux job; the Docker backend is portable and + the WSLC backend package is Windows-only. +- Keep your integration tests in their own project or category if you want the fast unit tests to stay + runtime-free; both can run in the same job. +- Pinning `PURVIEW_CONTAINERS_BACKEND=docker` makes a runner without a usable daemon **fail loudly** with + the probe report instead of quietly selecting something else. + +For a **Windows** job that should exercise WSLC, run the same code against `Purview.Containers.Wsl`: + +```yaml + integration-windows: + name: Integration tests (WSL Containers) + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 11.0.x + - run: dotnet test --configuration Release +``` + +> WSL Containers is a Windows developer runtime: hosted GitHub runners do not guarantee a WSL Containers +> installation, so the practical options are a **self-hosted Windows runner**, or running WSLC suites +> locally and Docker suites in CI. Either way the test code is the same. + +## What differs in practice + +| Capability | WSL Containers | Docker | +| --- | --- | --- | +| Random host ports | ✅ native (`windowsPort=0`) | ✅ assigned by the daemon | +| Fixed host ports | ✅ | ✅ | +| IPv6 host mapping | ❌ IPv4 loopback only | ✅ | +| UDP port mappings | ❌ `ContainerNotSupportedException` | ✅ | +| Bind mounts of host directories | ✅ | ✅ | +| Named volumes | ✅ (session VHD) | ✅ (Docker volumes) | +| GPU exposure | ✅ (`EnableGPU` session setting) | depends on the daemon and host runtime | +| `ExecOptions.WorkingDirectory` / `Timeout` | ✅ | ✅ | +| `ExecOptions.Environment` | ✅ | ❌ `ContainerNotSupportedException` | +| Log tailing | streamed while the init process runs | polled until the container stops | +| Leak protection | session disposal + process-exit hook | Testcontainers resource reaper (Ryuk) | +| Image store | shared WSLC store, warm across runs | the daemon's store | +| Lifetime cost | one process-wide session, cheap start | first pull per image, per daemon | + +Unsupported options throw `ContainerNotSupportedException` rather than being ignored, so a difference +between backends never turns into a silently weaker test. + +## Troubleshooting + +**`No container backend is registered.`** — no backend package is referenced by the test project (and the +assembly that would register it was never loaded). Add one: +`dotnet add package Purview.Containers.Docker` or `dotnet add package Purview.Containers.Wsl`. + +**`No usable container backend was found (selection: auto).`** — every registered backend failed its probe; +the report names each one and why: + +```text +No usable container backend was found (selection: auto). + wsl: unavailable (Sdk; SdkNeedsUpdate) + docker: unavailable (HttpRequestException: Connection refused) +Install or fix a backend, or set PURVIEW_CONTAINERS_BACKEND to one of: wsl, docker. +``` + +Fix the runtime it names (for WSLC: `wsl --install --no-distribution`; for Docker: start the daemon, or +point `DOCKER_HOST` at it), or pin the backend that does work. + +**`The selected container backend 'docker' is not usable: …`** — you named a backend that cannot run here. +This is intentional: a named backend never falls back to another one, so a CI job cannot pass by silently +using the wrong runtime. + +**`The container backend 'docker' is not registered. Registered backends: wsl.`** — the selection names a +backend whose package is not referenced by the project. + +**`PCC0001` / `PCC0002`** — a build-time guard from the WSL Containers backend package: the consuming project +does not target a .NET 11 Windows framework, or is 32-bit. See +[Consumer Requirements](Consumer-Requirements.md), or switch the project to the Docker backend. + +**The same image is pulled twice** — expected when both runtimes are used: WSLC and Docker keep separate +image stores. + +## Related + +- [Getting Started](Getting-Started.md) — install, first container, first typed module. +- [Consumer Requirements](Consumer-Requirements.md) — the .NET 11 Windows contract, the guards and the workarounds. +- [Architecture](Architecture.md#backend-selection) — how resolution and registration work internally. +- [Modules](Modules.md) — the service modules and their readiness strategies. + + + diff --git a/docs/wiki/Consumer-Requirements.md b/docs/wiki/Consumer-Requirements.md index a6cc670..eb79d23 100644 --- a/docs/wiki/Consumer-Requirements.md +++ b/docs/wiki/Consumer-Requirements.md @@ -1,15 +1,29 @@ # Consumer Requirements -> **Experimental.** `Purview.WslContainers.*` is an experimental project: the public API, the +> **Experimental.** `Purview.Containers.*` is an experimental project: the public API, the > defaults and these requirements can change between prereleases, and there is no production > support guarantee. Treat every version as a preview and pin the exact package version you build > against. -Every package targets `net11.0-windows10.0.19041.0` and is built on the +The WSL Containers backend package targets `net11.0-windows10.0.19041.0` and is built on the `Microsoft.WSL.Containers` WinRT projection, so a consuming project **must be a .NET 11 (or later) project that targets Windows specifically**. This page is the authoritative statement of that contract, of the workarounds that exist for it, and of how each of them is verified. +## Which package requires what + +| Package | Target framework | Notes | +| --- | --- | --- | +| `Purview.Containers` | `net10.0`, any platform | Backend-neutral abstractions. | +| `Purview.Containers.` | `net10.0`, any platform | Service modules. Restore anywhere; needs a backend package to actually run. | +| `Purview.Containers.Wsl` | `net11.0-windows10.0.19041.0` | The WSL Containers backend. **The requirements on this page are its contract.** | +| `Purview.Containers.Docker` | `net10.0`, any platform | The Docker backend (Testcontainers). Needs a reachable Docker daemon, not a Windows target framework. | + +Everything below describes the **WSL Containers backend**. A consumer of a different backend (for +example `Purview.Containers.Docker`) needs neither .NET 11 nor a Windows target framework: the +abstractions and the service modules are portable `net10.0` assets. For how to use and switch between the +backends, see [Backends: WSLC or Docker](Backends.md). + ## Copy this into your project ```xml @@ -29,7 +43,7 @@ file, and because the defaults only fill in a value when you have not chosen one | # | Requirement | Why it exists | | --- | --- | --- | -| 1 | **.NET 11 or later** | Every `lib/` asset in every package is built for `net11.0-windows10.0.19041.0`. | +| 1 | **.NET 11 or later** (the WSL Containers backend only) | `Purview.Containers.Wsl` ships only `lib/net11.0-windows10.0.19041/` assets. `Purview.Containers` and the service modules ship `lib/net10.0/` and restore on any .NET 10+ project. | | 2 | **A Windows-specific target framework that names the OS version** (`net11.0-windows10.0.19041.0` or later) | The `Microsoft.WSL.Containers` projection only ships Windows assets. `net11.0-windows` on its own is not enough. | | 3 | **64-bit** (`PlatformTarget` `x64`/`arm64`, or a matching `RuntimeIdentifier`) | The native `wslcsdk.dll` ships for `win-x64` and `win-arm64` only. | | 4 | **`WindowsSdkPackageVersion` at least `10.0.26100.80`** | The projection is compiled against `Microsoft.Windows.SDK.NET` `10.0.26100.79`; the pack the SDK resolves for a `10.0.19041.0` target framework is older. | @@ -63,7 +77,7 @@ The native WSL Containers SDK is shipped for `win-x64` and `win-arm64` only. A 3 consumer builds but cannot load `wslcsdk.dll` at runtime, so the packages fail the build instead: ```text -error PWC0002: Purview.WslContainers requires a 64-bit (x64 or arm64) consumer ... +error PCC0002: Purview.Containers.Wsl requires a 64-bit (x64 or arm64) consumer ... ``` ### 4. Windows SDK targeting pack version @@ -88,15 +102,19 @@ with WSL Containers installed (`wsl --install --no-distribution`) — see ## What the packages do for you -`Purview.WslContainers` ships two `buildTransitive` MSBuild files. NuGet imports them for **direct -references and transitive references**, so every module package -(`Purview.WslContainers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats`, `MySql`) gets -them too through its dependency on the core package. +`Purview.Containers.Wsl` ships two `buildTransitive` MSBuild files. NuGet imports them for **direct and +transitive references**, so a project that references the backend package directly — or through another +project that does — gets them automatically. + +> A **service module** (`Purview.Containers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats`, +> `MySql`) is backend-neutral and does **not** bring a backend, so reference the module **and** +> `Purview.Containers.Wsl` (or another backend package) to run it. Use `Purview.Containers.Docker` to run +> the same module on Docker, and `PURVIEW_CONTAINERS_BACKEND` to choose between them. | Package path | Effect | | --- | --- | -| `buildTransitive/Purview.WslContainers.props` | Defaults `WindowsSdkPackageVersion` to `10.0.26100.80` when the consumer has not set it. | -| `buildTransitive/Purview.WslContainers.targets` | Defaults `PlatformTarget` to `x64` when it is unset (or `AnyCPU`) and no `RuntimeIdentifier` is selected, and fails the build with `PWC0001`/`PWC0002` when the target framework or the platform cannot be supported. | +| `buildTransitive/Purview.Containers.Wsl.props` | Defaults `WindowsSdkPackageVersion` to `10.0.26100.80` when the consumer has not set it. | +| `buildTransitive/Purview.Containers.Wsl.targets` | Defaults `PlatformTarget` to `x64` when it is unset (or `AnyCPU`) and no `RuntimeIdentifier` is selected, and fails the build with `PCC0001`/`PCC0002` when the target framework or the platform cannot be supported. | A consumer therefore only has to choose a target framework: @@ -106,7 +124,7 @@ A consumer therefore only has to choose a target framework: net11.0-windows10.0.19041.0 - + ``` @@ -116,19 +134,19 @@ always wins over these defaults. > Because `buildTransitive` assets are framework-agnostic, NuGet no longer reports `NU1202` for an > incompatible target framework: restore succeeds and the failure comes from the guard targets -> instead. That is deliberate — `PWC0001` names the required target framework, whereas an empty +> instead. That is deliberate — `PCC0001` names the required target framework, whereas an empty > compile-asset set only produces `CS0246` errors in your own code. ## Error reference | Error | Raised by | Meaning | Fix | | --- | --- | --- | --- | -| `PWC0001` | the shipped guard target | The consumer's target framework is not .NET 11+ targeting Windows 10.0.19041.0+ | Retarget as above, or make the reference conditional when multi-targeting. | -| `PWC0002` | the shipped guard target | The consumer is not 64-bit | Remove your `PlatformTarget`, set it to `x64`/`arm64`, or choose a matching `RuntimeIdentifier`. | +| `PCC0001` | the shipped guard target | The consumer's target framework is not .NET 11+ targeting Windows 10.0.19041.0+ | Retarget as above, or make the reference conditional when multi-targeting. | +| `PCC0002` | the shipped guard target | The consumer is not 64-bit | Remove your `PlatformTarget`, set it to `x64`/`arm64`, or choose a matching `RuntimeIdentifier`. | | `CS1705` | the C# compiler | `WindowsSdkPackageVersion` is older than the projection requires | Set `WindowsSdkPackageVersion` to `10.0.26100.80` or later. | | `CS8012` | the C# compiler | A referenced assembly targets a different processor (seen when a consumer forces `x86`) | Use a 64-bit consumer. | | `NETSDK1100` | the .NET SDK | A Windows-targeted project is being built on a non-Windows operating system | Set `EnableWindowsTargeting` (see below). | -| `CS0234`/`CS0246` for `Microsoft.WSL`/`Purview` types | the C# compiler | The package contributed no compile assets (an unsupported target framework, or an unverified escape hatch) | Fix the target framework; `PWC0001` explains the same problem with a clearer message. | +| `CS0234`/`CS0246` for `Microsoft.WSL`/`Purview` types | the C# compiler | The package contributed no compile assets (an unsupported target framework, or an unverified escape hatch) | Fix the target framework; `PCC0001` explains the same problem with a clearer message. | ## Workaround: building on a non-Windows CI agent @@ -173,7 +191,7 @@ target framework for the project's package references: It does **not** work here. `AssetTargetFallback` is not applied to `netcoreapp`-family packages, so the package still contributes no compile assets and the build fails in your own code with -`CS0234`/`CS0246`. `PWC0001` fires as well. +`CS0234`/`CS0246`. `PCC0001` fires as well. Even if it did compile, it would not help: the `lib/` assemblies are .NET 11 assemblies, so they can only be loaded by a .NET 11 runtime. A consumer would have to move to .NET 11 to run them anyway. @@ -191,34 +209,38 @@ Only the Windows inner build may reference the packages, so make the reference c net11.0;net11.0-windows10.0.19041.0 - + ``` -An unconditional reference fails the whole build: the `net11.0` inner build reports `PWC0001`. +An unconditional reference fails the whole build: the `net11.0` inner build reports `PCC0001`. Keep any code that uses the library behind the same condition — `#if WINDOWS` (defined by the SDK for Windows target frameworks) or a conditional `Compile` item — so the non-Windows inner build can still compile. ## Verifying these requirements -`just verify-consumers` packs the solution and builds twelve throwaway consumer projects against the +`just verify-consumers` packs the solution and builds sixteen throwaway consumer projects against the produced packages, asserting every claim on this page: | Case | Consumer | Expected outcome | | --- | --- | --- | | 01 | `.NET 11` + Windows with the documented settings | builds; `wslcsdk.dll` copied to the output | | 02 | …with a stale `WindowsSdkPackageVersion` | `CS1705` | -| 03 | …with `PlatformTarget=x86` | `PWC0002` | +| 03 | …with `PlatformTarget=x86` | `PCC0002` | | 04 | …with no settings at all | builds via the shipped `buildTransitive` defaults | -| 05 | a module package with no settings | builds (the defaults are transitive) | -| 06 | `net8.0-windows` + `AssetTargetFallback` | `PWC0001` — the escape hatch does not work | -| 07 | `net8.0-windows` | `PWC0001` | -| 08 | plain `net11.0` | `PWC0001` | +| 05 | a module package plus the WSL Containers backend, with no settings | builds; `wslcsdk.dll` copied (the defaults are transitive through the backend) | +| 06 | `net8.0-windows` + `AssetTargetFallback` | `PCC0001` — the escape hatch does not work | +| 07 | `net8.0-windows` | `PCC0001` | +| 08 | plain `net11.0` | `PCC0001` | | 09 | …with `PlatformTarget=AnyCPU` | builds (corrected to `x64`) | -| 10 | the `net11.0-windows` shorthand (no OS version) | `PWC0001` | +| 10 | the `net11.0-windows` shorthand (no OS version) | `PCC0001` | | 11 | multi-targeting with a conditional `PackageReference` | builds | -| 12 | multi-targeting with an unconditional `PackageReference` | `PWC0001` | +| 12 | multi-targeting with an unconditional `PackageReference` | `PCC0001` | +| 13 | a `net10.0` consumer of the portable abstractions | builds | +| 14 | a `net10.0` consumer of the Docker backend | builds; the backend registration is generated into the consumer | +| 15 | a `net10.0` consumer of a service module, with no backend package | builds | +| 16 | a `.NET 11` Windows consumer with **both** backends referenced | builds; both registrations are generated | The script is `scripts/verify-consumers.ps1` and it only writes to the temp folder. It needs network access (it restores transitive dependencies from nuget.org), so it is a local/CI-explicit step diff --git a/docs/wiki/Contributing-Modules.md b/docs/wiki/Contributing-Modules.md index 5945bdd..e767c9c 100644 --- a/docs/wiki/Contributing-Modules.md +++ b/docs/wiki/Contributing-Modules.md @@ -1,22 +1,22 @@ # Contributing a module -How to add a new service module to `Purview.WslContainers`. +How to add a new service module to `Purview.Containers`. ## Files ``` -src/WslContainers.MyService/ - WslContainers.MyService.csproj -> PackageId Purview.WslContainers.MyService +src/MyService/ + MyService.csproj -> PackageId Purview.Containers.MyService MyServiceConfiguration.cs -> immutable record, module fields MyServiceBuilder.cs -> fluent builder MyServiceContainer.cs -> container, connection string / endpoints -tests/WslContainers.MyService.UnitTests/ -tests/WslContainers.MyService.IntegrationTests/ +tests/MyService.UnitTests/ +tests/MyService.IntegrationTests/ ``` ## Steps -1. **Reference the core**: `` (the Purview SDK adds the right `InternalsVisibleTo`/pack defaults). +1. **Reference the core**: `` (the Purview SDK adds the right `InternalsVisibleTo`/pack defaults). 2. **Configuration record** — derive from `ContainerConfiguration`, add module fields; credentials as `Secret`: ```csharp @@ -60,7 +60,7 @@ public class MyServiceBuilder : ContainerBuilder new(configuration, Runtime ?? WslContainerRuntime.Instance); + => new(configuration, Backend); } ``` @@ -71,8 +71,8 @@ public class MyServiceBuilder : ContainerBuilder **Experimental.** The API, defaults and packaging rules can change between prereleases; there is no > production support guarantee. ## 1. Reference a package -Reference the core runtime directly for a generic container: +Reference a backend package for generic containers: ```bash -dotnet add package Purview.WslContainers +dotnet add package Purview.Containers.Wsl # WSL Containers (Windows, .NET 11) +dotnet add package Purview.Containers.Docker # Docker / Testcontainers (any platform) ``` -or a service module, which depends on the core package: +or a service module, which is backend-neutral and needs a backend package alongside it: ```bash -dotnet add package Purview.WslContainers.PostgreSql +dotnet add package Purview.Containers.PostgreSql +dotnet add package Purview.Containers.Wsl # ...or Purview.Containers.Docker ``` ## 2. Run a generic container +The same code works on either backend — only the package reference from step 1 decides where it runs: + ```csharp -using Purview.WslContainers; -using Purview.WslContainers.Waiting; +using Purview.Containers; +using Purview.Containers.Waiting; await using var container = new ContainerBuilder() .WithImage("docker.io/library/redis:latest") @@ -49,15 +59,15 @@ await container.StartAsync(); ushort port = container.GetMappedPublicPort(6379); ``` -`Build()` validates the accumulated configuration; `StartAsync()` ensures the session and image, creates the -container, starts it, and only returns once every configured wait strategy is satisfied. `DisposeAsync()` -stops and deletes the container (and never terminates the shared session). +`Build()` validates the accumulated configuration and resolves the backend when the container starts; +`StartAsync()` creates the container, starts it, and only returns once every configured wait strategy is +satisfied. `DisposeAsync()` stops and deletes the container (and never terminates a shared WSLC session). ## 3. Use a typed module ```csharp using Npgsql; -using Purview.WslContainers.PostgreSql; +using Purview.Containers.PostgreSql; await using var postgres = new PostgreSqlBuilder() .WithDatabase("tests") @@ -71,18 +81,26 @@ await using var connection = new NpgsqlConnection(postgres.GetConnectionString() await connection.OpenAsync(); ``` -Each module ships a bespoke README inside the package (`Purview.WslContainers.`) and a page in the +Each module ships a bespoke README inside the package (`Purview.Containers.`) and a page in the [Modules](Modules.md) reference. ## 4. Run the tests ```powershell just test # dotnet test, one test module at a time -just test '/*/*/*/*[Category=Unit]' # unit tests only (no WSLC required) +just test '/*/*/*/*[Category=Unit]' # unit tests only (no runtime required) ``` -Integration tests need a working WSLC installation. A WSLC session exclusively locks its image-store VHD, so -test modules run serially by default — see [Testing](Testing.md) for the details and how to run a subset. +Integration tests need a **running backend**: WSL Containers or a Docker daemon, depending on which you +selected. A WSLC session exclusively locks its image-store VHD, so test modules run serially by default — +see [Testing](Testing.md) for the details and how to run a subset. + +To see a container start end to end, run one of the samples in this repository: + +```powershell +just sample-wsl # needs WSL Containers +just sample-docker # needs a Docker daemon +``` ## 5. Build and pack locally diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index a9cadae..0017009 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -1,12 +1,12 @@ -# Purview.WslContainers Wiki +# Purview.Containers Wiki -Purview.WslContainers is a **WSLC-native** Testcontainers-style library for .NET: it runs throwaway Linux -containers for integration testing on **Microsoft WSL Containers (WSLC)** with **no Docker installation**. -It is built directly against the `Microsoft.WSL.Containers` managed package — no `wslc.exe`/`wsl.exe`/ -`docker` CLI, no Docker.DotNet, and no Testcontainers dependency. +Purview.Containers is a Testcontainers-style library for .NET that runs throwaway Linux containers for +integration testing on **Microsoft WSL Containers (WSLC)** with **no Docker installation**, or on +**Docker** through Testcontainers. The WSLC backend is built directly against the +`Microsoft.WSL.Containers` managed package — no `wslc.exe`/`wsl.exe`/`docker` CLI. This wiki is the project documentation hub. The packages are published under the -`Purview.WslContainers.*` package IDs. +`Purview.Containers.*` package IDs. > **Experimental.** This project is an experiment in running throwaway containers through Microsoft > WSL Containers. The public API, defaults and packaging rules can change between prereleases, and @@ -16,6 +16,7 @@ This wiki is the project documentation hub. The packages are published under the ## Start here - [Getting Started](Getting-Started.md) +- [Backends: WSLC or Docker](Backends.md) - [Consumer Requirements](Consumer-Requirements.md) - [Architecture](Architecture.md) - [Lifecycle](Lifecycle.md) @@ -27,14 +28,16 @@ This wiki is the project documentation hub. The packages are published under the | Package | Purpose | | --- | --- | -| `Purview.WslContainers` | Core runtime: `ContainerBuilder`, sessions, images, ports, mounts, wait strategies, logs/exec, diagnostics. | -| `Purview.WslContainers.PostgreSql` | PostgreSQL container (`postgres:17`), `pg_isready` readiness, Npgsql connection string. | -| `Purview.WslContainers.Redis` | Redis-compatible container (`redis:7`), `redis-cli ping` readiness; also usable with Valkey/Garnet. | -| `Purview.WslContainers.MsSql` | SQL Server container (`mssql/server:2022-latest`), host-side `SqlClient` readiness, explicit EULA acceptance. | -| `Purview.WslContainers.RabbitMq` | RabbitMQ container (`rabbitmq:3-management`), AMQP + management endpoints. | -| `Purview.WslContainers.Azurite` | Azure Storage emulator (`azure-storage/azurite`), blob/queue/table endpoints. | -| `Purview.WslContainers.Nats` | NATS broker (`nats:2`), client + monitoring endpoints. | -| `Purview.WslContainers.MySql` | MySQL container (`mysql:8`), host-side `MySqlConnector` readiness. | +| `Purview.Containers` | Backend-neutral abstractions: the container contract, builders, wait strategies, images, networking, mounts, diagnostics, backend selection. | +| `Purview.Containers.Wsl` | The WSL Containers backend: sessions, images, ports, mounts, wait strategies, logs/exec, diagnostics for WSLC. | +| `Purview.Containers.Docker` | The Docker backend: the same containers on any reachable Docker daemon, driven by Testcontainers. | +| `Purview.Containers.PostgreSql` | PostgreSQL container (`postgres:17`), `pg_isready` readiness, Npgsql connection string. | +| `Purview.Containers.Redis` | Redis-compatible container (`redis:7`), `redis-cli ping` readiness; also usable with Valkey/Garnet. | +| `Purview.Containers.MsSql` | SQL Server container (`mssql/server:2022-latest`), host-side `SqlClient` readiness, explicit EULA acceptance. | +| `Purview.Containers.RabbitMq` | RabbitMQ container (`rabbitmq:3-management`), AMQP + management endpoints. | +| `Purview.Containers.Azurite` | Azure Storage emulator (`azure-storage/azurite`), blob/queue/table endpoints. | +| `Purview.Containers.Nats` | NATS broker (`nats:2`), client + monitoring endpoints. | +| `Purview.Containers.MySql` | MySQL container (`mysql:8`), host-side `MySqlConnector` readiness. | ## Feature highlights @@ -45,7 +48,7 @@ This wiki is the project documentation hub. The packages are published under the ports, instead of probing for a free port first. - **Fail-fast configuration** — invalid images and tags are rejected at configuration time (`Image.Parse`), UDP mappings and port `0` are rejected at build time, and module invariants (licences, passwords, required - fields) throw `WslContainerConfigurationException` before a pull is attempted. + fields) throw `ContainerConfigurationException` before a pull is attempted. - **Composable readiness** — TCP, HTTP(S), log-message, exec and custom wait strategies, with timeouts, intervals, retries, and `ForAll`/`ForAny` composition. - **Module model without duplication** — modules supply defaults (image, ports, environment, readiness, @@ -55,15 +58,19 @@ This wiki is the project documentation hub. The packages are published under the ## Requirements -- Windows 10/11 with **WSL Containers**, installed via `wsl --install --no-distribution`. The library - is verified against WSL **3.0.1.0**; `wsl --version` and `wslc version` should both report 3.0.1.0 - or later. +- **WSL Containers backend:** Windows 10/11 with **WSL Containers**, installed via + `wsl --install --no-distribution`. Verified against WSL **3.0.1.0**; `wsl --version` and `wslc version` + should both report 3.0.1.0 or later. +- **Docker backend:** any reachable Docker daemon (Docker Desktop, Docker Engine in WSL2, or a CI runner). + Verify with `docker info`. - .NET SDK 11 to build (the repository pins `11.0.100-rc.1.26425.128` in `global.json`). -- A consuming project must be a **.NET 11 project targeting Windows specifically**: - `net11.0-windows10.0.19041.0`, built for x64 or arm64, with `WindowsSdkPackageVersion` - `10.0.26100.80` or later. See [Consumer Requirements](Consumer-Requirements.md) for the full - contract, the exact errors raised when it is not met, and the `EnableWindowsTargeting` workaround - for non-Windows CI agents. +- A consuming project must be a **.NET 11 project targeting Windows specifically** when it uses the + **WSL Containers backend**: `net11.0-windows10.0.19041.0`, built for x64 or arm64, with + `WindowsSdkPackageVersion` `10.0.26100.80` or later. The abstractions and the service modules target + `net10.0` and are portable, but they need a backend package to run — see + [Backends: WSLC or Docker](Backends.md) for the side-by-side setup. The full contract, the exact errors + raised when it is not met, and the `EnableWindowsTargeting` workaround for non-Windows CI agents are in + [Consumer Requirements](Consumer-Requirements.md). ```powershell wsl --version # WSL Containers installed (verified against 3.0.1.0) @@ -73,14 +80,38 @@ wslc version # e.g. 3.0.1.0 The library reports missing prerequisites through `WslContainerRuntime.GetInfoAsync()`; it never installs or updates WSL on its own. +## Choosing a backend + +Two backends implement the same API. Automatic detection is the default; pin one in code or from the +environment: + +```powershell +# auto (the default) | wsl | docker | +$env:PURVIEW_CONTAINERS_BACKEND = "wsl" +``` + +```csharp +ContainerBackends.Use(ContainerBackendSelection.Named("docker")); // or Use(new WslContainerBackend()) +``` + +That is the switch that lets one test suite use WSLC on a developer machine and Docker in CI. Automatic +detection prefers WSLC and falls through to Docker when WSL Containers is not installed; a **named** +backend never silently falls back. + +**[Backends: WSLC or Docker](Backends.md)** has the full comparison, side-by-side project setup, the CI +example and the troubleshooting reference. + ## Repository layout | Path | Purpose | | --- | --- | | `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack. | -| `src/src/WslContainers` | Core runtime (`Purview.WslContainers`): containers, images, runtime, networking, mounts, diagnostics. | +| `src/src/Containers` | Backend-neutral abstractions (`Purview.Containers`): the container contract, builders and backend selection. | +| `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`): containers, images, runtime, networking, mounts, diagnostics. | +| `src/src/Docker` | Docker backend (`Purview.Containers.Docker`): the same containers on a Docker daemon, via Testcontainers. | | `src/src/` | Service modules; each carries a bespoke `Sdk/README.md` that ships as the package README. | -| `src/tests` | TUnit unit and integration test projects. | +| `src/tests` | TUnit unit and integration test projects (WSLC and Docker suites). | +| `samples/getting-started` | Runnable WSLC and Docker samples (`just sample-wsl`, `just sample-docker`). | | `spikes/WslcSpikes` | Phase 0 investigation harness (`s1`..`s14` behaviour probes). | | `docs/wiki` | This wiki, aggregated by the purview-dev website. | | `purview-build.json` | Shared `Purview.Build` pipeline configuration, including the exhaustive pack manifest. | diff --git a/docs/wiki/Lifecycle.md b/docs/wiki/Lifecycle.md index 5245e98..7c95d63 100644 --- a/docs/wiki/Lifecycle.md +++ b/docs/wiki/Lifecycle.md @@ -1,6 +1,6 @@ # Lifecycle -Container lifecycle semantics for `Purview.WslContainers`, derived from the Phase 0 spikes. +Container lifecycle semantics for `Purview.Containers`, derived from the Phase 0 spikes. ## Public API diff --git a/docs/wiki/Modules.md b/docs/wiki/Modules.md index ee1b3bc..8d7aa5e 100644 --- a/docs/wiki/Modules.md +++ b/docs/wiki/Modules.md @@ -1,10 +1,11 @@ # Modules -Module architecture for `Purview.WslContainers`. +Module architecture for `Purview.Containers`. ## Principle -Modules are thin packages layered on the core. A module supplies only: +Modules are thin packages layered on the backend-neutral abstractions +([`Purview.Containers`](Architecture.md)). A module supplies only: - default image - default ports @@ -14,7 +15,10 @@ Modules are thin packages layered on the core. A module supplies only: - connection string / endpoint generation - module-specific convenience APIs -Modules must **not** duplicate container runtime infrastructure. +Modules must **not** duplicate container runtime infrastructure, and they must never reference a backend +package (`Purview.Containers.Wsl`, …): the container base resolves the backend through +`ContainerBackends.ResolveAsync()`, which is what lets the same module package run on WSLC or Docker. Every +module targets `net10.0` and is portable. ## Builder model @@ -23,7 +27,7 @@ A generic CRTP base with immutable built configurations: ```csharp public abstract class ContainerBuilder where TBuilder : ContainerBuilder - where TContainer : WslContainer + where TContainer : IContainer where TConfiguration : ContainerConfiguration, new() { public TBuilder WithImage(string image) { /* accumulate */ return (TBuilder)this; } @@ -87,15 +91,15 @@ public sealed class PostgreSqlBuilder : ContainerBuilder new(configuration, Runtime ?? WslContainerRuntime.Instance); + => new(configuration, Backend); } -public sealed class PostgreSqlContainer : WslContainer +public sealed class PostgreSqlContainer : ContainerBase { private readonly PostgreSqlConfiguration configuration; - internal PostgreSqlContainer(PostgreSqlConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) => this.configuration = configuration; + internal PostgreSqlContainer(PostgreSqlConfiguration configuration, IContainerBackend backend) + : base(configuration, backend) => this.configuration = configuration; public string GetConnectionString() { diff --git a/docs/wiki/Networking.md b/docs/wiki/Networking.md index eb0c661..2b5b06c 100644 --- a/docs/wiki/Networking.md +++ b/docs/wiki/Networking.md @@ -1,6 +1,6 @@ # Networking -WSLC networking behaviour (verified in Phase 0) and how `Purview.WslContainers` models it. +WSLC networking behaviour (verified in Phase 0) and how `Purview.Containers` models it. ## Verified behaviour diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index 4087509..2b13123 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -1,6 +1,6 @@ # Packaging -Every project under `src/src` is packable and ships as `Purview.WslContainers.*`; test and spike projects +Every project under `src/src` is packable and ships as `Purview.Containers.*`; test and spike projects never pack. Packages are produced into `./artifacts` with a matching `.snupkg`. ## What a package contains @@ -9,17 +9,21 @@ Each package ships exactly: | Entry | Source | | --- | --- | -| `lib/$(TFM)/Purview.WslContainers[.].dll` | The built assembly (`net11.0-windows10.0.19041`). | -| `lib/$(TFM)/Purview.WslContainers[.].xml` | XML documentation, generated because `GenerateDocumentationFile` is on for packable projects. | +| `lib/$(TFM)/Purview.Containers.Wsl.dll` / `lib/$(TFM)/Purview.Containers..dll` | The built assembly: `net10.0` for `Purview.Containers` and the service modules, `net11.0-windows10.0.19041` for the WSL Containers backend. | +| `lib/$(TFM)/Purview.Containers.Wsl.xml` / `lib/$(TFM)/Purview.Containers..xml` | XML documentation, generated because `GenerateDocumentationFile` is on for packable projects. | | `README.md` | The package's bespoke `Sdk/README.md` (see below). | | `purview-logo-light.png` | The shared Purview package icon (`assets/images/purview-logo-light.png`). | -| `buildTransitive/Purview.WslContainers.props` | **Core package only.** Defaults `WindowsSdkPackageVersion` for consumers. | -| `buildTransitive/Purview.WslContainers.targets` | **Core package only.** Defaults `PlatformTarget` to `x64` and raises `PWC0001`/`PWC0002` for unsupported consumers. | +| `buildTransitive/Purview.Containers.props` / `.targets` | **Abstractions package.** Declares the `PurviewContainersBackends` list and turns it into a generated backend initializer in the consuming assembly. | +| `buildTransitive/Purview.Containers.Wsl.props` / `.targets` | **WSL Containers backend only.** Defaults `WindowsSdkPackageVersion`/`PlatformTarget` and raises `PCC0001`/`PCC0002` for unsupported consumers. | +| `buildTransitive/Purview.Containers.Docker.props` | **Docker backend only.** Adds the Docker backend to the `PurviewContainersBackends` list. | Portable PDBs are delivered through the `.snupkg`, never inside the `.nupkg`. -`$(TFM)` is `net11.0-windows10.0.19041.0`, so every package is a **.NET 11, Windows-only** package. -Consumers must target a matching framework; see [Consumer Requirements](Consumer-Requirements.md). +`$(TFM)` is expanded per shipped framework: `Purview.Containers`, `Purview.Containers.Docker` and the +service modules ship `lib/net10.0/`, and `Purview.Containers.Wsl` ships +`lib/net11.0-windows10.0.19041/`, so only the WSL Containers backend is a **.NET 11, Windows-only** +package. Consumers must target a matching framework; see +[Consumer Requirements](Consumer-Requirements.md). ## Package metadata @@ -82,10 +86,19 @@ alongside the `Sdk/README.md`. Removing content from a package means removing th 3. Add the package to `RequiredContent` in `purview-build.json`. 4. Prove it with `just pack` and `just pipeline-pack-validate`. -The two `Sdk/buildTransitive/` files belong to the **core** package only: a module inherits them -through its dependency on `Purview.WslContainers`. They are declared in `RequiredContent` like any -other asset, so a package that starts or stops shipping MSBuild assets has to update that manifest — -a produced package with no rule, or a packed entry matched by no glob, fails validation. +> **Packaging a backend or a new MSBuild asset?** NuGet only auto-imports `buildTransitive/.props` +> and `buildTransitive/.targets`, so an asset named after anything else — including a shorter +> product name — is shipped but never applied. A regression here is invisible to the build of this +> repository, because our own projects reference each other by project and never import these assets; +> `just verify-consumers` is the check that catches it. + +The `Sdk/buildTransitive/` assets are per package, and NuGet only auto-imports a file named after its own +package id (`buildTransitive/.props|targets`). The abstractions package ships the backend +registration, each backend package ships its own registration entry (and, for the WSL backend, the +consumer defaults and guards), and the service modules ship none — a module is backend-neutral, so a +consumer adds a backend package explicitly. They are declared in `RequiredContent` like any other asset, +so a package that starts or stops shipping MSBuild assets has to update that manifest — a produced +package with no rule, or a packed entry matched by no glob, fails validation. ## Related diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md index 57ff8bb..18de8b2 100644 --- a/docs/wiki/Release-Flow.md +++ b/docs/wiki/Release-Flow.md @@ -1,6 +1,6 @@ # Release Flow -How this repository builds, versions, validates and publishes `Purview.WslContainers.*`. +How this repository builds, versions, validates and publishes `Purview.Containers.*`. ## Versioning @@ -56,9 +56,9 @@ the run with the failing module's output. Both workflows pin `dotnet-version` to the SDK in `global.json` (`11.0.100-rc.1.26425.128`); keep them in sync when the SDK is bumped, and keep `purview-build.json` pointing at `src/WSLTestContainers.slnx`. -The shared workflow runs on **`ubuntu-latest`**, so the Linux agent builds these -`net11.0-windows10.0.19041.0` projects. That only works because `src/Directory.Build.props` sets -`EnableWindowsTargeting=true`; removing it fails the pipeline with `NETSDK1100`. The agent has no WSL +The shared workflow runs on **`ubuntu-latest`**, so the Linux agent builds the portable `net10.0` projects +and the `net11.0-windows10.0.19041.0` test projects. That only works because `src/Directory.Build.props` +sets `EnableWindowsTargeting=true`; removing it fails the pipeline with `NETSDK1100`. The agent has no WSL Containers, which is why the pipeline is filtered to `[Category=Unit]` — see [Consumer Requirements](Consumer-Requirements.md) and [Testing](Testing.md). diff --git a/docs/wiki/Testing.md b/docs/wiki/Testing.md index ad1588e..73b4c58 100644 --- a/docs/wiki/Testing.md +++ b/docs/wiki/Testing.md @@ -14,10 +14,10 @@ Test projects are discovered under `src/tests` and the SDK stamps every test ass `purview-build.json` filters the pipeline run to `/*/*/*/*[Category=Unit]`, so the shared pipeline never starts containers. Run integration projects explicitly when you have a WSLC host. -> The shared `purview-dev/build` workflow runs on **`ubuntu-latest`**, so the pipeline builds these -> `net11.0-windows…` projects on Linux. That works because `src/Directory.Build.props` sets -> `EnableWindowsTargeting=true`; without it the SDK reports `NETSDK1100`. See -> [Consumer Requirements](Consumer-Requirements.md) for what that workaround does and does not +> The shared `purview-dev/build` workflow runs on **`ubuntu-latest`**, so the pipeline builds the portable +> `net10.0` projects and the `net11.0-windows…` test projects on Linux. That works because +> `src/Directory.Build.props` sets `EnableWindowsTargeting=true`; without it the SDK reports `NETSDK1100`. +> See [Consumer Requirements](Consumer-Requirements.md) for what that workaround does and does not > cover. ```powershell @@ -26,6 +26,14 @@ just test '/*/*/*/*[Category=Unit]' # unit tests only just test '/*/*/*/*[Category=Integration]' --max-parallel-test-modules 1 ``` +Integration suites target whichever backend is selected. Automatic detection prefers WSLC and falls back +to Docker, so a WSLC host runs the WSLC suites and a Docker-only host runs the Docker ones; pin one with +`PURVIEW_CONTAINERS_BACKEND=wsl|docker` to fail loudly instead of falling back. The Docker suites +(`Docker.IntegrationTests` for the container contract, `Modules.DockerIntegrationTests` for the seven +service modules) skip themselves when no daemon is reachable, and the WSLC suites skip themselves when the +host lacks the WSL Containers components. See [Backends: WSLC or Docker](Backends.md) for consumer-facing +setup and CI examples. + ## Why test modules run serially A WSLC session **exclusively locks its `storage.vhdx`**, and the lock is taken lazily on the first store @@ -45,7 +53,7 @@ running the WSLC integration suites. ## Expectations -- The `WslContainers.IntegrationTests` module runs many real containers in one session and can take several +- The `Wsl.IntegrationTests` module runs many real containers in one session and can take several minutes, because WSLC serialises container operations. Slow-test warnings while it runs are expected. - Integration tests skip themselves when the host lacks the required WSL/WSLC components, so a machine without WSLC can still run the unit suites. @@ -54,7 +62,7 @@ running the WSLC integration suites. ## Verifying the consumer contract -`WslContainers.UnitTests/ConsumerRequirementsTests.cs` guards the shape of the shipped library inside +`Wsl.UnitTests/ConsumerRequirementsTests.cs` guards the shape of the shipped library inside the normal unit run: the assembly targets `.NETCoreApp,Version=v11.0`, targets `Windows10.0.19041.0` and declares only that OS platform. If the target framework ever drifts, that test fails — and [Consumer Requirements](Consumer-Requirements.md), the package READMEs and the shipped @@ -66,7 +74,7 @@ throwaway consumer projects for every documented outcome (see outside the `[Category=Unit]` filter because it packs and restores from nuget.org: ```powershell -just verify-consumers # 12 consumer projects, all assertions +just verify-consumers # 16 consumer projects, all assertions just verify-consumers -Keep # same, keeping the generated projects for inspection ``` diff --git a/docs/wiki/Wait-Strategies.md b/docs/wiki/Wait-Strategies.md index c64baaf..fae89b5 100644 --- a/docs/wiki/Wait-Strategies.md +++ b/docs/wiki/Wait-Strategies.md @@ -2,7 +2,7 @@ Composable readiness waits. **A started WSLC container is not necessarily a ready service** — `Container.Start()` returns once the init process is running; readiness is checked separately. -> **Status: implemented (Phase 2).** Verified by integration tests (`tests/WslContainers.IntegrationTests/WaitStrategyTests.cs`). +> **Status: implemented (Phase 2).** Verified by integration tests (`tests/Wsl.IntegrationTests/WaitStrategyTests.cs`). ## Model @@ -27,7 +27,7 @@ Wait (factory) WaitStrategy.WithTimeout / .WithInterval / .WithRetries (fluent) ``` -- Waits run inside `StartAsync()` after the container starts. `StartAsync` only returns once every configured strategy is ready (or throws `WslContainerTimeoutException`). +- Waits run inside `StartAsync()` after the container starts. `StartAsync` only returns once every configured strategy is ready (or throws `ContainerTimeoutException`). - Default timeout = the container's `StartupTimeout` (5 min, overridable via `.WithStartupTimeout(...)` or per-strategy `.WithTimeout(...)`). - Timeout failure produces a diagnostic including container name, image, state, mapped ports, the strategy type, the last check error, and a tail of stdout/stderr (bounded, secret-redacted). diff --git a/docs/wiki/Wslc-Capability-Matrix.md b/docs/wiki/Wslc-Capability-Matrix.md index 13c22c8..fe6c262 100644 --- a/docs/wiki/Wslc-Capability-Matrix.md +++ b/docs/wiki/Wslc-Capability-Matrix.md @@ -56,4 +56,4 @@ Legend: **Implemented** = planned in this library; **Mapped** = native WSLC API 5. Per-container CPU/memory limits — session-level only. 6. TTY, `--user`, labels, DNS options, tmpfs/shm/ulimit in `ContainerSettings`/`ProcessSettings`. -Where a Testcontainers feature has no WSLC equivalent, the library must either (a) provide the closest supported behaviour and document it, or (b) fail fast with a clear `WslContainerNotSupportedException`. \ No newline at end of file +Where a Testcontainers feature has no WSLC equivalent, the library must either (a) provide the closest supported behaviour and document it, or (b) fail fast with a clear `ContainerNotSupportedException`. \ No newline at end of file diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index e962982..8ef1e07 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -1,5 +1,6 @@ - [Home](Home.md) - [Getting Started](Getting-Started.md) +- [Backends: WSLC or Docker](Backends.md) - [Consumer Requirements](Consumer-Requirements.md) - [Architecture](Architecture.md) - [Lifecycle](Lifecycle.md) diff --git a/docs/wiki/index.md b/docs/wiki/index.md index db80aa2..6769104 100644 --- a/docs/wiki/index.md +++ b/docs/wiki/index.md @@ -1,14 +1,16 @@ # Purview WSL Test Containers -WSLC-native throwaway Linux containers for .NET integration testing — Testcontainers-style APIs built -directly on **Microsoft WSL Containers**, with no Docker installation. +Throwaway Linux containers for .NET integration testing — Testcontainers-style APIs for **Microsoft WSL +Containers (WSLC)** with no Docker installation, and for **Docker** through Testcontainers. The same test +code runs on either runtime; see [Backends: WSLC or Docker](Backends.md). [Get started](Getting-Started.md){ .md-button .md-button--primary } -[Documentation overview](Home.md){ .md-button } +[Choose a backend](Backends.md){ .md-button } ## Guides - [Getting Started](Getting-Started.md) +- [Backends: WSLC or Docker](Backends.md) - [Consumer requirements](Consumer-Requirements.md) - [Architecture](Architecture.md) - [Lifecycle](Lifecycle.md) diff --git a/purview-build.json b/purview-build.json index f1050a7..a252b13 100644 --- a/purview-build.json +++ b/purview-build.json @@ -15,53 +15,68 @@ // (assets/images/purview-logo-light.png, linked as Sdk/purview-logo-light.png). A package with // no rule here, or an entry matching none of its globs, fails validation. "RequiredContent": { - "purview.wslcontainers": [ - "lib/$(TFM)/Purview.WslContainers.dll", - "lib/$(TFM)/Purview.WslContainers.xml", + "purview.containers": [ + "lib/$(TFM)/Purview.Containers.dll", + "lib/$(TFM)/Purview.Containers.xml", "README.md", "purview-logo-light.png", - "buildTransitive/Purview.WslContainers.props", - "buildTransitive/Purview.WslContainers.targets" + "buildTransitive/Purview.Containers.props", + "buildTransitive/Purview.Containers.targets" ], - "purview.wslcontainers.azurite": [ - "lib/$(TFM)/Purview.WslContainers.Azurite.dll", - "lib/$(TFM)/Purview.WslContainers.Azurite.xml", + "purview.containers.docker": [ + "lib/$(TFM)/Purview.Containers.Docker.dll", + "lib/$(TFM)/Purview.Containers.Docker.xml", + "README.md", + "purview-logo-light.png", + "buildTransitive/Purview.Containers.Docker.props" + ], + "purview.containers.wsl": [ + "lib/$(TFM)/Purview.Containers.Wsl.dll", + "lib/$(TFM)/Purview.Containers.Wsl.xml", + "README.md", + "purview-logo-light.png", + "buildTransitive/Purview.Containers.Wsl.props", + "buildTransitive/Purview.Containers.Wsl.targets" + ], + "purview.containers.azurite": [ + "lib/$(TFM)/Purview.Containers.Azurite.dll", + "lib/$(TFM)/Purview.Containers.Azurite.xml", "README.md", "purview-logo-light.png" ], - "purview.wslcontainers.mssql": [ - "lib/$(TFM)/Purview.WslContainers.MsSql.dll", - "lib/$(TFM)/Purview.WslContainers.MsSql.xml", + "purview.containers.mssql": [ + "lib/$(TFM)/Purview.Containers.MsSql.dll", + "lib/$(TFM)/Purview.Containers.MsSql.xml", "README.md", "purview-logo-light.png" ], - "purview.wslcontainers.mysql": [ - "lib/$(TFM)/Purview.WslContainers.MySql.dll", - "lib/$(TFM)/Purview.WslContainers.MySql.xml", + "purview.containers.mysql": [ + "lib/$(TFM)/Purview.Containers.MySql.dll", + "lib/$(TFM)/Purview.Containers.MySql.xml", "README.md", "purview-logo-light.png" ], - "purview.wslcontainers.nats": [ - "lib/$(TFM)/Purview.WslContainers.Nats.dll", - "lib/$(TFM)/Purview.WslContainers.Nats.xml", + "purview.containers.nats": [ + "lib/$(TFM)/Purview.Containers.Nats.dll", + "lib/$(TFM)/Purview.Containers.Nats.xml", "README.md", "purview-logo-light.png" ], - "purview.wslcontainers.postgresql": [ - "lib/$(TFM)/Purview.WslContainers.PostgreSql.dll", - "lib/$(TFM)/Purview.WslContainers.PostgreSql.xml", + "purview.containers.postgresql": [ + "lib/$(TFM)/Purview.Containers.PostgreSql.dll", + "lib/$(TFM)/Purview.Containers.PostgreSql.xml", "README.md", "purview-logo-light.png" ], - "purview.wslcontainers.rabbitmq": [ - "lib/$(TFM)/Purview.WslContainers.RabbitMq.dll", - "lib/$(TFM)/Purview.WslContainers.RabbitMq.xml", + "purview.containers.rabbitmq": [ + "lib/$(TFM)/Purview.Containers.RabbitMq.dll", + "lib/$(TFM)/Purview.Containers.RabbitMq.xml", "README.md", "purview-logo-light.png" ], - "purview.wslcontainers.redis": [ - "lib/$(TFM)/Purview.WslContainers.Redis.dll", - "lib/$(TFM)/Purview.WslContainers.Redis.xml", + "purview.containers.redis": [ + "lib/$(TFM)/Purview.Containers.Redis.dll", + "lib/$(TFM)/Purview.Containers.Redis.xml", "README.md", "purview-logo-light.png" ] diff --git a/samples/getting-started/DockerSample/DockerSample.csproj b/samples/getting-started/DockerSample/DockerSample.csproj new file mode 100644 index 0000000..7aede1f --- /dev/null +++ b/samples/getting-started/DockerSample/DockerSample.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + false + + + + + + + diff --git a/samples/getting-started/DockerSample/Program.cs b/samples/getting-started/DockerSample/Program.cs new file mode 100644 index 0000000..8006760 --- /dev/null +++ b/samples/getting-started/DockerSample/Program.cs @@ -0,0 +1,28 @@ +// Docker sample: byte-for-byte the same container code as the WSLC sample, on the Docker backend. +// Prerequisite: a reachable Docker daemon (`docker info`). +using Purview.Containers; +using Purview.Containers.Docker; +using Purview.Containers.Waiting; + +// A package consumer gets backend registration generated from the package's buildTransitive assets, so +// this line does not appear in consumer code. The sample references the backend as a project, so it +// registers explicitly. +ContainerBackends.Register(new DockerContainerBackend()); + +foreach (var backend in await ContainerBackends.ProbeAllAsync()) +{ + Console.WriteLine($"backend : {backend.Name} usable={backend.IsUsable} version={backend.Version}"); +} + +await using var container = new ContainerBuilder() + .WithImage("alpine:3.19") + .WithCommand("/bin/sh", "-c", "echo ready && sleep 30") + .WithPortBinding(8080, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForLogMessage("ready")) + .Build(); + +await container.StartAsync(); + +Console.WriteLine($"container : {container.Name} ({container.Id[..12]})"); +Console.WriteLine($"host port : {container.GetMappedPublicPort(8080)} -> 8080"); +Console.WriteLine($"logs : {(await container.GetLogsAsync()).Trim()}"); diff --git a/samples/getting-started/README.md b/samples/getting-started/README.md new file mode 100644 index 0000000..1cd0b46 --- /dev/null +++ b/samples/getting-started/README.md @@ -0,0 +1,32 @@ +# Getting-started samples + +Two runnable samples that differ in exactly one thing: which backend they use. The container code is the +same, which is the point — see [Backends: WSLC or Docker](../../docs/wiki/Backends.md). + +| Sample | Backend | Target framework | Prerequisite | +| --- | --- | --- | --- | +| `WslSample` | `Purview.Containers.Wsl` | `net11.0-windows10.0.19041.0` | Windows with WSL Containers (`wsl --install --no-distribution`) | +| `DockerSample` | `Purview.Containers.Docker` | `net10.0` | a reachable Docker daemon (`docker info`) | + +Run them from the repository root: + +```powershell +just sample-wsl # or: dotnet run --project samples/getting-started/WslSample +just sample-docker # or: dotnet run --project samples/getting-started/DockerSample +``` + +Each sample starts a real container, waits for it to log `ready`, prints the mapped host port and the +container's output, and disposes it again. + +## These are repository samples + +They reference the backends as **projects** so they build from a clone with no package feed. A real +consumer uses packages instead: + +```bash +dotnet add package Purview.Containers.Docker # or Purview.Containers.Wsl +``` + +Because they are project references, no `buildTransitive` assets apply, so each sample registers its +backend explicitly (`ContainerBackends.Register(...)`); a package consumer gets that registration generated +automatically and never writes that line. diff --git a/samples/getting-started/WslSample/Program.cs b/samples/getting-started/WslSample/Program.cs new file mode 100644 index 0000000..a7ec7ab --- /dev/null +++ b/samples/getting-started/WslSample/Program.cs @@ -0,0 +1,28 @@ +// WSL Containers sample: the same code as the Docker sample, running on the WSLC backend. +// Prerequisite: `wsl --install --no-distribution`, then verify with `wsl --version` and `wslc version`. +using Purview.Containers; +using Purview.Containers.Waiting; +using Purview.Containers.Wsl; + +// A package consumer gets backend registration generated from the package's buildTransitive assets, so +// this line does not appear in consumer code. The sample references the backend as a project, so it +// registers explicitly. +ContainerBackends.Register(new WslContainerBackend()); + +foreach (var backend in await ContainerBackends.ProbeAllAsync()) +{ + Console.WriteLine($"backend : {backend.Name} usable={backend.IsUsable} version={backend.Version}"); +} + +await using var container = new ContainerBuilder() + .WithImage("alpine:3.19") + .WithCommand("/bin/sh", "-c", "echo ready && sleep 30") + .WithPortBinding(8080, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForLogMessage("ready")) + .Build(); + +await container.StartAsync(); + +Console.WriteLine($"container : {container.Name} ({container.Id[..12]})"); +Console.WriteLine($"host port : {container.GetMappedPublicPort(8080)} -> 8080"); +Console.WriteLine($"logs : {(await container.GetLogsAsync()).Trim()}"); diff --git a/samples/getting-started/WslSample/WslSample.csproj b/samples/getting-started/WslSample/WslSample.csproj new file mode 100644 index 0000000..0ef4530 --- /dev/null +++ b/samples/getting-started/WslSample/WslSample.csproj @@ -0,0 +1,25 @@ + + + + Exe + net11.0-windows10.0.19041.0 + x64 + + 10.0.26100.80 + enable + enable + false + + true + + + + + + + diff --git a/scripts/verify-consumers.ps1 b/scripts/verify-consumers.ps1 index 80f13e7..75e265f 100644 --- a/scripts/verify-consumers.ps1 +++ b/scripts/verify-consumers.ps1 @@ -14,16 +14,22 @@ Expectations and their documentation: 01 .NET 11 + Windows with the documented settings -> builds, wslcsdk.dll copied 02 ...with a stale WindowsSdkPackageVersion -> CS1705 - 03 ...with PlatformTarget=x86 -> fails PWC0002 + 03 ...with PlatformTarget=x86 -> fails PCC0002 04 ...with no settings at all (buildTransitive defaults) -> builds, wslcsdk.dll copied 05 a module package with no settings (defaults are transitive) -> builds - 06 net8.0-windows + AssetTargetFallback escape hatch -> PWC0001 (the hatch does not work) - 07 net8.0-windows without the escape hatch -> PWC0001 - 08 plain net11.0 (not Windows-specific) -> PWC0001 + 06 net8.0-windows + AssetTargetFallback escape hatch -> PCC0001 (the hatch does not work) + 07 net8.0-windows without the escape hatch -> PCC0001 + 08 plain net11.0 (not Windows-specific) -> PCC0001 09 ...with PlatformTarget=AnyCPU (corrected to x64) -> builds, wslcsdk.dll copied - 10 the net11.0-windows shorthand TFM (no OS version) -> PWC0001 + 10 the net11.0-windows shorthand TFM (no OS version) -> PCC0001 11 multi-targeting with a conditional PackageReference -> builds - 12 multi-targeting with an unconditional PackageReference -> PWC0001 + 12 multi-targeting with an unconditional PackageReference -> PCC0001 + 13 net10.0 + portable abstractions -> builds + 14 net10.0 + Docker backend -> builds, backend registration generated + 15 net10.0 + service module (no backend package) -> builds + 16 net11.0 windows + both backends (auto detection) -> builds, both registrations generated + 17 the documented backend example, on WSL Containers -> builds + 18 the documented backend example, on Docker (net10.0) -> builds .PARAMETER FeedPath Folder holding the packed .nupkg files. Defaults to /artifacts. @@ -35,6 +41,9 @@ Scratch folder for the generated consumers. Defaults to %TEMP%/wslc-consumer-verification. +.PARAMETER Only + Runs only the cases with these ids, e.g. -Only 13,14. Defaults to every case. + .PARAMETER Keep Keep the generated consumer projects for inspection instead of deleting them. @@ -49,6 +58,7 @@ param( [string] $FeedPath, [string] $PackageVersion, [string] $WorkPath = (Join-Path ([System.IO.Path]::GetTempPath()) 'wslc-consumer-verification'), + [string[]] $Only = @(), [switch] $Keep ) @@ -67,7 +77,7 @@ if (-not (Test-Path $FeedPath)) { throw "Package feed '$FeedPath' does not exist. Run 'just pack' first." } -$corePackage = Join-Path $FeedPath "Purview.WslContainers.$PackageVersion.nupkg" +$corePackage = Join-Path $FeedPath "Purview.Containers.Wsl.$PackageVersion.nupkg" if (-not (Test-Path $corePackage)) { throw "Package '$corePackage' was not found. Run 'just pack' (or pass -FeedPath) first." } @@ -76,10 +86,10 @@ $globalPackagesLine = & dotnet nuget locals global-packages --list | Select-Obje $globalPackages = ($globalPackagesLine -replace '^global-packages:\s*', '').Trim() # The packages keep the same version between local packs, so a stale extraction in the global -# packages folder would silently test the previous build. Drop it before consuming the feed. -foreach ($packageId in 'purview.wslcontainers', 'purview.wslcontainers.postgresql') { - $packageFolder = Join-Path $globalPackages $packageId - $versioned = Join-Path $packageFolder $PackageVersion +# packages folder would silently test the previous build. Drop the extracted version of every +# Purview.Containers package before consuming the feed. +foreach ($packageFolder in @(Get-ChildItem -Path $globalPackages -Directory -Filter 'purview.containers*' -ErrorAction SilentlyContinue)) { + $versioned = Join-Path $packageFolder.FullName $PackageVersion if (Test-Path $versioned) { Remove-Item $versioned -Recurse -Force @@ -116,7 +126,8 @@ Set-Content -Path (Join-Path $WorkPath 'nuget.config') -Value $nugetConfig # OutputType=Exe so the runtime assets (wslcsdk.dll) are copied to the output for inspection. $apiSource = @' using Microsoft.WSL.Containers; -using Purview.WslContainers; +using Purview.Containers; +using Purview.Containers.Wsl; namespace Consumer; @@ -124,7 +135,7 @@ public static class Program { public static SessionSettings Session() => new("consumer-session", @"C:\temp\wslc"); - public static WslContainer Container() => + public static IContainer Container() => new ContainerBuilder().WithImage("docker.io/library/alpine:3.19").Build(); public static void Main() { } @@ -162,8 +173,8 @@ $projectTemplate = @' $net11Windows = 'net11.0-windows10.0.19041.0' $net8Windows = 'net8.0-windows10.0.19041.0' -$sdkPackage = 'Purview.WslContainers' -$modulePackage = 'Purview.WslContainers.PostgreSql' +$sdkPackage = 'Purview.Containers.Wsl' +$modulePackage = 'Purview.Containers.PostgreSql' $sdkVersion = '10.0.26100.80' $staleSdkVersion = '10.0.19041.38' $x64 = 'x64' @@ -178,6 +189,80 @@ $multiTargetItems = @' '@ +# A consumer that touches the Docker backend. Nothing registers the backend here: the generated module +# initializer arrives from the package's buildTransitive assets, which is what case 14 asserts. +$dockerSource = @' +using Purview.Containers; +using Purview.Containers.Docker; + +namespace Consumer; + +public static class Program +{ + public static async Task BackendName() => (await ContainerBackends.ResolveAsync()).Name; + + public static void Main() { } +} +'@ + +# A consumer that only references a service module: no backend package, so it restores and builds anywhere. +$portableModuleSource = @' +using Purview.Containers.PostgreSql; + +namespace Consumer; + +public static class Program +{ + public static PostgreSqlBuilder Builder() => new PostgreSqlBuilder().WithDatabase("tests"); + + public static void Main() { } +} +'@ + +# A second backend package alongside the first: the developer-machine shape (WSLC plus Docker present, so +# automatic detection picks WSLC and the environment can pin Docker). +$bothBackendsItems = @' + + + +'@ + +# The realistic Windows consumer shape: a service module plus the WSL Containers backend. The module is +# backend-neutral, so the backend package is what supplies the WSL build defaults and the guards. +$wslBackendItems = @' + + + +'@ + +# Mirrors the container code in the "The same test, either backend" example of docs/wiki/Backends.md, so +# the documented example is compiled against both backend packages on every run. The test assertion is +# omitted because the generated consumer project has no test framework. +$documentedBackendSource = @' +using Purview.Containers; +using Purview.Containers.Waiting; + +namespace Consumer; + +public static class Program +{ + public static async Task MappedPortAsync() + { + await using var container = new ContainerBuilder() + .WithImage("redis:7") + .WithPortBinding(6379, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForTcpPort(6379)) + .Build(); + + await container.StartAsync(); + + return container.GetMappedPublicPort(6379); + } + + public static void Main() { } +} +'@ + function New-ConsumerCase { param( [string] $Id, @@ -189,7 +274,8 @@ function New-ConsumerCase { [string] $Items = '', [hashtable] $Sources = @{ 'Smoke.cs' = $apiSource }, [string] $Expect = 'Builds', - [string] $RequireFile = '' + [string] $RequireFile = '', + [string] $RequireText = '' ) [pscustomobject]@{ @@ -203,6 +289,7 @@ function New-ConsumerCase { Sources = $Sources Expect = $Expect RequireFile = $RequireFile + RequireText = $RequireText } } @@ -214,32 +301,34 @@ $cases = @( -Properties "$staleSdkVersion$x64" -Expect 'CS1705' New-ConsumerCase -Id '03' -Name 'net11 windows + PlatformTarget=x86' ` - -Properties "$sdkVersionx86" -Expect 'PWC0002' + -Properties "$sdkVersionx86" -Expect 'PCC0002' New-ConsumerCase -Id '04' -Name 'net11 windows + buildTransitive defaults only' ` -RequireFile 'wslcsdk.dll' - New-ConsumerCase -Id '05' -Name 'module package + transitive defaults only' ` - -Package $modulePackage + New-ConsumerCase -Id '05' -Name 'module package + WSL backend (defaults are transitive)' ` + -Package $modulePackage ` + -Items $wslBackendItems ` + -RequireFile 'wslcsdk.dll' New-ConsumerCase -Id '06' -Name 'net8 windows + AssetTargetFallback escape hatch' ` -Framework "$net8Windows" ` - -Properties "$escapeHatch$sdkVersion$x64" -Expect 'PWC0001' + -Properties "$escapeHatch$sdkVersion$x64" -Expect 'PCC0001' New-ConsumerCase -Id '07' -Name 'net8 windows without the escape hatch' ` -Framework "$net8Windows" ` - -Properties "$sdkVersion$x64" -Expect 'PWC0001' + -Properties "$sdkVersion$x64" -Expect 'PCC0001' New-ConsumerCase -Id '08' -Name 'plain net11.0 (not Windows-specific)' ` -Framework 'net11.0' ` - -Properties "$sdkVersion$x64" -Expect 'PWC0001' + -Properties "$sdkVersion$x64" -Expect 'PCC0001' New-ConsumerCase -Id '09' -Name 'net11 windows + PlatformTarget=AnyCPU' ` -Properties "$sdkVersionAnyCPU" -RequireFile 'wslcsdk.dll' New-ConsumerCase -Id '10' -Name 'net11.0-windows shorthand TFM (no OS version)' ` -Framework 'net11.0-windows' ` - -Properties "$sdkVersion$x64" -Expect 'PWC0001' + -Properties "$sdkVersion$x64" -Expect 'PCC0001' New-ConsumerCase -Id '11' -Name 'multi-targeting + conditional PackageReference' ` -Framework "net11.0;$net11Windows" ` @@ -253,12 +342,43 @@ $cases = @( -Properties 'false' ` -Items $multiTargetItems ` -Sources @{ 'Smoke.Windows.cs' = $apiSource; 'Smoke.Portable.cs' = $portableSource } ` - -Expect 'PWC0001' + -Expect 'PCC0001' + + New-ConsumerCase -Id '13' -Name 'net10 + portable abstractions' ` + -Framework 'net10.0' ` + -Package 'Purview.Containers' ` + -Sources @{ 'Smoke.Portable.cs' = $portableSource } + + New-ConsumerCase -Id '14' -Name 'net10 + docker backend (generated registration)' ` + -Framework 'net10.0' ` + -Package 'Purview.Containers.Docker' ` + -RequireText 'DockerContainerBackend.Create()' ` + -Sources @{ 'Smoke.cs' = $dockerSource } + + New-ConsumerCase -Id '15' -Name 'net10 + service module (backend-neutral, no backend package)' ` + -Framework 'net10.0' ` + -Package $modulePackage ` + -Sources @{ 'Smoke.cs' = $portableModuleSource } + + New-ConsumerCase -Id '16' -Name 'net11 windows + both backends (auto detection, WSLC first)' ` + -Package $sdkPackage ` + -Items $bothBackendsItems ` + -RequireText 'WslContainerBackend.Create()' ` + -Sources @{ 'Smoke.cs' = $dockerSource } + + New-ConsumerCase -Id '17' -Name 'documented backend example on WSL Containers' ` + -Package $sdkPackage ` + -Sources @{ 'Smoke.cs' = $documentedBackendSource } + + New-ConsumerCase -Id '18' -Name 'documented backend example on Docker (net10.0)' ` + -Framework 'net10.0' ` + -Package 'Purview.Containers.Docker' ` + -Sources @{ 'Smoke.cs' = $documentedBackendSource } ) Write-Host '' -Write-Host 'Purview.WslContainers consumer verification' -ForegroundColor Cyan +Write-Host 'Purview.Containers.Wsl consumer verification' -ForegroundColor Cyan Write-Host " feed : $FeedPath" Write-Host " version : $PackageVersion" Write-Host " scratch : $WorkPath" @@ -266,6 +386,22 @@ Write-Host '' $results = [System.Collections.Generic.List[object]]::new() +if ($Only.Count -gt 0) { + # `pwsh -File script.ps1 -Only 13,14` binds the value as one comma-joined string, so split it. Ids are + # zero-padded ('08'), so an unpadded value ('8') matches too. + $wanted = @($Only | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $cases = @( + $cases | Where-Object { + $id = $_.Id + $wanted -contains $id -or ($id -match '^\d+$' -and $wanted -contains ([string][int]$id)) + } + ) + + if ($cases.Count -eq 0) { + throw "No consumer cases matched -Only '$($wanted -join ', ')'." + } +} + foreach ($case in $cases) { $slug = ($case.Name -replace '[^a-zA-Z0-9]+', '-').Trim('-').ToLowerInvariant() $directory = Join-Path $WorkPath ("{0}-{1}" -f $case.Id, $slug) @@ -312,6 +448,21 @@ foreach ($case in $cases) { } } + if ($passed -and $case.RequireText) { + $found = @( + Get-ChildItem -Path $directory -Recurse -File -Include *.cs, *.csproj, *.json -ErrorAction SilentlyContinue | + Select-String -Pattern $case.RequireText -SimpleMatch -ErrorAction SilentlyContinue + ) + + if ($found.Count -eq 0) { + $passed = $false + $signal = "$signal; '$($case.RequireText)' was not generated into the consumer" + } + else { + $signal = "$signal; generated '$($case.RequireText)'" + } + } + $results.Add( [pscustomobject]@{ Id = $case.Id diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 39f39eb..043be86 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - Purview.WslContainers + Purview.Containers net11.0-windows10.0.19041.0 x64 10.0.26100.80 diff --git a/src/WSLTestContainers.slnx b/src/WSLTestContainers.slnx index cf4e852..3c4f6b7 100644 --- a/src/WSLTestContainers.slnx +++ b/src/WSLTestContainers.slnx @@ -11,18 +11,28 @@ + + - + + + + + + + + + @@ -36,7 +46,7 @@ - - + + diff --git a/src/src/Azurite/Azurite.csproj b/src/src/Azurite/Azurite.csproj index e9cd8ce..be5a7f7 100644 --- a/src/src/Azurite/Azurite.csproj +++ b/src/src/Azurite/Azurite.csproj @@ -1,13 +1,13 @@ true - Azurite module for Purview.WslContainers: throwaway Azure Storage emulators (blob/queue/table) on WSL Containers. + Azurite module for Purview.Containers: throwaway Azure Storage emulators (blob/queue/table) on WSL Containers. wsl;containers;azurite;azure;storage;testing;integration MIT Purview - + diff --git a/src/src/Azurite/AzuriteAccount.cs b/src/src/Azurite/AzuriteAccount.cs index ce559ac..f1d9c34 100644 --- a/src/src/Azurite/AzuriteAccount.cs +++ b/src/src/Azurite/AzuriteAccount.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Azurite; +namespace Purview.Containers.Azurite; /// Well-known Azurite emulator account details. public static class AzuriteAccount diff --git a/src/src/Azurite/AzuriteBuilder.cs b/src/src/Azurite/AzuriteBuilder.cs index 5990692..ce2dd7a 100644 --- a/src/src/Azurite/AzuriteBuilder.cs +++ b/src/src/Azurite/AzuriteBuilder.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.Azurite; +namespace Purview.Containers.Azurite; /// Fluent builder for an Azurite (Azure Storage emulator) test container. public class AzuriteBuilder : ContainerBuilder @@ -32,8 +32,8 @@ public AzuriteBuilder(string image) } /// Creates a builder using an explicit runtime. - public AzuriteBuilder(IContainerRuntime runtime) - : base(runtime) + public AzuriteBuilder(IContainerBackend backend) + : base(backend) { WithImage(AzuriteImage) .WithCommand("azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0") @@ -58,5 +58,5 @@ protected override AzuriteConfiguration BuildConfiguration() /// protected override AzuriteContainer CreateContainer(AzuriteConfiguration configuration) => - new(configuration, Runtime ?? WslContainerRuntime.Instance); + new(configuration, Backend); } diff --git a/src/src/Azurite/AzuriteConfiguration.cs b/src/src/Azurite/AzuriteConfiguration.cs index 5fe9554..09e2eea 100644 --- a/src/src/Azurite/AzuriteConfiguration.cs +++ b/src/src/Azurite/AzuriteConfiguration.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Azurite; +namespace Purview.Containers.Azurite; /// Immutable configuration for an Azurite test container. public sealed record AzuriteConfiguration : ContainerConfiguration { } diff --git a/src/src/Azurite/AzuriteContainer.cs b/src/src/Azurite/AzuriteContainer.cs index 973d5de..a3f22ef 100644 --- a/src/src/Azurite/AzuriteContainer.cs +++ b/src/src/Azurite/AzuriteContainer.cs @@ -1,13 +1,13 @@ using System.Globalization; using System.Text; -namespace Purview.WslContainers.Azurite; +namespace Purview.Containers.Azurite; /// A throwaway Azurite (Azure Storage emulator) instance running on WSL Containers. -public sealed class AzuriteContainer : WslContainer +public sealed class AzuriteContainer : ContainerBase { - internal AzuriteContainer(AzuriteConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) { } + internal AzuriteContainer(AzuriteConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { } /// Blob service endpoint. Safe to call after . public Uri GetBlobEndpoint() diff --git a/src/src/Azurite/Sdk/README.md b/src/src/Azurite/Sdk/README.md index 4e469ba..acb818e 100644 --- a/src/src/Azurite/Sdk/README.md +++ b/src/src/Azurite/Sdk/README.md @@ -1,29 +1,32 @@ -# Purview.WslContainers.Azurite +# Purview.Containers.Azurite Throwaway [Azurite](https://github.com/Azure/Azurite) (Azure Storage emulator) instances for .NET -integration testing, running as WSLC containers on **Microsoft WSL Containers** — no Docker installation. +integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.Azurite +dotnet add package Purview.Containers.Azurite ``` -Depends on `Purview.WslContainers` (the core runtime). +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md). ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. ## Quick start ```csharp -using Purview.WslContainers.Azurite; +using Purview.Containers.Azurite; await using var azurite = new AzuriteBuilder().Build(); diff --git a/src/src/Containers/Container.cs b/src/src/Containers/Container.cs new file mode 100644 index 0000000..8f658d4 --- /dev/null +++ b/src/src/Containers/Container.cs @@ -0,0 +1,13 @@ +namespace Purview.Containers; + +/// +/// The generic, backend-agnostic container produced by . The backend is +/// resolved when the container starts, so a builder can be configured without knowing which runtime will +/// run it. +/// +public class Container : ContainerBase +{ + /// Creates a container for the configuration, optionally bound to a specific backend. + public Container(IContainerConfiguration configuration, IContainerBackend? backend = null) + : base(configuration, backend) { } +} diff --git a/src/src/Containers/ContainerBackendInfo.cs b/src/src/Containers/ContainerBackendInfo.cs new file mode 100644 index 0000000..a958b1f --- /dev/null +++ b/src/src/Containers/ContainerBackendInfo.cs @@ -0,0 +1,22 @@ +namespace Purview.Containers; + +/// +/// Availability and version information reported by a container backend. Used by diagnostics and by the +/// backend selection policy: a backend is selectable when it is both available and compatible. +/// +/// Backend identifier, matching . +/// True when every component the backend needs is installed. +/// True when the installed components are compatible with this library. +/// Reported backend version, or an empty string when unknown. +/// Components that are missing or need an update. +public sealed record ContainerBackendInfo( + string Name, + bool IsAvailable, + bool IsCompatible, + string Version, + IReadOnlyList MissingComponents +) +{ + /// True when the backend can be selected. + public bool IsUsable => IsAvailable && IsCompatible; +} diff --git a/src/src/Containers/ContainerBackendSelection.cs b/src/src/Containers/ContainerBackendSelection.cs new file mode 100644 index 0000000..7a97bc8 --- /dev/null +++ b/src/src/Containers/ContainerBackendSelection.cs @@ -0,0 +1,53 @@ +namespace Purview.Containers; + +/// +/// Chooses which container backend runs new containers: automatic detection, or one named backend. +/// +/// +/// The value comes from, in order of precedence: , +/// the PURVIEW_CONTAINERS_BACKEND environment variable (see +/// ), and finally . +/// A backend named here is used even when another backend would also work, and the resolution fails with +/// the named backend's diagnostics instead of silently falling back. +/// +public readonly record struct ContainerBackendSelection +{ + ContainerBackendSelection(string? name) => Name = name; + + /// Probe every registered backend and use the first usable one. + public static ContainerBackendSelection Auto { get; } = new(null); + + /// Uses the backend registered under (e.g. wsl, docker). + public static ContainerBackendSelection Named(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + return new ContainerBackendSelection(name.Trim()); + } + + /// The backend name, or null for automatic detection. + public string? Name { get; } + + /// True when the backend should be detected by probing. + public bool IsAuto => Name is null; + + /// + /// Parses a configuration value. null, empty, whitespace and auto (case-insensitive) + /// mean automatic detection; any other value names a backend. + /// + public static ContainerBackendSelection Parse(string? value) + { + if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "auto", StringComparison.OrdinalIgnoreCase)) + { + return Auto; + } + + return Named(value); + } + + /// Reads the selection from the PURVIEW_CONTAINERS_BACKEND environment variable. + public static ContainerBackendSelection FromEnvironment() => + Parse(Environment.GetEnvironmentVariable(ContainerBackends.SelectionEnvironmentVariable)); + + /// + public override string ToString() => Name ?? "auto"; +} diff --git a/src/src/Containers/ContainerBackends.cs b/src/src/Containers/ContainerBackends.cs new file mode 100644 index 0000000..8b43505 --- /dev/null +++ b/src/src/Containers/ContainerBackends.cs @@ -0,0 +1,293 @@ +using System.Text; +using Purview.Containers.Runtime; + +namespace Purview.Containers; + +/// +/// Registry and selection point for the container backends available to this process. +/// +/// +/// +/// Backend packages register themselves from the consuming assembly through the generated module +/// initializer in Purview.Containers.Backends.targets, so a package consumer gets registration +/// without reflection and without depending on which assemblies the runtime happens to load. A +/// project-reference consumer (this repository's own tests, for example) can register explicitly with +/// . +/// +/// +/// The backend that actually runs is chosen by using this precedence: +/// a backend pinned with , then the requested +/// (set in code or read from ), then +/// automatic probing of every registered backend. A named backend never falls back to another one: the +/// failure carries that backend's own diagnostics. +/// +/// +public static class ContainerBackends +{ + /// + /// Environment variable that selects the backend: auto (the default), or the name of a + /// registered backend such as wsl or docker. Case-insensitive; an unrecognised value + /// fails the resolution rather than being ignored. + /// + public const string SelectionEnvironmentVariable = "PURVIEW_CONTAINERS_BACKEND"; + + static readonly Lock Sync = new(); + static readonly List Registered = []; + static IContainerBackend? PinnedBackend; + static ContainerBackendSelection? PinnedSelection; + static Task? Resolution; + + /// Every registered backend, in registration order. + public static IReadOnlyList All + { + get + { + lock (Sync) + { + return [.. Registered]; + } + } + } + + /// + /// The requested selection: the value set with , otherwise + /// the value, otherwise + /// . + /// + public static ContainerBackendSelection Selection + { + get + { + lock (Sync) + { + return PinnedSelection ?? ContainerBackendSelection.FromEnvironment(); + } + } + } + + /// + /// Registers a backend. Idempotent: registering a backend whose + /// is already present replaces that entry, so an explicitly registered backend and a generated + /// registration can coexist. + /// + public static void Register(IContainerBackend backend) + { + ArgumentNullException.ThrowIfNull(backend); + lock (Sync) + { + var index = Registered.FindIndex(existing => + string.Equals(existing.Name, backend.Name, StringComparison.Ordinal) + ); + if (index >= 0) + { + Registered[index] = backend; + } + else + { + Registered.Add(backend); + } + + Resolution = null; + } + } + + /// + /// Pins a backend instance, which then wins over every other selection rule. Pass null to return + /// to the configured selection. + /// + public static void Use(IContainerBackend? backend) + { + lock (Sync) + { + PinnedBackend = backend; + Resolution = null; + } + } + + /// + /// Pins the selection (automatic detection or one named backend), which wins over the environment + /// variable. Pass null to fall back to the environment. + /// + public static void Use(ContainerBackendSelection? selection) + { + lock (Sync) + { + PinnedSelection = selection; + Resolution = null; + } + } + + /// + /// Resolves the backend to use, probing usability when necessary and caching the result for the process. + /// + /// + /// No backend is registered, the requested backend is not registered, or no registered backend is usable. + /// The message carries each backend's diagnostics. + /// + public static async Task ResolveAsync(CancellationToken cancellationToken = default) + { + var cached = Resolution; + if (cached is not null) + { + return await cached.ConfigureAwait(false); + } + + Task resolution; + lock (Sync) + { + resolution = Resolution ??= ResolveCoreAsync(PinnedBackend, Selection, [.. Registered], cancellationToken); + } + + try + { + return await resolution.ConfigureAwait(false); + } + catch + { + // Do not cache a failed resolution: a host that installs a backend while the process runs, or a + // test that registers one after a failure, must be able to resolve again. + lock (Sync) + { + if (ReferenceEquals(Resolution, resolution)) + { + Resolution = null; + } + } + + throw; + } + } + + /// + /// Probes every registered backend and reports its availability, version and missing components. + /// Intended for diagnostics and for callers that want to apply their own policy. + /// + public static async Task> ProbeAllAsync( + CancellationToken cancellationToken = default + ) + { + List probes = new(); + foreach (var backend in All) + { + probes.Add(await ProbeAsync(backend, cancellationToken).ConfigureAwait(false)); + } + + return probes; + } + + /// Clears every registration, every selection and the cached resolution. Intended for tests. + public static void Reset() + { + lock (Sync) + { + Registered.Clear(); + PinnedBackend = null; + PinnedSelection = null; + Resolution = null; + } + } + + static async Task ResolveCoreAsync( + IContainerBackend? pinned, + ContainerBackendSelection selection, + IContainerBackend[] registered, + CancellationToken cancellationToken + ) + { + if (registered.Length == 0) + { + throw new ContainerBackendUnavailableException( + "No container backend is registered. Reference a backend package (for example " + + "'Purview.Containers.Wsl' on Windows or 'Purview.Containers.Docker' with a reachable " + + "Docker daemon) or register one explicitly with ContainerBackends.Register(...)." + ); + } + + if (pinned is not null) + { + var pinnedInfo = await ProbeAsync(pinned, cancellationToken).ConfigureAwait(false); + if (!pinnedInfo.IsUsable) + { + throw new ContainerBackendUnavailableException( + $"The pinned container backend '{pinned.Name}' is not usable: {Describe(pinnedInfo)}." + ); + } + + return pinned; + } + + if (!selection.IsAuto) + { + var named = + registered.FirstOrDefault(backend => + string.Equals(backend.Name, selection.Name, StringComparison.OrdinalIgnoreCase) + ) + ?? throw new ContainerBackendUnavailableException( + $"The container backend '{selection.Name}' is not registered. Registered backends: " + + $"{string.Join(", ", registered.Select(backend => backend.Name))}." + ); + + var namedInfo = await ProbeAsync(named, cancellationToken).ConfigureAwait(false); + if (!namedInfo.IsUsable) + { + throw new ContainerBackendUnavailableException( + $"The selected container backend '{named.Name}' is not usable: {Describe(namedInfo)}." + ); + } + + return named; + } + + StringBuilder report = new("No usable container backend was found (selection: auto)."); + foreach (var backend in registered) + { + var info = await ProbeAsync(backend, cancellationToken).ConfigureAwait(false); + report.Append(Environment.NewLine).Append(" ").Append(Describe(info)); + if (info.IsUsable) + { + return backend; + } + } + + report + .Append(Environment.NewLine) + .Append("Install or fix a backend, or set ") + .Append(SelectionEnvironmentVariable) + .Append(" to one of: ") + .Append(string.Join(", ", registered.Select(backend => backend.Name))) + .Append('.'); + throw new ContainerBackendUnavailableException(report.ToString()); + } + + static async Task ProbeAsync(IContainerBackend backend, CancellationToken cancellationToken) + { + try + { + return await backend.GetInfoAsync(cancellationToken).ConfigureAwait(false) + ?? Unavailable(backend.Name, "the backend reported no information"); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + return Unavailable(backend.Name, $"{exception.GetType().Name}: {exception.Message}"); + } + } + + static ContainerBackendInfo Unavailable(string name, string reason) => + new(name, IsAvailable: false, IsCompatible: false, Version: string.Empty, MissingComponents: [reason]); + + static string Describe(ContainerBackendInfo info) + { + if (!info.IsAvailable) + { + return $"{info.Name}: unavailable ({string.Join("; ", info.MissingComponents)})"; + } + + if (!info.IsCompatible) + { + return $"{info.Name}: incompatible, version {info.Version} " + + $"({string.Join("; ", info.MissingComponents)})"; + } + + return $"{info.Name}: available, version {info.Version}"; + } +} diff --git a/src/src/Containers/ContainerBase.cs b/src/src/Containers/ContainerBase.cs new file mode 100644 index 0000000..38886bf --- /dev/null +++ b/src/src/Containers/ContainerBase.cs @@ -0,0 +1,133 @@ +namespace Purview.Containers; + +/// +/// Base class for the typed containers a service module exposes. It creates the backend container on +/// and delegates every member to it, which keeps the +/// module packages backend-neutral: a module package depends on this abstraction assembly only. +/// +public abstract class ContainerBase : IContainer +{ + readonly IContainerConfiguration _configuration; + readonly IContainerBackend? _backend; + IContainer? _container; + int _started; + int _disposed; + + /// + /// Creates a container bound to a backend. Prefer building via a module builder. When + /// is null (the default) the backend is resolved on + /// through . + /// + protected ContainerBase(IContainerConfiguration configuration, IContainerBackend? backend = null) + { + ArgumentNullException.ThrowIfNull(configuration); + _configuration = configuration; + _backend = backend; + Name = ContainerName.Generate(configuration); + } + + /// + /// The backend that will create the container, or null to resolve it on + /// . + /// + protected IContainerBackend? Backend => _backend; + + /// The immutable configuration this container was built from. + protected IContainerConfiguration Configuration => _configuration; + + /// The started backend container. + /// The container has not been started. + protected IContainer Container => + _container ?? throw new InvalidOperationException("Container has not been started. Call StartAsync() first."); + + /// + public string Name { get; } + + /// + public string Id => _container?.Id ?? string.Empty; + + /// + public ContainerState State => _container?.State ?? ContainerState.Created; + + /// + public string Image => _configuration.Image; + + /// + public virtual async Task StartAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + if (Interlocked.Exchange(ref _started, 1) == 1) + { + return; + } + + var backend = _backend ?? await ContainerBackends.ResolveAsync(cancellationToken).ConfigureAwait(false); + _container = backend.CreateContainer(WithResolvedName()); + await _container.StartAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public virtual Task StopAsync(CancellationToken cancellationToken = default) + { + return _container?.StopAsync(cancellationToken) ?? Task.CompletedTask; + } + + /// + public virtual Task ExecAsync( + string[] command, + ExecOptions? options = null, + CancellationToken cancellationToken = default + ) + { + return Container.ExecAsync(command, options, cancellationToken); + } + + /// + public virtual ushort GetMappedPublicPort(ushort containerPort) + { + return Container.GetMappedPublicPort(containerPort); + } + + /// + public virtual IReadOnlyDictionary GetMappedPublicPorts() + { + return Container.GetMappedPublicPorts(); + } + + /// + public virtual Task GetLogsAsync(LogOutput? stream = null, CancellationToken cancellationToken = default) + { + return Container.GetLogsAsync(stream, cancellationToken); + } + + /// + public virtual IAsyncEnumerable GetLogsAsync(CancellationToken cancellationToken) + { + return Container.GetLogsAsync(cancellationToken); + } + + /// + public virtual async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + { + return; + } + + if (_container is not null) + { + await _container.DisposeAsync().ConfigureAwait(false); + } + + GC.SuppressFinalize(this); + } + + /// + /// Snapshots the configuration with the resolved container name so the backend creates the container + /// under the name this instance reports. + /// + IContainerConfiguration WithResolvedName() + { + return _configuration is ContainerConfiguration concrete ? concrete with { Name = Name } : _configuration; + } +} diff --git a/src/src/WslContainers/ContainerBuilder.cs b/src/src/Containers/ContainerBuilder.cs similarity index 80% rename from src/src/WslContainers/ContainerBuilder.cs rename to src/src/Containers/ContainerBuilder.cs index fc83484..868649b 100644 --- a/src/src/WslContainers/ContainerBuilder.cs +++ b/src/src/Containers/ContainerBuilder.cs @@ -1,20 +1,20 @@ -using Purview.WslContainers.Images; -using Purview.WslContainers.Mounts; -using Purview.WslContainers.Networking; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Images; +using Purview.Containers.Mounts; +using Purview.Containers.Networking; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers; /// /// Fluent builder base for containers and strongly typed service modules. /// Builder instances accumulate configuration; produces an immutable configuration -/// and a container bound to a runtime. +/// and a container bound to a backend. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1005:Avoid excessive parameters on generic types")] public abstract class ContainerBuilder where TBuilder : ContainerBuilder - where TContainer : WslContainer + where TContainer : IContainer where TConfiguration : ContainerConfiguration, new() { string? _image; @@ -37,17 +37,17 @@ public abstract class ContainerBuilder readonly List _waitStrategies = []; RegistryCredentials? _registryCredentials; - /// The runtime the built container will use. Defaults to the process-wide . - protected IContainerRuntime? Runtime { get; private set; } + /// The backend the built container will use. When null it is resolved at start time via . + protected IContainerBackend? Backend { get; private set; } - /// Creates a builder using the default process-wide runtime. + /// Creates a builder using the default backend. protected ContainerBuilder() { } - /// Creates a builder using an explicit runtime. - protected ContainerBuilder(IContainerRuntime runtime) + /// Creates a builder using an explicit backend. + protected ContainerBuilder(IContainerBackend backend) { - ArgumentNullException.ThrowIfNull(runtime); - Runtime = runtime; + ArgumentNullException.ThrowIfNull(backend); + Backend = backend; } /// Sets the container image. Invalid references fail fast (at configuration time). @@ -60,7 +60,7 @@ public TBuilder WithImage(string image) } catch (ArgumentException ex) { - throw new WslContainerConfigurationException(ex.Message, ex); + throw new ContainerConfigurationException(ex.Message, ex); } return (TBuilder)this; @@ -79,7 +79,7 @@ public TBuilder WithTag(string tag) { if (_parsedImage is not Image current) { - throw new WslContainerConfigurationException("Set an image with WithImage(...) before applying a tag."); + throw new ContainerConfigurationException("Set an image with WithImage(...) before applying a tag."); } try @@ -89,7 +89,7 @@ public TBuilder WithTag(string tag) } catch (ArgumentException ex) { - throw new WslContainerConfigurationException(ex.Message, ex); + throw new ContainerConfigurationException(ex.Message, ex); } return (TBuilder)this; @@ -273,11 +273,11 @@ public TBuilder WithRegistryCredentials(Uri serverAddress, string username, stri return (TBuilder)this; } - /// Sets the runtime the built container binds to. - public TBuilder WithRuntime(IContainerRuntime runtime) + /// Pins the backend the built container is created on, overriding . + public TBuilder WithBackend(IContainerBackend backend) { - ArgumentNullException.ThrowIfNull(runtime); - Runtime = runtime; + ArgumentNullException.ThrowIfNull(backend); + Backend = backend; return (TBuilder)this; } @@ -296,7 +296,7 @@ protected virtual TConfiguration BuildConfiguration() { Image = _image - ?? throw new WslContainerConfigurationException( + ?? throw new ContainerConfigurationException( "An image is required. Call WithImage(...) before Build()." ), Name = _name, @@ -326,33 +326,26 @@ protected virtual void Validate(ContainerConfiguration configuration) if (string.IsNullOrWhiteSpace(configuration.Image)) { - throw new WslContainerConfigurationException("An image is required."); + throw new ContainerConfigurationException("An image is required."); } if (configuration.StartupTimeout <= TimeSpan.Zero) { - throw new WslContainerConfigurationException( + throw new ContainerConfigurationException( $"Startup timeout must be positive; got {configuration.StartupTimeout}." ); } foreach (var portBinding in configuration.PortBindings) { - if (portBinding.Protocol == PortProtocol.Udp) - { - throw new WslContainerNotSupportedException( - "UDP port mappings are not supported by the WSLC managed API." - ); - } - if (portBinding.ContainerPort == 0) { - throw new WslContainerConfigurationException("Container port 0 is invalid for a port binding."); + throw new ContainerConfigurationException("Container port 0 is invalid for a port binding."); } if (portBinding.HostPort == 0) { - throw new WslContainerConfigurationException("Host port 0 is invalid; use a random host port instead."); + throw new ContainerConfigurationException("Host port 0 is invalid; use a random host port instead."); } } @@ -360,7 +353,7 @@ protected virtual void Validate(ContainerConfiguration configuration) { if (string.IsNullOrWhiteSpace(bindMount.HostPath) || string.IsNullOrWhiteSpace(bindMount.ContainerPath)) { - throw new WslContainerConfigurationException("Bind mounts require a host path and a container path."); + throw new ContainerConfigurationException("Bind mounts require a host path and a container path."); } } @@ -368,31 +361,28 @@ protected virtual void Validate(ContainerConfiguration configuration) { if (string.IsNullOrWhiteSpace(namedVolume.Name) || string.IsNullOrWhiteSpace(namedVolume.ContainerPath)) { - throw new WslContainerConfigurationException( - "Named volumes require a volume name and a container path." - ); + throw new ContainerConfigurationException("Named volumes require a volume name and a container path."); } } } - /// Creates the container object bound to a runtime. + /// Creates the container object bound to a backend. protected abstract TContainer CreateContainer(TConfiguration configuration); } /// Fluent builder for a generic container. -public class ContainerBuilder : ContainerBuilder +public class ContainerBuilder : ContainerBuilder { /// Creates a builder for a generic container. public ContainerBuilder() { } - /// Creates a builder for a generic container using an explicit runtime. - public ContainerBuilder(IContainerRuntime runtime) - : base(runtime) { } + /// Creates a builder for a generic container using an explicit backend. + public ContainerBuilder(IContainerBackend backend) + : base(backend) { } /// - protected override WslContainer CreateContainer(ContainerConfiguration configuration) + protected override Container CreateContainer(ContainerConfiguration configuration) { - var runtime = Runtime ?? WslContainerRuntime.Instance; - return new WslContainer(configuration, runtime); + return new Container(configuration, Backend); } } diff --git a/src/src/WslContainers/ContainerConfiguration.cs b/src/src/Containers/ContainerConfiguration.cs similarity index 94% rename from src/src/WslContainers/ContainerConfiguration.cs rename to src/src/Containers/ContainerConfiguration.cs index 020b91b..e5fc4e9 100644 --- a/src/src/WslContainers/ContainerConfiguration.cs +++ b/src/src/Containers/ContainerConfiguration.cs @@ -1,11 +1,11 @@ using System.Text; -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Images; -using Purview.WslContainers.Mounts; -using Purview.WslContainers.Networking; -using Purview.WslContainers.Waiting; +using Purview.Containers.Diagnostics; +using Purview.Containers.Images; +using Purview.Containers.Mounts; +using Purview.Containers.Networking; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers; /// Immutable configuration for a generic WSLC container. public record ContainerConfiguration : IContainerConfiguration diff --git a/src/src/WslContainers/Containers/ContainerLogEntry.cs b/src/src/Containers/ContainerLogEntry.cs similarity index 79% rename from src/src/WslContainers/Containers/ContainerLogEntry.cs rename to src/src/Containers/ContainerLogEntry.cs index 7f8e6c1..cfc5e26 100644 --- a/src/src/WslContainers/Containers/ContainerLogEntry.cs +++ b/src/src/Containers/ContainerLogEntry.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Containers; +namespace Purview.Containers; /// A single line of container init-process output. public sealed record ContainerLogEntry(DateTimeOffset Timestamp, LogOutput Stream, string Text); diff --git a/src/src/Containers/ContainerName.cs b/src/src/Containers/ContainerName.cs new file mode 100644 index 0000000..1f08d85 --- /dev/null +++ b/src/src/Containers/ContainerName.cs @@ -0,0 +1,41 @@ +namespace Purview.Containers; + +/// +/// Generates the unique container name used when a configuration does not set one. Shared by every +/// backend so a name is stable before and after the container starts. +/// +public static class ContainerName +{ + /// Builds a name from the image's short name, the process id and a random suffix. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1031:Do not catch general exception types", + Justification = "A container name must always be produced, even for an unparseable image reference." + )] + public static string Generate(string image) + { + string shortName; + try + { + shortName = Images.Image.Parse(image).ShortName; + } + catch + { + shortName = "container"; + } + + string safe = new([ + .. shortName.Select(character => + char.IsAsciiLetterOrDigit(character) || character is '-' or '_' ? character : '-' + ), + ]); + return $"{safe}-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..6]}"; + } + + /// Builds a name for a configuration, preferring an explicitly configured name. + public static string Generate(IContainerConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + return configuration.Name ?? Generate(configuration.Image); + } +} diff --git a/src/src/WslContainers/Containers/ContainerState.cs b/src/src/Containers/ContainerState.cs similarity index 91% rename from src/src/WslContainers/Containers/ContainerState.cs rename to src/src/Containers/ContainerState.cs index 09df5b4..506fa5e 100644 --- a/src/src/WslContainers/Containers/ContainerState.cs +++ b/src/src/Containers/ContainerState.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Containers; +namespace Purview.Containers; /// Lifecycle state of a WSLC container. public enum ContainerState diff --git a/src/src/Containers/Containers.csproj b/src/src/Containers/Containers.csproj new file mode 100644 index 0000000..570d3e0 --- /dev/null +++ b/src/src/Containers/Containers.csproj @@ -0,0 +1,9 @@ + + + true + Backend-neutral container testing abstractions for .NET: a Testcontainers-style API that can run on WSL Containers or Docker. + containers;testing;integration;testcontainers;abstractions + MIT + Purview + + diff --git a/src/src/Containers/Diagnostics/ContainerActivity.cs b/src/src/Containers/Diagnostics/ContainerActivity.cs new file mode 100644 index 0000000..d9cbbb2 --- /dev/null +++ b/src/src/Containers/Diagnostics/ContainerActivity.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; + +namespace Purview.Containers.Diagnostics; + +/// +/// Activity source for backend-neutral container operations. Listener names: +/// Purview.Containers (the WSL Containers backend emits to the same name). +/// +static class ContainerActivity +{ + /// Activities: wait. + public static readonly ActivitySource Source = new("Purview.Containers"); +} diff --git a/src/src/WslContainers/Diagnostics/Secret.cs b/src/src/Containers/Diagnostics/Secret.cs similarity index 93% rename from src/src/WslContainers/Diagnostics/Secret.cs rename to src/src/Containers/Diagnostics/Secret.cs index bc8ee2c..c1eac6d 100644 --- a/src/src/WslContainers/Diagnostics/Secret.cs +++ b/src/src/Containers/Diagnostics/Secret.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Diagnostics; +namespace Purview.Containers.Diagnostics; /// /// Wraps a sensitive value so it is never printed by default diagnostics. diff --git a/src/src/WslContainers/Diagnostics/SecretRedactor.cs b/src/src/Containers/Diagnostics/SecretRedactor.cs similarity index 94% rename from src/src/WslContainers/Diagnostics/SecretRedactor.cs rename to src/src/Containers/Diagnostics/SecretRedactor.cs index 08e7e66..5265036 100644 --- a/src/src/WslContainers/Diagnostics/SecretRedactor.cs +++ b/src/src/Containers/Diagnostics/SecretRedactor.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Diagnostics; +namespace Purview.Containers.Diagnostics; /// Redacts sensitive values from diagnostics and configuration rendering. static class SecretRedactor diff --git a/src/src/WslContainers/Containers/ExecOptions.cs b/src/src/Containers/ExecOptions.cs similarity index 94% rename from src/src/WslContainers/Containers/ExecOptions.cs rename to src/src/Containers/ExecOptions.cs index ccf2980..ffa8236 100644 --- a/src/src/WslContainers/Containers/ExecOptions.cs +++ b/src/src/Containers/ExecOptions.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Containers; +namespace Purview.Containers; /// Options for . public sealed record ExecOptions diff --git a/src/src/WslContainers/Containers/ExecResult.cs b/src/src/Containers/ExecResult.cs similarity index 85% rename from src/src/WslContainers/Containers/ExecResult.cs rename to src/src/Containers/ExecResult.cs index 4ef5489..8de48b3 100644 --- a/src/src/WslContainers/Containers/ExecResult.cs +++ b/src/src/Containers/ExecResult.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Containers; +namespace Purview.Containers; /// Result of executing a command inside a container. public sealed record ExecResult(long ExitCode, string Stdout, string Stderr) diff --git a/src/src/WslContainers/IContainer.cs b/src/src/Containers/IContainer.cs similarity index 95% rename from src/src/WslContainers/IContainer.cs rename to src/src/Containers/IContainer.cs index 2db4bbc..e7b2a46 100644 --- a/src/src/WslContainers/IContainer.cs +++ b/src/src/Containers/IContainer.cs @@ -1,6 +1,4 @@ -using Purview.WslContainers.Containers; - -namespace Purview.WslContainers; +namespace Purview.Containers; /// A throwaway Linux container running on WSL Containers. public interface IContainer : IAsyncDisposable diff --git a/src/src/Containers/IContainerBackend.cs b/src/src/Containers/IContainerBackend.cs new file mode 100644 index 0000000..d40756e --- /dev/null +++ b/src/src/Containers/IContainerBackend.cs @@ -0,0 +1,18 @@ +namespace Purview.Containers; + +/// +/// A container runtime provider: it creates containers and reports whether it is usable on this host. +/// Implemented by the backend packages (Purview.Containers.Wsl, Purview.Containers.Docker) +/// and selected through . +/// +public interface IContainerBackend +{ + /// Stable backend identifier, for example wsl or docker. + string Name { get; } + + /// Creates (but does not start) a container for the given configuration. + IContainer CreateContainer(IContainerConfiguration configuration); + + /// Reports availability, version and missing components for diagnostics and selection. + Task GetInfoAsync(CancellationToken cancellationToken = default); +} diff --git a/src/src/WslContainers/IContainerBuilder.cs b/src/src/Containers/IContainerBuilder.cs similarity index 89% rename from src/src/WslContainers/IContainerBuilder.cs rename to src/src/Containers/IContainerBuilder.cs index 0b3e680..67767a5 100644 --- a/src/src/WslContainers/IContainerBuilder.cs +++ b/src/src/Containers/IContainerBuilder.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers; /// Produces an immutable from fluent configuration. public interface IContainerBuilder diff --git a/src/src/WslContainers/IContainerConfiguration.cs b/src/src/Containers/IContainerConfiguration.cs similarity index 92% rename from src/src/WslContainers/IContainerConfiguration.cs rename to src/src/Containers/IContainerConfiguration.cs index b9d5631..dcbc9af 100644 --- a/src/src/WslContainers/IContainerConfiguration.cs +++ b/src/src/Containers/IContainerConfiguration.cs @@ -1,9 +1,9 @@ -using Purview.WslContainers.Images; -using Purview.WslContainers.Mounts; -using Purview.WslContainers.Networking; -using Purview.WslContainers.Waiting; +using Purview.Containers.Images; +using Purview.Containers.Mounts; +using Purview.Containers.Networking; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers; /// Immutable container configuration consumed by the runtime at start time. public interface IContainerConfiguration diff --git a/src/src/WslContainers/Images/Image.cs b/src/src/Containers/Images/Image.cs similarity index 92% rename from src/src/WslContainers/Images/Image.cs rename to src/src/Containers/Images/Image.cs index be23cb4..55dd82f 100644 --- a/src/src/WslContainers/Images/Image.cs +++ b/src/src/Containers/Images/Image.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Images; +namespace Purview.Containers.Images; /// /// A parsed container image reference (registry / repository / tag). Immutable; invalid references @@ -29,7 +29,7 @@ public readonly record struct Image public string FullReference => $"{Registry}/{Repository}:{Tag}"; /// Last repository segment, e.g. alpine. - public string ShortName => Repository[(Repository.LastIndexOf('/', StringComparison.Ordinal) + 1)..]; + public string ShortName => Repository[(Repository.LastIndexOf('/') + 1)..]; /// /// Parses an image reference. Accepts alpine, alpine:latest, docker.io/library/alpine:latest, @@ -45,8 +45,8 @@ public static Image Parse(string reference) } var tag = DefaultTag; - var lastSlash = reference.LastIndexOf('/', StringComparison.Ordinal); - var lastColon = reference.LastIndexOf(':', StringComparison.Ordinal); + var lastSlash = reference.LastIndexOf('/'); + var lastColon = reference.LastIndexOf(':'); if (lastColon > lastSlash) { tag = NormalizeTag(reference[(lastColon + 1)..]); @@ -101,8 +101,8 @@ public Image WithTag(string tag) } /// - /// Returns true when a name returned by - /// (e.g. alpine:latest) refers to this image. + /// Returns true when a name returned by the backend's image list (e.g. alpine:latest) refers + /// to this image. /// public bool MatchesStoredName(string storedName) { diff --git a/src/src/WslContainers/Images/ImagePullProgress.cs b/src/src/Containers/Images/ImagePullProgress.cs similarity index 81% rename from src/src/WslContainers/Images/ImagePullProgress.cs rename to src/src/Containers/Images/ImagePullProgress.cs index 6306193..11ed4bd 100644 --- a/src/src/WslContainers/Images/ImagePullProgress.cs +++ b/src/src/Containers/Images/ImagePullProgress.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Images; +namespace Purview.Containers.Images; /// Progress reported while pulling an image. public sealed record ImagePullProgress(string? Id, string Status, ulong CurrentBytes, ulong TotalBytes); diff --git a/src/src/WslContainers/Images/ImageSummary.cs b/src/src/Containers/Images/ImageSummary.cs similarity index 80% rename from src/src/WslContainers/Images/ImageSummary.cs rename to src/src/Containers/Images/ImageSummary.cs index d85344d..d413dc5 100644 --- a/src/src/WslContainers/Images/ImageSummary.cs +++ b/src/src/Containers/Images/ImageSummary.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Images; +namespace Purview.Containers.Images; /// Summary of an image present in the session store. public sealed record ImageSummary(string Name, ulong Size, DateTimeOffset CreatedTimestamp); diff --git a/src/src/WslContainers/Images/PullPolicy.cs b/src/src/Containers/Images/PullPolicy.cs similarity index 89% rename from src/src/WslContainers/Images/PullPolicy.cs rename to src/src/Containers/Images/PullPolicy.cs index 2c99788..10cca6b 100644 --- a/src/src/WslContainers/Images/PullPolicy.cs +++ b/src/src/Containers/Images/PullPolicy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Images; +namespace Purview.Containers.Images; /// Controls when an image is pulled. public enum PullPolicy diff --git a/src/src/WslContainers/Containers/LogStream.cs b/src/src/Containers/LogStream.cs similarity index 82% rename from src/src/WslContainers/Containers/LogStream.cs rename to src/src/Containers/LogStream.cs index 27e7268..1c04f8d 100644 --- a/src/src/WslContainers/Containers/LogStream.cs +++ b/src/src/Containers/LogStream.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Containers; +namespace Purview.Containers; /// Standard output stream of a container or exec process. public enum LogOutput diff --git a/src/src/WslContainers/Mounts/BindMount.cs b/src/src/Containers/Mounts/BindMount.cs similarity index 81% rename from src/src/WslContainers/Mounts/BindMount.cs rename to src/src/Containers/Mounts/BindMount.cs index 30f4eda..507aaef 100644 --- a/src/src/WslContainers/Mounts/BindMount.cs +++ b/src/src/Containers/Mounts/BindMount.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Mounts; +namespace Purview.Containers.Mounts; /// A bind mount of a Windows directory into the container. public sealed record BindMount(string HostPath, string ContainerPath, bool ReadOnly = false); diff --git a/src/src/WslContainers/Mounts/NamedVolume.cs b/src/src/Containers/Mounts/NamedVolume.cs similarity index 83% rename from src/src/WslContainers/Mounts/NamedVolume.cs rename to src/src/Containers/Mounts/NamedVolume.cs index bff93df..28db35b 100644 --- a/src/src/WslContainers/Mounts/NamedVolume.cs +++ b/src/src/Containers/Mounts/NamedVolume.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Mounts; +namespace Purview.Containers.Mounts; /// A named volume (session VHD) mounted into the container. Auto-provisioned on first use. public sealed record NamedVolume(string Name, string ContainerPath, bool ReadOnly = false); diff --git a/src/src/WslContainers/Networking/ContainerNetworkingMode.cs b/src/src/Containers/Networking/ContainerNetworkingMode.cs similarity index 89% rename from src/src/WslContainers/Networking/ContainerNetworkingMode.cs rename to src/src/Containers/Networking/ContainerNetworkingMode.cs index d139b58..302a397 100644 --- a/src/src/WslContainers/Networking/ContainerNetworkingMode.cs +++ b/src/src/Containers/Networking/ContainerNetworkingMode.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Networking; +namespace Purview.Containers.Networking; /// Container networking mode. WSLC only supports none and bridged. public enum ContainerNetworkingMode diff --git a/src/src/WslContainers/Networking/PortBinding.cs b/src/src/Containers/Networking/PortBinding.cs similarity index 94% rename from src/src/WslContainers/Networking/PortBinding.cs rename to src/src/Containers/Networking/PortBinding.cs index e7dc7f4..e28078c 100644 --- a/src/src/WslContainers/Networking/PortBinding.cs +++ b/src/src/Containers/Networking/PortBinding.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Networking; +namespace Purview.Containers.Networking; /// A host-to-container port mapping. /// The port inside the container. diff --git a/src/src/WslContainers/Networking/PortProtocol.cs b/src/src/Containers/Networking/PortProtocol.cs similarity index 81% rename from src/src/WslContainers/Networking/PortProtocol.cs rename to src/src/Containers/Networking/PortProtocol.cs index 1d1d7aa..bf894e0 100644 --- a/src/src/WslContainers/Networking/PortProtocol.cs +++ b/src/src/Containers/Networking/PortProtocol.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Networking; +namespace Purview.Containers.Networking; /// Port transport protocol. public enum PortProtocol diff --git a/src/src/WslContainers/RegistryCredentials.cs b/src/src/Containers/RegistryCredentials.cs similarity index 90% rename from src/src/WslContainers/RegistryCredentials.cs rename to src/src/Containers/RegistryCredentials.cs index 6998c9c..c9dc0f5 100644 --- a/src/src/WslContainers/RegistryCredentials.cs +++ b/src/src/Containers/RegistryCredentials.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Diagnostics; +using Purview.Containers.Diagnostics; -namespace Purview.WslContainers; +namespace Purview.Containers; /// Credentials for authenticating to a private container registry. public sealed record RegistryCredentials(Uri ServerAddress, string Username, Secret Password) diff --git a/src/src/Containers/Runtime/ContainerException.cs b/src/src/Containers/Runtime/ContainerException.cs new file mode 100644 index 0000000..d2eae77 --- /dev/null +++ b/src/src/Containers/Runtime/ContainerException.cs @@ -0,0 +1,41 @@ +namespace Purview.Containers.Runtime; + +#pragma warning disable CA1032 // Implement standard exception constructors + +/// Base exception for all errors raised by a container backend. +public class ContainerException : Exception +{ + public ContainerException(string message) + : base(message) { } + + public ContainerException(string message, Exception innerException) + : base(message, innerException) { } +} + +/// Invalid container configuration detected before any backend operation. +public class ContainerConfigurationException : ContainerException +{ + public ContainerConfigurationException(string message) + : base(message) { } + + public ContainerConfigurationException(string message, Exception innerException) + : base(message, innerException) { } +} + +/// The backend's prerequisites are missing or incompatible with this library. +public class ContainerPrerequisiteException(string message) : ContainerException(message) { } + +/// A requested feature is not supported by the selected backend. +public class ContainerNotSupportedException(string message) : ContainerException(message) { } + +/// Starting the container failed (image, runtime, or environment). +public class ContainerStartupException(string message, Exception innerException) + : ContainerException(message, innerException) { } + +/// An operation did not complete within its configured timeout. +public class ContainerTimeoutException(string message) : ContainerException(message) { } + +/// +/// No container backend is registered, or the explicitly selected backend cannot be used on this host. +/// +public class ContainerBackendUnavailableException(string message) : ContainerException(message) { } diff --git a/src/src/Containers/Sdk/README.md b/src/src/Containers/Sdk/README.md new file mode 100644 index 0000000..116ef9e --- /dev/null +++ b/src/src/Containers/Sdk/README.md @@ -0,0 +1,68 @@ +# Purview.Containers + +Backend-neutral container testing abstractions for .NET — the shared surface behind +[`Purview.Containers.Wsl`](https://www.nuget.org/packages/Purview.Containers.Wsl) (WSL Containers) and +`Purview.Containers.Docker` (Docker Engine via Testcontainers). + +```bash +dotnet add package Purview.Containers +``` + +Most consumers reference a backend package or a service module instead; this package arrives +transitively and supplies the model plus the backend registration hook. + +## What is in the package + +| Area | Types | +| --- | --- | +| Container contract | `IContainer`, `IContainerConfiguration`, `ContainerConfiguration`, `ContainerBuilder`, `ContainerBase` | +| Backends | `IContainerBackend`, `ContainerBackendInfo`, `ContainerBackends`, `ContainerBackendUnavailableException` | +| Readiness | `Purview.Containers.Waiting`: `Wait`, `IWaitStrategy`, TCP/HTTP/command/log strategies | +| Configuration model | `PortBinding`, `BindMount`, `NamedVolume`, `RegistryCredentials`, `Image`, `PullPolicy` | +| Diagnostics | `Secret`, `SecretRedactor`, `ContainerState`, `ExecResult`, `ContainerLogEntry` | +| Errors | `Purview.Containers.Runtime`: `ContainerException` and its specialisations | + +## Backend selection + +A container is produced by an `IContainerBackend`. Backend packages register themselves into the +consuming assembly through the `buildTransitive` assets shipped here: + +```csharp +// generated by Purview.Containers.targets +[ModuleInitializer] +internal static void Initialize() +{ + ContainerBackends.Register(WslContainerBackend.Create()); +} +``` + +`ContainerBackends.ResolveAsync()` picks the backend with this precedence: + +| # | Rule | How | +| --- | --- | --- | +| 1 | Pinned instance | `ContainerBackends.Use(new WslContainerBackend())`, or `WithBackend(...)` on one builder | +| 2 | Named backend | `ContainerBackends.Use(ContainerBackendSelection.Named("docker"))`, or `PURVIEW_CONTAINERS_BACKEND=docker` | +| 3 | Automatic detection | probe every registered backend; the first usable one wins (WSLC before Docker) | + +`PURVIEW_CONTAINERS_BACKEND` accepts `auto` (the default) or a backend name, and is case-insensitive. +A named or pinned backend **never** silently falls back: if it is missing or unusable, the resolution +fails with that backend's own diagnostics. When nothing is usable, the error lists every backend's +availability, version and missing components, and names the fix. Results are cached per process, so the +probe cost is paid once. + +`ContainerBackends.ProbeAllAsync()` returns the same probe information without selecting anything, for +callers that want to apply their own policy. + +## Requirements + +- **.NET 10 or later**, any platform (this package is portable). +- A **backend package**: `Purview.Containers.Wsl` (Windows, .NET 11) or `Purview.Containers.Docker` + (Docker Engine reachable from the test host). + +## Related + +- [Purview.Containers.Wsl](https://www.nuget.org/packages/Purview.Containers.Wsl) — WSL Containers backend. +- Documentation wiki: . + +> **Experimental.** The public API, defaults and packaging rules can change between prereleases, and +> there is no production support guarantee. Pin the exact package version you build against. diff --git a/src/src/Containers/Sdk/buildTransitive/Purview.Containers.props b/src/src/Containers/Sdk/buildTransitive/Purview.Containers.props new file mode 100644 index 0000000..87aaea5 --- /dev/null +++ b/src/src/Containers/Sdk/buildTransitive/Purview.Containers.props @@ -0,0 +1,16 @@ + + + + + + + diff --git a/src/src/Containers/Sdk/buildTransitive/Purview.Containers.targets b/src/src/Containers/Sdk/buildTransitive/Purview.Containers.targets new file mode 100644 index 0000000..d3f21f8 --- /dev/null +++ b/src/src/Containers/Sdk/buildTransitive/Purview.Containers.targets @@ -0,0 +1,50 @@ + + + + + + <_PurviewContainersBackendType Include="$(PurviewContainersBackends)" /> + <_PurviewContainersRegistrationLine Include="// <auto-generated/>" /> + <_PurviewContainersRegistrationLine Include="// Registers the container backends referenced by this project." /> + <_PurviewContainersRegistrationLine Include="internal static class PurviewContainersBackendRegistration" /> + <_PurviewContainersRegistrationLine Include="{" /> + <_PurviewContainersRegistrationLine Include=" [global::System.Runtime.CompilerServices.ModuleInitializer]" /> + <_PurviewContainersRegistrationLine Include=" internal static void Initialize()" /> + <_PurviewContainersRegistrationLine Include=" {" /> + <_PurviewContainersRegistrationLine Include="@(_PurviewContainersBackendType->' global::Purview.Containers.ContainerBackends.Register(global::%(Identity).Create());')" /> + <_PurviewContainersRegistrationLine Include=" }" /> + <_PurviewContainersRegistrationLine Include="}" /> + + + + <_PurviewContainersRegistrationFile>$(IntermediateOutputPath)Purview.Containers.Backends.g.cs + + + + + + + + + + diff --git a/src/src/WslContainers/Waiting/AllWaitStrategy.cs b/src/src/Containers/Waiting/AllWaitStrategy.cs similarity index 94% rename from src/src/WslContainers/Waiting/AllWaitStrategy.cs rename to src/src/Containers/Waiting/AllWaitStrategy.cs index 2c8170c..12b629b 100644 --- a/src/src/WslContainers/Waiting/AllWaitStrategy.cs +++ b/src/src/Containers/Waiting/AllWaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Composes strategies; ready only when all contained strategies are ready on the same check. public sealed class AllWaitStrategy : WaitStrategy diff --git a/src/src/WslContainers/Waiting/AnyWaitStrategy.cs b/src/src/Containers/Waiting/AnyWaitStrategy.cs similarity index 94% rename from src/src/WslContainers/Waiting/AnyWaitStrategy.cs rename to src/src/Containers/Waiting/AnyWaitStrategy.cs index 1c698bf..503923e 100644 --- a/src/src/WslContainers/Waiting/AnyWaitStrategy.cs +++ b/src/src/Containers/Waiting/AnyWaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Composes strategies; ready when any contained strategy is ready on the same check. public sealed class AnyWaitStrategy : WaitStrategy diff --git a/src/src/WslContainers/Waiting/CommandWaitStrategy.cs b/src/src/Containers/Waiting/CommandWaitStrategy.cs similarity index 96% rename from src/src/WslContainers/Waiting/CommandWaitStrategy.cs rename to src/src/Containers/Waiting/CommandWaitStrategy.cs index ae04827..a4ff9f5 100644 --- a/src/src/WslContainers/Waiting/CommandWaitStrategy.cs +++ b/src/src/Containers/Waiting/CommandWaitStrategy.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Waits until a command executed inside the container exits with the expected code (default 0). [SuppressMessage( diff --git a/src/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs b/src/src/Containers/Waiting/ContainerRunningWaitStrategy.cs similarity index 81% rename from src/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs rename to src/src/Containers/Waiting/ContainerRunningWaitStrategy.cs index 1841ba2..3671bfd 100644 --- a/src/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs +++ b/src/src/Containers/Waiting/ContainerRunningWaitStrategy.cs @@ -1,6 +1,4 @@ -using Purview.WslContainers.Containers; - -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Waits until the container init process is running. public sealed class ContainerRunningWaitStrategy : WaitStrategy diff --git a/src/src/WslContainers/Waiting/CustomWaitStrategy.cs b/src/src/Containers/Waiting/CustomWaitStrategy.cs similarity index 93% rename from src/src/WslContainers/Waiting/CustomWaitStrategy.cs rename to src/src/Containers/Waiting/CustomWaitStrategy.cs index 3479195..65078e4 100644 --- a/src/src/WslContainers/Waiting/CustomWaitStrategy.cs +++ b/src/src/Containers/Waiting/CustomWaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Waits until a custom predicate returns true. public sealed class CustomWaitStrategy : WaitStrategy diff --git a/src/src/WslContainers/Waiting/HttpWaitStrategy.cs b/src/src/Containers/Waiting/HttpWaitStrategy.cs similarity index 98% rename from src/src/WslContainers/Waiting/HttpWaitStrategy.cs rename to src/src/Containers/Waiting/HttpWaitStrategy.cs index 17ca9f0..8da012f 100644 --- a/src/src/WslContainers/Waiting/HttpWaitStrategy.cs +++ b/src/src/Containers/Waiting/HttpWaitStrategy.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Waits until an HTTP(S) request against the mapped host port succeeds. [SuppressMessage( diff --git a/src/src/WslContainers/Waiting/IWaitStrategy.cs b/src/src/Containers/Waiting/IWaitStrategy.cs similarity index 94% rename from src/src/WslContainers/Waiting/IWaitStrategy.cs rename to src/src/Containers/Waiting/IWaitStrategy.cs index eb638b8..3a898c8 100644 --- a/src/src/WslContainers/Waiting/IWaitStrategy.cs +++ b/src/src/Containers/Waiting/IWaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// A composable readiness check evaluated until the container is ready or the timeout expires. public interface IWaitStrategy diff --git a/src/src/WslContainers/Waiting/LogMessageWaitStrategy.cs b/src/src/Containers/Waiting/LogMessageWaitStrategy.cs similarity index 95% rename from src/src/WslContainers/Waiting/LogMessageWaitStrategy.cs rename to src/src/Containers/Waiting/LogMessageWaitStrategy.cs index d23acb7..a417ea9 100644 --- a/src/src/WslContainers/Waiting/LogMessageWaitStrategy.cs +++ b/src/src/Containers/Waiting/LogMessageWaitStrategy.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Waits until the container's accumulated init-process output matches a message or regex. public sealed class LogMessageWaitStrategy : WaitStrategy diff --git a/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs b/src/src/Containers/Waiting/TcpPortWaitStrategy.cs similarity index 96% rename from src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs rename to src/src/Containers/Waiting/TcpPortWaitStrategy.cs index f3f2b55..4845977 100644 --- a/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs +++ b/src/src/Containers/Waiting/TcpPortWaitStrategy.cs @@ -1,7 +1,7 @@ using System.Net; using System.Net.Sockets; -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Waits until a TCP connection to the mapped host port succeeds (IPv4 loopback). public sealed class TcpPortWaitStrategy(ushort containerPort, TimeSpan connectTimeout) : WaitStrategy diff --git a/src/src/WslContainers/Waiting/Wait.cs b/src/src/Containers/Waiting/Wait.cs similarity index 98% rename from src/src/WslContainers/Waiting/Wait.cs rename to src/src/Containers/Waiting/Wait.cs index 1dbe2d8..bd571ed 100644 --- a/src/src/WslContainers/Waiting/Wait.cs +++ b/src/src/Containers/Waiting/Wait.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Factory for composable wait strategies. public static class Wait diff --git a/src/src/WslContainers/Waiting/WaitContext.cs b/src/src/Containers/Waiting/WaitContext.cs similarity index 80% rename from src/src/WslContainers/Waiting/WaitContext.cs rename to src/src/Containers/Waiting/WaitContext.cs index abbe0af..bb1efa3 100644 --- a/src/src/WslContainers/Waiting/WaitContext.cs +++ b/src/src/Containers/Waiting/WaitContext.cs @@ -1,11 +1,12 @@ -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Readiness-check context: the container plus resolved runtime state (ports, IP). public sealed class WaitContext { readonly IReadOnlyDictionary _portMappings; - internal WaitContext(IContainer container, IReadOnlyDictionary portMappings, string? networkIp) + /// Creates the readiness context for a started container. + public WaitContext(IContainer container, IReadOnlyDictionary portMappings, string? networkIp) { Container = container; _portMappings = portMappings; diff --git a/src/src/WslContainers/Waiting/WaitStrategy.cs b/src/src/Containers/Waiting/WaitStrategy.cs similarity index 96% rename from src/src/WslContainers/Waiting/WaitStrategy.cs rename to src/src/Containers/Waiting/WaitStrategy.cs index cdadd2a..d407521 100644 --- a/src/src/WslContainers/Waiting/WaitStrategy.cs +++ b/src/src/Containers/Waiting/WaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; /// Base class for wait strategies with shared polling configuration. public abstract class WaitStrategy : IWaitStrategy diff --git a/src/src/WslContainers/Waiting/WaitStrategyRunner.cs b/src/src/Containers/Waiting/WaitStrategyRunner.cs similarity index 85% rename from src/src/WslContainers/Waiting/WaitStrategyRunner.cs rename to src/src/Containers/Waiting/WaitStrategyRunner.cs index 21b44d6..d28e885 100644 --- a/src/src/WslContainers/Waiting/WaitStrategyRunner.cs +++ b/src/src/Containers/Waiting/WaitStrategyRunner.cs @@ -1,18 +1,21 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; +using Purview.Containers.Diagnostics; +using Purview.Containers.Runtime; -namespace Purview.WslContainers.Waiting; +namespace Purview.Containers.Waiting; -/// Runs wait strategies with timeout, interval and retries, producing actionable diagnostics on failure. +/// +/// Runs wait strategies with timeout, interval and retries, producing actionable diagnostics on failure. +/// Public so a container backend can execute the shared readiness contract. +/// [SuppressMessage( "Design", "CA1031:Do not catch general exception types", Justification = "Wait checks observe transient errors; diagnostics collection is best-effort." )] -static class WaitStrategyRunner +public static class WaitStrategyRunner { public static async Task RunAsync( WaitContext context, @@ -21,6 +24,8 @@ public static async Task RunAsync( CancellationToken cancellationToken ) { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(strategies); foreach (var strategy in strategies) { await RunSingleAsync(context, strategy, defaultTimeout, cancellationToken).ConfigureAwait(false); @@ -34,7 +39,7 @@ static async Task RunSingleAsync( CancellationToken cancellationToken ) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.wait"); + using var activity = ContainerActivity.Source.StartActivity("wslcontainer.wait"); activity?.SetTag("container.name", context.Container.Name); activity?.SetTag("strategy", strategy.GetType().Name); var timeout = strategy.Timeout ?? defaultTimeout; @@ -85,7 +90,7 @@ CancellationToken cancellationToken var diagnostics = await BuildDiagnosticsAsync(context, timeout, lastError, cancellationToken) .ConfigureAwait(false); - throw new WslContainerTimeoutException( + throw new ContainerTimeoutException( $"Container '{context.Container.Name}' did not become ready within {timeout} for strategy {strategy.GetType().Name}. {diagnostics}" ); } diff --git a/src/src/Directory.Build.props b/src/src/Directory.Build.props new file mode 100644 index 0000000..d530b3d --- /dev/null +++ b/src/src/Directory.Build.props @@ -0,0 +1,23 @@ + + + + + + net10.0 + + + + + diff --git a/src/src/Docker/Docker.csproj b/src/src/Docker/Docker.csproj new file mode 100644 index 0000000..8fe2837 --- /dev/null +++ b/src/src/Docker/Docker.csproj @@ -0,0 +1,14 @@ + + + true + Docker backend for Purview.Containers: throwaway containers from any reachable Docker daemon, driven by Testcontainers. + docker;containers;testing;integration;testcontainers + MIT + Purview + + + + + + + diff --git a/src/src/Docker/DockerContainer.cs b/src/src/Docker/DockerContainer.cs new file mode 100644 index 0000000..4180551 --- /dev/null +++ b/src/src/Docker/DockerContainer.cs @@ -0,0 +1,218 @@ +using System.Runtime.CompilerServices; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; +using TcContainer = DotNet.Testcontainers.Containers.IContainer; +using TcStates = DotNet.Testcontainers.Containers.TestcontainersStates; + +namespace Purview.Containers.Docker; + +/// +/// A Docker container adapted to the backend-neutral contract. Readiness uses +/// the shared wait strategies (host TCP/HTTP, in-container command, log match), so a module behaves the +/// same here as it does on WSLC. +/// +public sealed class DockerContainer : IContainer +{ + readonly IContainerConfiguration _configuration; + int _disposed; + + internal DockerContainer(IContainerConfiguration configuration, TcContainer container, string name) + { + _configuration = configuration; + Handle = container; + Name = name; + } + + /// The container Testcontainers created for this adapter. + internal TcContainer Handle { get; } + + /// + public string Id => Handle.Id; + + /// + public string Name { get; } + + /// + public ContainerState State => ToState(Handle.State); + + /// + public string Image => _configuration.Image; + + /// + public async Task StartAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + await Handle.StartAsync(cancellationToken).ConfigureAwait(false); + + if (_configuration.WaitStrategies.Count > 0) + { + WaitContext context = new(this, GetMappedPublicPorts(), networkIp: null); + await WaitStrategyRunner + .RunAsync(context, _configuration.WaitStrategies, _configuration.StartupTimeout, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + public Task StopAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + return Handle.StopAsync(cancellationToken); + } + + /// + public async Task ExecAsync( + string[] command, + ExecOptions? options = null, + CancellationToken cancellationToken = default + ) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + ArgumentNullException.ThrowIfNull(command); + + if (options?.EnableStandardInput == true) + { + throw new ContainerNotSupportedException( + "ExecOptions.EnableStandardInput is not supported by the Docker backend." + ); + } + + if (options?.Environment is { Count: > 0 }) + { + throw new ContainerNotSupportedException( + "ExecOptions.Environment is not supported by the Docker backend; set it on the container instead." + ); + } + + using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + if (options?.Timeout is TimeSpan timeout) + { + timeoutSource.CancelAfter(timeout); + } + + // Testcontainers exposes a single argv exec overload, so a requested working directory is applied + // by executing through the shell. + var argv = options?.WorkingDirectory is string workingDirectory + ? new[] { "sh", "-c", $"cd {Quote(workingDirectory)} && {string.Join(' ', command.Select(Quote))}" } + : command; + + var result = await Handle.ExecAsync(argv, timeoutSource.Token).ConfigureAwait(false); + return new ExecResult(result.ExitCode ?? -1, result.Stdout, result.Stderr); + } + + /// + public ushort GetMappedPublicPort(ushort containerPort) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + return Handle.GetMappedPublicPort(containerPort); + } + + /// + public IReadOnlyDictionary GetMappedPublicPorts() + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + Dictionary mappings = []; + foreach (var binding in _configuration.PortBindings) + { + mappings[binding.ContainerPort] = Handle.GetMappedPublicPort(binding.ContainerPort); + } + + return mappings; + } + + /// + public async Task GetLogsAsync(LogOutput? stream = null, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + var (stdout, stderr) = await ReadLogsAsync(cancellationToken).ConfigureAwait(false); + return stream switch + { + LogOutput.Stdout => stdout, + LogOutput.Stderr => stderr, + _ => stdout + stderr, + }; + } + + /// + public async IAsyncEnumerable GetLogsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken + ) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + + // Testcontainers exposes no positioned log stream, so the accumulated output is polled and new lines + // are emitted until the container is no longer running. + var emitted = 0; + while (true) + { + var (stdout, stderr) = await ReadLogsAsync(cancellationToken).ConfigureAwait(false); + var lines = Split(stdout, stderr); + for (; emitted < lines.Count; emitted++) + { + yield return lines[emitted]; + } + + if (State is not ContainerState.Running) + { + yield break; + } + + await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken).ConfigureAwait(false); + } + } + + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + { + return; + } + + await Handle.DisposeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); + } + + Task<(string Stdout, string Stderr)> ReadLogsAsync(CancellationToken cancellationToken) + { + // Docker filters logs by a time window and compares it against UTC log timestamps, so both bounds are + // UTC: the epoch (DateTime.MinValue is not a valid Docker timestamp) and now plus a small margin that + // absorbs second-level truncation. + return Handle.GetLogsAsync( + DateTime.UnixEpoch, + DateTime.UtcNow.AddMinutes(1), + timestampsEnabled: false, + cancellationToken + ); + } + + static List Split(string stdout, string stderr) + { + List entries = []; + Append(entries, stdout, LogOutput.Stdout); + Append(entries, stderr, LogOutput.Stderr); + return entries; + } + + static void Append(List entries, string content, LogOutput stream) + { + foreach (var line in content.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + entries.Add(new ContainerLogEntry(DateTimeOffset.Now, stream, line.TrimEnd('\r'))); + } + } + + static string Quote(string value) => + value.Contains('\'', StringComparison.Ordinal) + ? $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\"" + : $"'{value}'"; + + static ContainerState ToState(TcStates state) => + state switch + { + TcStates.Created => ContainerState.Created, + TcStates.Running or TcStates.Paused or TcStates.Restarting => ContainerState.Running, + TcStates.Exited or TcStates.Dead => ContainerState.Exited, + _ => ContainerState.Invalid, + }; +} diff --git a/src/src/Docker/DockerContainerBackend.cs b/src/src/Docker/DockerContainerBackend.cs new file mode 100644 index 0000000..54d12f9 --- /dev/null +++ b/src/src/Docker/DockerContainerBackend.cs @@ -0,0 +1,123 @@ +using DotNet.Testcontainers.Configurations; +using TcContainerBuilder = DotNet.Testcontainers.Builders.ContainerBuilder; +using TcPullPolicy = DotNet.Testcontainers.Images.PullPolicy; + +namespace Purview.Containers.Docker; + +/// +/// The Docker backend: containers on any Docker daemon reachable from the test host — Docker Desktop, +/// Docker Engine inside WSL2, a remote daemon, or the daemon a CI runner provides. It drives the daemon +/// through Testcontainers, so the resource reaper (Ryuk) still cleans up after a crashed test host. +/// +public sealed class DockerContainerBackend : IContainerBackend +{ + /// Stable backend identifier. + public string Name => "docker"; + + /// Factory used by the generated backend registration. + public static DockerContainerBackend Create() => new(); + + /// + public IContainer CreateContainer(IContainerConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + var container = CreateBuilder(configuration).Build(); + return new DockerContainer(configuration, container, ContainerName.Generate(configuration)); + } + + /// + public async Task GetInfoAsync(CancellationToken cancellationToken = default) + { + try + { + // Resolving the endpoint throws when no Docker environment can be discovered, and the ping + // throws when the daemon is not running; both are reported as "unavailable" with the reason. + var endpoint = TestcontainersSettings.OS.DockerEndpointAuthConfig; + using var client = endpoint.GetDockerClientBuilder(Guid.NewGuid()).Build(); + await client.System.PingAsync(cancellationToken).ConfigureAwait(false); + var info = await client.System.GetSystemInfoAsync(cancellationToken).ConfigureAwait(false); + return new ContainerBackendInfo( + Name, + IsAvailable: true, + IsCompatible: true, + info.ServerVersion ?? string.Empty, + [] + ); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + return new ContainerBackendInfo( + Name, + IsAvailable: false, + IsCompatible: false, + Version: string.Empty, + MissingComponents: [$"{exception.GetType().Name}: {exception.Message}"] + ); + } + } + + static TcContainerBuilder CreateBuilder(IContainerConfiguration configuration) + { + var builder = new TcContainerBuilder(configuration.Image) + .WithAutoRemove(configuration.EnableAutoRemove) + .WithImagePullPolicy( + configuration.PullPolicy switch + { + Images.PullPolicy.Always => TcPullPolicy.Always, + Images.PullPolicy.Never => TcPullPolicy.Never, + _ => TcPullPolicy.Missing, + } + ); + + if (!string.IsNullOrWhiteSpace(configuration.Name)) + { + builder = builder.WithName(configuration.Name); + } + + if (!string.IsNullOrWhiteSpace(configuration.Hostname)) + { + builder = builder.WithHostname(configuration.Hostname); + } + + if (!string.IsNullOrWhiteSpace(configuration.WorkingDirectory)) + { + builder = builder.WithWorkingDirectory(configuration.WorkingDirectory); + } + + if (configuration.Command.Count > 0) + { + builder = builder.WithCommand([.. configuration.Command]); + } + + if (configuration.Environment.Count > 0) + { + builder = builder.WithEnvironment(configuration.Environment); + } + + if (configuration.Privileged) + { + builder = builder.WithPrivileged(true); + } + + foreach (var binding in configuration.PortBindings) + { + builder = binding.HostPort is ushort hostPort + ? builder.WithPortBinding(hostPort, binding.ContainerPort) + : builder.WithPortBinding(binding.ContainerPort, assignRandomHostPort: true); + } + + foreach (var mount in configuration.BindMounts) + { + builder = builder.WithBindMount(mount.HostPath, mount.ContainerPath, ToAccessMode(mount.ReadOnly)); + } + + foreach (var volume in configuration.NamedVolumes) + { + builder = builder.WithVolumeMount(volume.Name, volume.ContainerPath, ToAccessMode(volume.ReadOnly)); + } + + return builder; + } + + static AccessMode ToAccessMode(bool readOnly) => readOnly ? AccessMode.ReadOnly : AccessMode.ReadWrite; +} diff --git a/src/src/Docker/Sdk/README.md b/src/src/Docker/Sdk/README.md new file mode 100644 index 0000000..56a7080 --- /dev/null +++ b/src/src/Docker/Sdk/README.md @@ -0,0 +1,56 @@ +# Purview.Containers.Docker + +The **Docker backend** for [`Purview.Containers`](https://www.nuget.org/packages/Purview.Containers): +throwaway containers for integration testing on any Docker daemon reachable from the test host — Docker +Desktop, Docker Engine inside WSL2, a remote daemon, Docker-in-Docker, or the daemon a CI runner +provides. It drives the daemon through [Testcontainers for .NET](https://dotnet.testcontainers.org/). + +```bash +dotnet add package Purview.Containers.Docker +``` + +```csharp +await using var container = new ContainerBuilder() + .WithImage("alpine:3.19") + .WithCommand("/bin/sh", "-c", "echo ready && sleep 30") + .WithWaitStrategy(Wait.ForLogMessage("ready")) + .Build(); + +await container.StartAsync(); +var result = await container.ExecAsync(["/bin/echo", "hello"]); +``` + +Referencing this package makes `docker` selectable; the module packages need no Docker-specific variant, +because a module reaches the backend through the shared abstractions: + +```bash +PURVIEW_CONTAINERS_BACKEND=docker # or "auto" (the default) to probe WSLC first, then Docker +``` + +| Rule | How | +| --- | --- | +| Pinned instance | `ContainerBackends.Use(new DockerContainerBackend())`, or `WithBackend(...)` | +| Named backend | `PURVIEW_CONTAINERS_BACKEND=docker` | +| Automatic detection | probe every registered backend; WSLC is preferred when it is usable | + +## Requirements + +- **.NET 10 or later**, any platform (this package is portable). +- A reachable Docker daemon. `DockerContainerBackend.GetInfoAsync()` pings it and reports the server + version, or the reason it could not be reached, so a missing daemon is a clear message and not a + timeout deep inside a test. +- The daemon must accept the standard Testcontainers configuration (`DOCKER_HOST`, Docker contexts, + `~/.testcontainers.properties`). The resource reaper (Ryuk) is used, so a crashed test host does not + leak containers. + +## Behaviour notes + +- Readiness uses the same shared wait strategies as every other backend (host TCP/HTTP, an in-container + command, or a log match), so module behaviour does not change with the backend. +- `ExecAsync` supports `WorkingDirectory` and `Timeout`; `Environment` and `EnableStandardInput` throw + `ContainerNotSupportedException` rather than being silently ignored. +- Log tailing polls the accumulated output until the container stops, because the Testcontainers API + exposes no positioned log stream. + +> **Experimental.** The public API, defaults and packaging rules can change between prereleases, and +> there is no production support guarantee. Pin the exact package version you build against. diff --git a/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props b/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props new file mode 100644 index 0000000..c17c2f9 --- /dev/null +++ b/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props @@ -0,0 +1,15 @@ + + + + + $(PurviewContainersBackends);Purview.Containers.Docker.DockerContainerBackend + + diff --git a/src/src/MsSql/MsSql.csproj b/src/src/MsSql/MsSql.csproj index fbbc163..dbe5f2c 100644 --- a/src/src/MsSql/MsSql.csproj +++ b/src/src/MsSql/MsSql.csproj @@ -1,14 +1,14 @@ true - Microsoft SQL Server module for Purview.WslContainers: throwaway SQL Server databases on WSL Containers. + Microsoft SQL Server module for Purview.Containers: throwaway SQL Server databases on WSL Containers. wsl;containers;sqlserver;sql;mssql;testing;integration MIT Purview - + diff --git a/src/src/MsSql/MsSqlBuilder.cs b/src/src/MsSql/MsSqlBuilder.cs index 1431fb2..dd8a622 100644 --- a/src/src/MsSql/MsSqlBuilder.cs +++ b/src/src/MsSql/MsSqlBuilder.cs @@ -1,9 +1,9 @@ using Microsoft.Data.SqlClient; -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Diagnostics; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.MsSql; +namespace Purview.Containers.MsSql; /// Fluent builder for a Microsoft SQL Server test container. public class MsSqlBuilder : ContainerBuilder @@ -28,8 +28,8 @@ public MsSqlBuilder(string image) } /// Creates a builder using an explicit runtime. - public MsSqlBuilder(IContainerRuntime runtime) - : base(runtime) + public MsSqlBuilder(IContainerBackend backend) + : base(backend) { WithImage(MsSqlImage).WithPortBinding(MsSqlPort, assignRandomHostPort: true); } @@ -87,23 +87,21 @@ protected override void Validate(ContainerConfiguration configuration) base.Validate(configuration); if (!_acceptLicense) { - throw new WslContainerConfigurationException( + throw new ContainerConfigurationException( "The SQL Server image requires accepting the EULA. Call AcceptLicense() explicitly before Build()." ); } if (_password.Value.Length < 8) { - throw new WslContainerConfigurationException( - "The SQL Server SA password must be at least 8 characters long." - ); + throw new ContainerConfigurationException("The SQL Server SA password must be at least 8 characters long."); } } /// protected override MsSqlContainer CreateContainer(MsSqlConfiguration configuration) { - return new MsSqlContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new MsSqlContainer(configuration, Backend); } WaitStrategy BuildReadinessWait() diff --git a/src/src/MsSql/MsSqlConfiguration.cs b/src/src/MsSql/MsSqlConfiguration.cs index de960c4..b19a121 100644 --- a/src/src/MsSql/MsSqlConfiguration.cs +++ b/src/src/MsSql/MsSqlConfiguration.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Diagnostics; +using Purview.Containers.Diagnostics; -namespace Purview.WslContainers.MsSql; +namespace Purview.Containers.MsSql; /// Immutable configuration for a Microsoft SQL Server test container. public sealed record MsSqlConfiguration : ContainerConfiguration diff --git a/src/src/MsSql/MsSqlContainer.cs b/src/src/MsSql/MsSqlContainer.cs index eacce1e..cd75be8 100644 --- a/src/src/MsSql/MsSqlContainer.cs +++ b/src/src/MsSql/MsSqlContainer.cs @@ -1,14 +1,14 @@ using Microsoft.Data.SqlClient; -namespace Purview.WslContainers.MsSql; +namespace Purview.Containers.MsSql; /// A throwaway Microsoft SQL Server instance running on WSL Containers. -public sealed class MsSqlContainer : WslContainer +public sealed class MsSqlContainer : ContainerBase { readonly MsSqlConfiguration _configuration; - internal MsSqlContainer(MsSqlConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) + internal MsSqlContainer(MsSqlConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { _configuration = configuration; } diff --git a/src/src/MsSql/Sdk/README.md b/src/src/MsSql/Sdk/README.md index e69334e..f233742 100644 --- a/src/src/MsSql/Sdk/README.md +++ b/src/src/MsSql/Sdk/README.md @@ -1,22 +1,24 @@ -# Purview.WslContainers.MsSql +# Purview.Containers.MsSql -Throwaway Microsoft SQL Server databases for .NET integration testing, running as WSLC containers on -**Microsoft WSL Containers** — no Docker installation. +Throwaway Microsoft SQL Server databases for .NET integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.MsSql +dotnet add package Purview.Containers.MsSql ``` -Depends on `Purview.WslContainers` (the core runtime) and brings `Microsoft.Data.SqlClient` for +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Microsoft.Data.SqlClient` for connection-string generation and readiness probing. ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. @@ -24,7 +26,7 @@ connection-string generation and readiness probing. ```csharp using Microsoft.Data.SqlClient; -using Purview.WslContainers.MsSql; +using Purview.Containers.MsSql; await using var sqlServer = new MsSqlBuilder() .WithPassword("SomeStrong!Password1") @@ -52,7 +54,7 @@ await connection.OpenAsync(); - **Memory:** SQL Server refuses to start below 2000 MB. The default session VM is capped at 4096 MB (`WslContainerRuntimeOptions.Default`), so it works out of the box; override with `MemorySizeInMB` if you configure your own runtime. -- **Licence:** `Build()` throws `WslContainerConfigurationException` unless `AcceptLicense()` was called. +- **Licence:** `Build()` throws `ContainerConfigurationException` unless `AcceptLicense()` was called. The SA password must be at least 8 characters. - **Readiness:** a listening TCP port is not enough — the image reports readiness before it can serve queries, so the default strategy opens a host-side `SqlClient` connection every 2 seconds. The @@ -61,5 +63,6 @@ await connection.OpenAsync(); ## Documentation +- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. - [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. - [Getting Started](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md) — prerequisites and first-container walkthrough. diff --git a/src/src/MySql/MySql.csproj b/src/src/MySql/MySql.csproj index b9e3992..4d15704 100644 --- a/src/src/MySql/MySql.csproj +++ b/src/src/MySql/MySql.csproj @@ -1,14 +1,14 @@ true - MySQL module for Purview.WslContainers: throwaway MySQL databases on WSL Containers. + MySQL module for Purview.Containers: throwaway MySQL databases on WSL Containers. wsl;containers;mysql;database;testing;integration MIT Purview - + diff --git a/src/src/MySql/MySqlBuilder.cs b/src/src/MySql/MySqlBuilder.cs index 908cd6e..41b9793 100644 --- a/src/src/MySql/MySqlBuilder.cs +++ b/src/src/MySql/MySqlBuilder.cs @@ -1,9 +1,9 @@ using MySqlConnector; -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Diagnostics; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.MySql; +namespace Purview.Containers.MySql; /// Fluent builder for a MySQL test container. public class MySqlBuilder : ContainerBuilder @@ -30,8 +30,8 @@ public MySqlBuilder(string image) } /// Creates a builder using an explicit runtime. - public MySqlBuilder(IContainerRuntime runtime) - : base(runtime) + public MySqlBuilder(IContainerBackend backend) + : base(backend) { WithImage(MySqlImage).WithPortBinding(MySqlPort, assignRandomHostPort: true); } @@ -101,19 +101,19 @@ protected override void Validate(ContainerConfiguration configuration) base.Validate(configuration); if (string.IsNullOrWhiteSpace(_database)) { - throw new WslContainerConfigurationException("MySQL database cannot be empty."); + throw new ContainerConfigurationException("MySQL database cannot be empty."); } if (string.IsNullOrEmpty(_password.Value)) { - throw new WslContainerConfigurationException("MySQL password cannot be empty."); + throw new ContainerConfigurationException("MySQL password cannot be empty."); } } /// protected override MySqlContainer CreateContainer(MySqlConfiguration configuration) { - return new MySqlContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new MySqlContainer(configuration, Backend); } WaitStrategy BuildReadinessWait() diff --git a/src/src/MySql/MySqlConfiguration.cs b/src/src/MySql/MySqlConfiguration.cs index 299841f..15a1a2a 100644 --- a/src/src/MySql/MySqlConfiguration.cs +++ b/src/src/MySql/MySqlConfiguration.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Diagnostics; +using Purview.Containers.Diagnostics; -namespace Purview.WslContainers.MySql; +namespace Purview.Containers.MySql; /// Immutable configuration for a MySQL test container. public sealed record MySqlConfiguration : ContainerConfiguration diff --git a/src/src/MySql/MySqlContainer.cs b/src/src/MySql/MySqlContainer.cs index 8d78fb5..ab85b0c 100644 --- a/src/src/MySql/MySqlContainer.cs +++ b/src/src/MySql/MySqlContainer.cs @@ -1,14 +1,14 @@ using MySqlConnector; -namespace Purview.WslContainers.MySql; +namespace Purview.Containers.MySql; /// A throwaway MySQL database running on WSL Containers. -public sealed class MySqlContainer : WslContainer +public sealed class MySqlContainer : ContainerBase { readonly MySqlConfiguration _configuration; - internal MySqlContainer(MySqlConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) + internal MySqlContainer(MySqlConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { _configuration = configuration; } diff --git a/src/src/MySql/Sdk/README.md b/src/src/MySql/Sdk/README.md index 2ff2c0d..668e47e 100644 --- a/src/src/MySql/Sdk/README.md +++ b/src/src/MySql/Sdk/README.md @@ -1,22 +1,24 @@ -# Purview.WslContainers.MySql +# Purview.Containers.MySql -Throwaway MySQL databases for .NET integration testing, running as WSLC containers on -**Microsoft WSL Containers** — no Docker installation. +Throwaway MySQL databases for .NET integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.MySql +dotnet add package Purview.Containers.MySql ``` -Depends on `Purview.WslContainers` (the core runtime) and brings `MySqlConnector` for readiness probing and +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `MySqlConnector` for readiness probing and connection-string generation. ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. @@ -24,7 +26,7 @@ connection-string generation. ```csharp using MySqlConnector; -using Purview.WslContainers.MySql; +using Purview.Containers.MySql; await using var mysql = new MySqlBuilder() .WithDatabase("tests") @@ -50,7 +52,7 @@ await connection.OpenAsync(); | `WithRootPassword(string)` | Sets `MYSQL_ROOT_PASSWORD` (default `test`); stored as a redacted `Secret`. | | `MySqlContainer.GetConnectionString()` | `MySqlConnectionStringBuilder` connection string for the mapped host port. | -`Build()` rejects an empty database or password with `WslContainerConfigurationException`. +`Build()` rejects an empty database or password with `ContainerConfigurationException`. ## Readiness @@ -61,5 +63,6 @@ strategy with `WithWaitStrategy(...)` if you need different behaviour. ## Documentation +- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. - [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. - [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Nats/Nats.csproj b/src/src/Nats/Nats.csproj index d2bf9b3..e1ac2ac 100644 --- a/src/src/Nats/Nats.csproj +++ b/src/src/Nats/Nats.csproj @@ -1,13 +1,13 @@ true - NATS module for Purview.WslContainers: throwaway NATS message brokers on WSL Containers. + NATS module for Purview.Containers: throwaway NATS message brokers on WSL Containers. wsl;containers;nats;messaging;testing;integration MIT Purview - + diff --git a/src/src/Nats/NatsBuilder.cs b/src/src/Nats/NatsBuilder.cs index 52d5872..8fdbafd 100644 --- a/src/src/Nats/NatsBuilder.cs +++ b/src/src/Nats/NatsBuilder.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.Nats; +namespace Purview.Containers.Nats; /// Fluent builder for a NATS test container. public class NatsBuilder : ContainerBuilder @@ -27,8 +27,8 @@ public NatsBuilder(string image) } /// Creates a builder using an explicit runtime. - public NatsBuilder(IContainerRuntime runtime) - : base(runtime) + public NatsBuilder(IContainerBackend backend) + : base(backend) { WithImage(NatsImage) .WithPortBinding(ClientPort, assignRandomHostPort: true) @@ -52,6 +52,6 @@ protected override NatsConfiguration BuildConfiguration() /// protected override NatsContainer CreateContainer(NatsConfiguration configuration) { - return new NatsContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new NatsContainer(configuration, Backend); } } diff --git a/src/src/Nats/NatsConfiguration.cs b/src/src/Nats/NatsConfiguration.cs index b887630..58f21d4 100644 --- a/src/src/Nats/NatsConfiguration.cs +++ b/src/src/Nats/NatsConfiguration.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Nats; +namespace Purview.Containers.Nats; /// Immutable configuration for a NATS test container. public sealed record NatsConfiguration : ContainerConfiguration { } diff --git a/src/src/Nats/NatsContainer.cs b/src/src/Nats/NatsContainer.cs index 14e806b..62c2e30 100644 --- a/src/src/Nats/NatsContainer.cs +++ b/src/src/Nats/NatsContainer.cs @@ -1,10 +1,10 @@ -namespace Purview.WslContainers.Nats; +namespace Purview.Containers.Nats; /// A throwaway NATS broker running on WSL Containers. -public sealed class NatsContainer : WslContainer +public sealed class NatsContainer : ContainerBase { - internal NatsContainer(NatsConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) { } + internal NatsContainer(NatsConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { } /// Client endpoint (nats://127.0.0.1:{port}). Safe to call after . public Uri GetClientEndpoint() diff --git a/src/src/Nats/Sdk/README.md b/src/src/Nats/Sdk/README.md index 8c43cfd..e3b295e 100644 --- a/src/src/Nats/Sdk/README.md +++ b/src/src/Nats/Sdk/README.md @@ -1,29 +1,31 @@ -# Purview.WslContainers.Nats +# Purview.Containers.Nats -Throwaway NATS brokers for .NET integration testing, running as WSLC containers on -**Microsoft WSL Containers** — no Docker installation. +Throwaway NATS brokers for .NET integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.Nats +dotnet add package Purview.Containers.Nats ``` -Depends on `Purview.WslContainers` (the core runtime). `NATS.Client.Core` is not referenced by this +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `NATS.Client.Core` is not referenced by this package — bring your own client. ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. ## Quick start ```csharp -using Purview.WslContainers.Nats; +using Purview.Containers.Nats; using NATS.Client.Core; await using var nats = new NatsBuilder().Build(); @@ -50,5 +52,6 @@ with `WithWaitStrategy(...)`. Endpoint accessors resolve the mapped host ports, ## Documentation +- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. - [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. - [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/PostgreSql/PostgreSql.csproj b/src/src/PostgreSql/PostgreSql.csproj index 4f2480c..0def7a7 100644 --- a/src/src/PostgreSql/PostgreSql.csproj +++ b/src/src/PostgreSql/PostgreSql.csproj @@ -1,14 +1,14 @@ true - PostgreSQL module for Purview.WslContainers: throwaway PostgreSQL databases on WSL Containers. + PostgreSQL module for Purview.Containers: throwaway PostgreSQL databases on WSL Containers. wsl;containers;postgresql;postgres;testing;integration MIT Purview - + diff --git a/src/src/PostgreSql/PostgreSqlBuilder.cs b/src/src/PostgreSql/PostgreSqlBuilder.cs index 0a6ca1a..cfc50f3 100644 --- a/src/src/PostgreSql/PostgreSqlBuilder.cs +++ b/src/src/PostgreSql/PostgreSqlBuilder.cs @@ -1,8 +1,8 @@ -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Diagnostics; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.PostgreSql; +namespace Purview.Containers.PostgreSql; /// Fluent builder for a PostgreSQL test container. public class PostgreSqlBuilder : ContainerBuilder @@ -28,8 +28,8 @@ public PostgreSqlBuilder(string image) } /// Creates a builder using an explicit runtime. - public PostgreSqlBuilder(IContainerRuntime runtime) - : base(runtime) + public PostgreSqlBuilder(IContainerBackend backend) + : base(backend) { WithImage(PostgreSqlImage).WithPortBinding(PostgreSqlPort, assignRandomHostPort: true); } @@ -91,23 +91,23 @@ protected override void Validate(ContainerConfiguration configuration) base.Validate(configuration); if (string.IsNullOrWhiteSpace(_database)) { - throw new WslContainerConfigurationException("PostgreSQL database cannot be empty."); + throw new ContainerConfigurationException("PostgreSQL database cannot be empty."); } if (string.IsNullOrWhiteSpace(_username)) { - throw new WslContainerConfigurationException("PostgreSQL username cannot be empty."); + throw new ContainerConfigurationException("PostgreSQL username cannot be empty."); } if (string.IsNullOrEmpty(_password.Value)) { - throw new WslContainerConfigurationException("PostgreSQL password cannot be empty."); + throw new ContainerConfigurationException("PostgreSQL password cannot be empty."); } } /// protected override PostgreSqlContainer CreateContainer(PostgreSqlConfiguration configuration) { - return new PostgreSqlContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new PostgreSqlContainer(configuration, Backend); } } diff --git a/src/src/PostgreSql/PostgreSqlConfiguration.cs b/src/src/PostgreSql/PostgreSqlConfiguration.cs index a24f585..0e43f11 100644 --- a/src/src/PostgreSql/PostgreSqlConfiguration.cs +++ b/src/src/PostgreSql/PostgreSqlConfiguration.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Diagnostics; +using Purview.Containers.Diagnostics; -namespace Purview.WslContainers.PostgreSql; +namespace Purview.Containers.PostgreSql; /// Immutable configuration for a PostgreSQL test container. public sealed record PostgreSqlConfiguration : ContainerConfiguration diff --git a/src/src/PostgreSql/PostgreSqlContainer.cs b/src/src/PostgreSql/PostgreSqlContainer.cs index 77a7a0d..62d160e 100644 --- a/src/src/PostgreSql/PostgreSqlContainer.cs +++ b/src/src/PostgreSql/PostgreSqlContainer.cs @@ -1,14 +1,14 @@ using Npgsql; -namespace Purview.WslContainers.PostgreSql; +namespace Purview.Containers.PostgreSql; /// A throwaway PostgreSQL database running on WSL Containers. -public sealed class PostgreSqlContainer : WslContainer +public sealed class PostgreSqlContainer : ContainerBase { readonly PostgreSqlConfiguration _configuration; - internal PostgreSqlContainer(PostgreSqlConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) + internal PostgreSqlContainer(PostgreSqlConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { _configuration = configuration; } diff --git a/src/src/PostgreSql/Sdk/README.md b/src/src/PostgreSql/Sdk/README.md index 874d0d5..e9ca121 100644 --- a/src/src/PostgreSql/Sdk/README.md +++ b/src/src/PostgreSql/Sdk/README.md @@ -1,22 +1,24 @@ -# Purview.WslContainers.PostgreSql +# Purview.Containers.PostgreSql -Throwaway PostgreSQL databases for .NET integration testing, running as WSLC containers on -**Microsoft WSL Containers** — no Docker installation. +Throwaway PostgreSQL databases for .NET integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.PostgreSql +dotnet add package Purview.Containers.PostgreSql ``` -Depends on `Purview.WslContainers` (the core runtime) and brings `Npgsql` for connection-string generation. +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Npgsql` for connection-string generation. See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md). ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. @@ -24,7 +26,7 @@ See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/bl ```csharp using Npgsql; -using Purview.WslContainers.PostgreSql; +using Purview.Containers.PostgreSql; await using var postgres = new PostgreSqlBuilder() .WithDatabase("tests") @@ -49,11 +51,12 @@ await connection.OpenAsync(); | `WithPassword(string)` | Sets `POSTGRES_PASSWORD` (default `postgres`); stored as a redacted `Secret`. | | `PostgreSqlContainer.GetConnectionString()` | `NpgsqlConnectionStringBuilder` connection string for the mapped host port. | -`Build()` rejects an empty database, username or password with `WslContainerConfigurationException`. The +`Build()` rejects an empty database, username or password with `ContainerConfigurationException`. The default wait strategy runs `pg_isready -U {username} -d {database}` inside the container unless you supply your own with `WithWaitStrategy(...)`. Call `GetConnectionString()` after `StartAsync()`. ## Documentation +- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. - [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. - [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/RabbitMq/RabbitMq.csproj b/src/src/RabbitMq/RabbitMq.csproj index e18f387..1e9ba0f 100644 --- a/src/src/RabbitMq/RabbitMq.csproj +++ b/src/src/RabbitMq/RabbitMq.csproj @@ -1,13 +1,13 @@ true - RabbitMQ module for Purview.WslContainers: throwaway RabbitMQ brokers on WSL Containers. + RabbitMQ module for Purview.Containers: throwaway RabbitMQ brokers on WSL Containers. wsl;containers;rabbitmq;amqp;testing;integration MIT Purview - + diff --git a/src/src/RabbitMq/RabbitMqBuilder.cs b/src/src/RabbitMq/RabbitMqBuilder.cs index 8d257aa..507ea65 100644 --- a/src/src/RabbitMq/RabbitMqBuilder.cs +++ b/src/src/RabbitMq/RabbitMqBuilder.cs @@ -1,8 +1,8 @@ -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Diagnostics; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.RabbitMq; +namespace Purview.Containers.RabbitMq; /// Fluent builder for a RabbitMQ test container. public class RabbitMqBuilder : ContainerBuilder @@ -33,8 +33,8 @@ public RabbitMqBuilder(string image) } /// Creates a builder using an explicit runtime. - public RabbitMqBuilder(IContainerRuntime runtime) - : base(runtime) + public RabbitMqBuilder(IContainerBackend backend) + : base(backend) { WithImage(RabbitMqImage) .WithPortBinding(AmqpPort, assignRandomHostPort: true) @@ -98,23 +98,23 @@ protected override void Validate(ContainerConfiguration configuration) base.Validate(configuration); if (string.IsNullOrWhiteSpace(_username)) { - throw new WslContainerConfigurationException("RabbitMQ username cannot be empty."); + throw new ContainerConfigurationException("RabbitMQ username cannot be empty."); } if (string.IsNullOrEmpty(_password.Value)) { - throw new WslContainerConfigurationException("RabbitMQ password cannot be empty."); + throw new ContainerConfigurationException("RabbitMQ password cannot be empty."); } if (string.IsNullOrEmpty(_virtualHost)) { - throw new WslContainerConfigurationException("RabbitMQ virtual host cannot be empty."); + throw new ContainerConfigurationException("RabbitMQ virtual host cannot be empty."); } } /// protected override RabbitMqContainer CreateContainer(RabbitMqConfiguration configuration) { - return new RabbitMqContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new RabbitMqContainer(configuration, Backend); } } diff --git a/src/src/RabbitMq/RabbitMqConfiguration.cs b/src/src/RabbitMq/RabbitMqConfiguration.cs index 7df0cf8..f8011ff 100644 --- a/src/src/RabbitMq/RabbitMqConfiguration.cs +++ b/src/src/RabbitMq/RabbitMqConfiguration.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Diagnostics; +using Purview.Containers.Diagnostics; -namespace Purview.WslContainers.RabbitMq; +namespace Purview.Containers.RabbitMq; /// Immutable configuration for a RabbitMQ test container. public sealed record RabbitMqConfiguration : ContainerConfiguration diff --git a/src/src/RabbitMq/RabbitMqContainer.cs b/src/src/RabbitMq/RabbitMqContainer.cs index 78b8283..9bec76f 100644 --- a/src/src/RabbitMq/RabbitMqContainer.cs +++ b/src/src/RabbitMq/RabbitMqContainer.cs @@ -1,12 +1,12 @@ -namespace Purview.WslContainers.RabbitMq; +namespace Purview.Containers.RabbitMq; /// A throwaway RabbitMQ broker running on WSL Containers. -public sealed class RabbitMqContainer : WslContainer +public sealed class RabbitMqContainer : ContainerBase { readonly RabbitMqConfiguration _configuration; - internal RabbitMqContainer(RabbitMqConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) + internal RabbitMqContainer(RabbitMqConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { _configuration = configuration; } diff --git a/src/src/RabbitMq/Sdk/README.md b/src/src/RabbitMq/Sdk/README.md index de3ea32..358db1d 100644 --- a/src/src/RabbitMq/Sdk/README.md +++ b/src/src/RabbitMq/Sdk/README.md @@ -1,29 +1,31 @@ -# Purview.WslContainers.RabbitMq +# Purview.Containers.RabbitMq -Throwaway RabbitMQ brokers for .NET integration testing, running as WSLC containers on -**Microsoft WSL Containers** — no Docker installation. +Throwaway RabbitMQ brokers for .NET integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.RabbitMq +dotnet add package Purview.Containers.RabbitMq ``` -Depends on `Purview.WslContainers` (the core runtime). `RabbitMQ.Client` is not referenced by this package — +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `RabbitMQ.Client` is not referenced by this package — bring your own client. ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. ## Quick start ```csharp -using Purview.WslContainers.RabbitMq; +using Purview.Containers.RabbitMq; using RabbitMQ.Client; await using var rabbitMq = new RabbitMqBuilder() @@ -50,7 +52,7 @@ await using var connection = await factory.CreateConnectionAsync(); | `RabbitMqContainer.GetConnectionString()` | The same AMQP endpoint as a string. | | `RabbitMqContainer.GetManagementEndpoint()` | Management web UI endpoint (`http://localhost:{mappedPort}`). | -`Build()` rejects an empty username, password or virtual host with `WslContainerConfigurationException`. +`Build()` rejects an empty username, password or virtual host with `ContainerConfigurationException`. Call the endpoint accessors after `StartAsync()`. ## Readiness @@ -62,5 +64,6 @@ startup failure (`eacces` reading `.erlang.cookie`). See ## Documentation +- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. - [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. - [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Redis/Redis.csproj b/src/src/Redis/Redis.csproj index 0ce04f1..b953118 100644 --- a/src/src/Redis/Redis.csproj +++ b/src/src/Redis/Redis.csproj @@ -1,13 +1,13 @@ true - Redis module for Purview.WslContainers: throwaway Redis instances on WSL Containers. + Redis module for Purview.Containers: throwaway Redis instances on WSL Containers. wsl;containers;redis;testing;integration MIT Purview - + diff --git a/src/src/Redis/RedisBuilder.cs b/src/src/Redis/RedisBuilder.cs index 3260466..4ea7d5f 100644 --- a/src/src/Redis/RedisBuilder.cs +++ b/src/src/Redis/RedisBuilder.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.Redis; +namespace Purview.Containers.Redis; /// /// Fluent builder for a Redis-compatible test container. Works with Redis and Redis-compatible images @@ -25,8 +25,8 @@ public RedisBuilder(string image) } /// Creates a builder using an explicit runtime. - public RedisBuilder(IContainerRuntime runtime) - : base(runtime) + public RedisBuilder(IContainerBackend backend) + : base(backend) { WithImage(RedisImage).WithPortBinding(RedisPort, assignRandomHostPort: true); } @@ -48,6 +48,6 @@ protected override RedisConfiguration BuildConfiguration() /// protected override RedisContainer CreateContainer(RedisConfiguration configuration) { - return new RedisContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new RedisContainer(configuration, Backend); } } diff --git a/src/src/Redis/RedisConfiguration.cs b/src/src/Redis/RedisConfiguration.cs index 40fdf75..dfe4c16 100644 --- a/src/src/Redis/RedisConfiguration.cs +++ b/src/src/Redis/RedisConfiguration.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Redis; +namespace Purview.Containers.Redis; /// Immutable configuration for a Redis-compatible test container. public sealed record RedisConfiguration : ContainerConfiguration { } diff --git a/src/src/Redis/RedisContainer.cs b/src/src/Redis/RedisContainer.cs index f9ff66d..5997c64 100644 --- a/src/src/Redis/RedisContainer.cs +++ b/src/src/Redis/RedisContainer.cs @@ -1,10 +1,10 @@ -namespace Purview.WslContainers.Redis; +namespace Purview.Containers.Redis; /// A throwaway Redis-compatible instance running on WSL Containers. -public sealed class RedisContainer : WslContainer +public sealed class RedisContainer : ContainerBase { - internal RedisContainer(RedisConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) { } + internal RedisContainer(RedisConfiguration configuration, IContainerBackend? backend) + : base(configuration, backend) { } /// Connection string pointing at the mapped host port. Safe to call after . public string GetConnectionString() diff --git a/src/src/Redis/Sdk/README.md b/src/src/Redis/Sdk/README.md index 6eacbff..40f0e87 100644 --- a/src/src/Redis/Sdk/README.md +++ b/src/src/Redis/Sdk/README.md @@ -1,29 +1,31 @@ -# Purview.WslContainers.Redis +# Purview.Containers.Redis -Throwaway Redis-compatible instances for .NET integration testing, running as WSLC containers on -**Microsoft WSL Containers** — no Docker installation. +Throwaway Redis-compatible instances for .NET integration testing on **WSL Containers (WSLC)** or **Docker**. ```bash -dotnet add package Purview.WslContainers.Redis +dotnet add package Purview.Containers.Redis ``` -Depends on `Purview.WslContainers` (the core runtime). `StackExchange.Redis` is not referenced by this +Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `StackExchange.Redis` is not referenced by this package — bring your own client. ## Requirements -- Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`). -- **A .NET 11 project targeting Windows specifically** (`net11.0-windows10.0.19041.0`, x64 or arm64). - `Purview.WslContainers` supplies `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget`, and rejects an unsupported consumer with `PWC0001`/`PWC0002` — see the +- **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a + consuming project that is a .NET 11 project targeting Windows specifically + (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and + supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an + unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). +- **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any + platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no production support guarantee. ## Quick start ```csharp -using Purview.WslContainers.Redis; +using Purview.Containers.Redis; using StackExchange.Redis; await using var redis = new RedisBuilder().Build(); @@ -57,5 +59,6 @@ await using var garnet = new RedisBuilder("ghcr.io/microsoft/garnet:latest").Bui ## Documentation +- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. - [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. - [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Wsl/Diagnostics/WslContainerActivity.cs b/src/src/Wsl/Diagnostics/WslContainerActivity.cs new file mode 100644 index 0000000..4909782 --- /dev/null +++ b/src/src/Wsl/Diagnostics/WslContainerActivity.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace Purview.Containers.Wsl.Diagnostics; + +/// Activity source for container operations. Listener names: Purview.Containers. +static class WslContainerActivity +{ + /// Activities: session.start, image.pull, container.create/start/stop/delete, exec.create, wait. + public static readonly ActivitySource Source = new("Purview.Containers"); +} diff --git a/src/src/WslContainers/IContainerRuntime.cs b/src/src/Wsl/IContainerRuntime.cs similarity index 93% rename from src/src/WslContainers/IContainerRuntime.cs rename to src/src/Wsl/IContainerRuntime.cs index 3383028..5fbbb7a 100644 --- a/src/src/WslContainers/IContainerRuntime.cs +++ b/src/src/Wsl/IContainerRuntime.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// The process-wide WSL Containers runtime. Owns the shared WSLC session. public interface IContainerRuntime : IAsyncDisposable diff --git a/src/src/WslContainers/IContainerSession.cs b/src/src/Wsl/IContainerSession.cs similarity index 92% rename from src/src/WslContainers/IContainerSession.cs rename to src/src/Wsl/IContainerSession.cs index b59cc70..8b43394 100644 --- a/src/src/WslContainers/IContainerSession.cs +++ b/src/src/Wsl/IContainerSession.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Images; +using Purview.Containers.Images; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// A WSLC session: the shared host for images and containers. public interface IContainerSession : IAsyncDisposable diff --git a/src/src/WslContainers/LogBuffer.cs b/src/src/Wsl/LogBuffer.cs similarity index 97% rename from src/src/WslContainers/LogBuffer.cs rename to src/src/Wsl/LogBuffer.cs index ded2dbd..3248de8 100644 --- a/src/src/WslContainers/LogBuffer.cs +++ b/src/src/Wsl/LogBuffer.cs @@ -1,9 +1,8 @@ using System.Text; using System.Threading.Channels; using Microsoft.WSL.Containers; -using Purview.WslContainers.Containers; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// /// Bounded buffer for init-process output. Accumulates decoded lines for diagnostics while publishing diff --git a/src/src/WslContainers/Sdk/README.md b/src/src/Wsl/Sdk/README.md similarity index 79% rename from src/src/WslContainers/Sdk/README.md rename to src/src/Wsl/Sdk/README.md index a078c38..db820ce 100644 --- a/src/src/WslContainers/Sdk/README.md +++ b/src/src/Wsl/Sdk/README.md @@ -1,19 +1,28 @@ -# Purview.WslContainers +# Purview.Containers.Wsl -WSLC-native throwaway Linux containers for .NET integration testing — a Testcontainers-style library built -directly on the `Microsoft.WSL.Containers` managed API, with **no Docker installation** and no -`wslc.exe`/`wsl.exe`/`docker` CLI, Docker.DotNet or Testcontainers dependency. +The **WSL Containers (WSLC) backend** for [`Purview.Containers`](https://www.nuget.org/packages/Purview.Containers): +throwaway Linux containers for .NET integration testing built directly on the `Microsoft.WSL.Containers` +managed API, with **no Docker installation** and no `wslc.exe`/`wsl.exe`/`docker` CLI, Docker.DotNet or +Testcontainers dependency. ```bash -dotnet add package Purview.WslContainers +dotnet add package Purview.Containers.Wsl ``` +Reference this package (or a service module) and every container the abstractions create runs on WSLC; it +registers itself as the `wsl` backend in the consuming assembly. + +> **Running in CI, or on a machine without WSL Containers?** The same tests run on Docker through +> [`Purview.Containers.Docker`](https://www.nuget.org/packages/Purview.Containers.Docker). Select a backend +> with `PURVIEW_CONTAINERS_BACKEND=wsl|docker` or `ContainerBackends.Use(...)` — see +> [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md). + ## Requirements - Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`), verified against WSL 3.0.1.0. - **A .NET 11 project targeting Windows specifically** — `net11.0-windows10.0.19041.0`, x64 or arm64. - A consuming project that targets anything else fails the build with `PWC0001` (target framework) or - `PWC0002` (platform), and without the packages' MSBuild defaults a stale + A consuming project that targets anything else fails the build with `PCC0001` (target framework) or + `PCC0002` (platform), and without the packages' MSBuild defaults a stale `WindowsSdkPackageVersion` fails with `CS1705`. - This package supplies the `buildTransitive` defaults for `WindowsSdkPackageVersion` and `PlatformTarget` that every module package inherits, so a consumer usually only chooses a target @@ -28,8 +37,8 @@ dotnet add package Purview.WslContainers ## Quick start ```csharp -using Purview.WslContainers; -using Purview.WslContainers.Waiting; +using Purview.Containers.Wsl; +using Purview.Containers.Waiting; await using var container = new ContainerBuilder() .WithImage("docker.io/library/redis:latest") @@ -80,7 +89,7 @@ invalid images, ports and mounts fail during configuration rather than at pull t | `Wait.ForAll(...)` / `Wait.ForAny(...)` | All / any contained strategy succeeds. | Every strategy supports `.WithTimeout(...)`, `.WithInterval(...)` and `.WithRetries(...)`. The default -timeout is the container's `StartupTimeout` (5 minutes). Timeouts throw `WslContainerTimeoutException` with +timeout is the container's `StartupTimeout` (5 minutes). Timeouts throw `ContainerTimeoutException` with a diagnostic naming the container, image, state, mapped ports, strategy and a bounded, secret-redacted log tail. ## Runtime, sessions and storage @@ -91,14 +100,14 @@ a diagnostic naming the container, image, state, mapped ports, strategy and a bo A session exclusively locks its `storage.vhdx`; when a concurrent process holds the default shared store the runtime verifies the store once and transparently falls back to an isolated per-process store (removed when that session terminates). Configure `WslContainerRuntimeOptions` for CPU, memory, GPU, session name, -`StoragePath` (or the `WSL_CONTAINERS_STORAGE_PATH` environment variable) and `StorageMode.PerSession`. +`StoragePath` (or the `PURVIEW_CONTAINERS_STORAGE_PATH` environment variable) and `StorageMode.PerSession`. The Microsoft types (`Session`, `Container`, `Process`, …) stay behind the public interfaces; the only escape hatch is the opt-in accessor for `Inspect()` and raw handles. ## Diagnostics -`System.Diagnostics.ActivitySource("Purview.WslContainers")` emits session, pull, container lifecycle, exec +`System.Diagnostics.ActivitySource("Purview.Containers")` emits session, pull, container lifecycle, exec and wait spans. Credentials and other sensitive values are wrapped in `Secret` and redacted from `ToString()` and diagnostics. @@ -110,5 +119,5 @@ See the [project wiki](https://github.com/purview-dev/wsl-containers/blob/main/d [Lifecycle](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Lifecycle.md), [Networking](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Networking.md) and [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md). -Ready-made service modules ship as `Purview.WslContainers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, +Ready-made service modules ship as `Purview.Containers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats` and `MySql`. diff --git a/src/src/WslContainers/Sdk/buildTransitive/Purview.WslContainers.props b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props similarity index 62% rename from src/src/WslContainers/Sdk/buildTransitive/Purview.WslContainers.props rename to src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props index d758601..e0f58b3 100644 --- a/src/src/WslContainers/Sdk/buildTransitive/Purview.WslContainers.props +++ b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props @@ -1,8 +1,8 @@ 10.0.26100.80 + + + $(PurviewContainersBackends);Purview.Containers.Wsl.WslContainerBackend diff --git a/src/src/WslContainers/Sdk/buildTransitive/Purview.WslContainers.targets b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets similarity index 58% rename from src/src/WslContainers/Sdk/buildTransitive/Purview.WslContainers.targets rename to src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets index 961b94a..94ee50c 100644 --- a/src/src/WslContainers/Sdk/buildTransitive/Purview.WslContainers.targets +++ b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets @@ -1,6 +1,6 @@ @@ -33,41 +34,41 @@ TargetPlatformVersion is empty (a platform-less target framework such as net11.0). --> - <_PurviewWslContainersPlatformVersionSupported + <_PurviewContainersWslPlatformVersionSupported Condition="'$(TargetPlatformVersion)' != '' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetPlatformVersion), '10.0.19041.0'))" - >true - <_PurviewWslContainersTargetFrameworkSupported - Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '11.0')) AND '$(TargetPlatformIdentifier)' == 'Windows' AND '$(_PurviewWslContainersPlatformVersionSupported)' == 'true'" - >true + >true + <_PurviewContainersWslTargetFrameworkSupported + Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '11.0')) AND '$(TargetPlatformIdentifier)' == 'Windows' AND '$(_PurviewContainersWslPlatformVersionSupported)' == 'true'" + >true - <_PurviewWslContainersPlatformTargetSupported + <_PurviewContainersWslPlatformTargetSupported Condition="'$(PlatformTarget)' == 'x64' OR '$(PlatformTarget)' == 'arm64' OR ('$(PlatformTarget)' == '' AND '$(RuntimeIdentifier)' != '')" - >true + >true diff --git a/src/src/WslContainers/StorageMode.cs b/src/src/Wsl/StorageMode.cs similarity index 93% rename from src/src/WslContainers/StorageMode.cs rename to src/src/Wsl/StorageMode.cs index 9df4d97..1338ead 100644 --- a/src/src/WslContainers/StorageMode.cs +++ b/src/src/Wsl/StorageMode.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// How the session storage path (and therefore the image store) is scoped. public enum StorageMode diff --git a/src/src/Wsl/Wsl.csproj b/src/src/Wsl/Wsl.csproj new file mode 100644 index 0000000..1d7cc17 --- /dev/null +++ b/src/src/Wsl/Wsl.csproj @@ -0,0 +1,21 @@ + + + true + + net11.0-windows10.0.19041.0 + x64 + 10.0.26100.80 + WSL Containers backend for Purview.Containers: WSLC-native throwaway Linux containers for .NET integration testing. + wsl;containers;linux;windows;testing;integration + MIT + Purview + + + + + + + diff --git a/src/src/WslContainers/WslContainer.cs b/src/src/Wsl/WslContainer.cs similarity index 96% rename from src/src/WslContainers/WslContainer.cs rename to src/src/Wsl/WslContainer.cs index bd51988..a0137c7 100644 --- a/src/src/WslContainers/WslContainer.cs +++ b/src/src/Wsl/WslContainer.cs @@ -2,15 +2,14 @@ using System.Runtime.CompilerServices; using System.Text; using Microsoft.WSL.Containers; -using Purview.WslContainers.Containers; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; +using MsContainer = Microsoft.WSL.Containers.Container; using MsContainerState = Microsoft.WSL.Containers.ContainerState; using MsProcess = Microsoft.WSL.Containers.Process; using MsProcessState = Microsoft.WSL.Containers.ProcessState; using MsSignal = Microsoft.WSL.Containers.Signal; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// A WSLC-backed throwaway container. [SuppressMessage( @@ -22,7 +21,7 @@ public class WslContainer : IContainer { readonly LogBuffer _logBuffer = new(); WslContainerSession? _session; - Container? _handle; + MsContainer? _handle; IReadOnlyDictionary _portMappings = new Dictionary(); string? _networkIp; int _started; @@ -51,7 +50,7 @@ public WslContainer(ContainerConfiguration configuration, IContainerRuntime runt public string Id => _handle?.Id ?? string.Empty; /// - public Containers.ContainerState State { get; private set; } + public ContainerState State { get; private set; } /// public string Image => Configuration.Image; @@ -347,9 +346,9 @@ void EnsureStarted() } } - static Containers.ContainerState MapState(MsContainerState state) + static ContainerState MapState(MsContainerState state) { - return (Containers.ContainerState)state; + return (ContainerState)state; } static string GenerateName(ContainerConfiguration configuration) diff --git a/src/src/Wsl/WslContainerBackend.cs b/src/src/Wsl/WslContainerBackend.cs new file mode 100644 index 0000000..2c627c2 --- /dev/null +++ b/src/src/Wsl/WslContainerBackend.cs @@ -0,0 +1,68 @@ +using Purview.Containers.Runtime; + +namespace Purview.Containers.Wsl; + +/// +/// The WSL Containers (WSLC) backend: it creates containers on the process-wide WSLC session and reports +/// the installed WSL Containers components. No Docker installation is involved. +/// +public sealed class WslContainerBackend : IContainerBackend +{ + readonly IContainerRuntime? _runtime; + + /// Creates the backend over the process-wide . + public WslContainerBackend() { } + + /// Creates the backend over an explicit runtime (isolation experiments and tests). + public WslContainerBackend(IContainerRuntime runtime) + { + ArgumentNullException.ThrowIfNull(runtime); + _runtime = runtime; + } + + /// Stable backend identifier. + public string Name => "wsl"; + + /// Factory used by the generated backend registration. + public static WslContainerBackend Create() => new(); + + IContainerRuntime Runtime => _runtime ?? WslContainerRuntime.Instance; + + /// + public IContainer CreateContainer(IContainerConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + if (configuration is not ContainerConfiguration wslc) + { + throw new ContainerConfigurationException( + "The WSL Containers backend requires a ContainerConfiguration-derived configuration; " + + $"{configuration.GetType().Name} is not supported." + ); + } + + foreach (var binding in configuration.PortBindings) + { + if (binding.Protocol == Networking.PortProtocol.Udp) + { + throw new ContainerNotSupportedException( + "UDP port mappings are not supported by the WSL Containers managed API." + ); + } + } + + return new WslContainer(wslc, Runtime); + } + + /// + public async Task GetInfoAsync(CancellationToken cancellationToken = default) + { + var info = await Runtime.GetInfoAsync(cancellationToken).ConfigureAwait(false); + return new ContainerBackendInfo( + "wsl", + info.IsAvailable, + info.IsCompatible, + info.Version, + info.MissingComponents + ); + } +} diff --git a/src/src/Wsl/WslContainerException.cs b/src/src/Wsl/WslContainerException.cs new file mode 100644 index 0000000..d38e698 --- /dev/null +++ b/src/src/Wsl/WslContainerException.cs @@ -0,0 +1,25 @@ +using Purview.Containers.Runtime; + +namespace Purview.Containers.Wsl; + +#pragma warning disable CA1032 // Implement standard exception constructors + +/// Base exception for errors raised by the WSL Containers (WSLC) backend. +public class WslContainerException : ContainerException +{ + public WslContainerException(string message) + : base(message) { } + + public WslContainerException(string message, Exception innerException) + : base(message, innerException) { } +} + +/// WSL or WSL Containers prerequisites are missing. +public class WslContainerPrerequisiteException(string message) : ContainerPrerequisiteException(message) { } + +/// Starting the container failed inside WSLC (image, OCI runtime, or environment). +public class WslContainerStartupException(string message, Exception innerException) + : ContainerStartupException(message, innerException) { } + +/// The backing WSLC session terminated unexpectedly. +public class WslContainerSessionTerminatedException(string message) : WslContainerException(message) { } diff --git a/src/src/WslContainers/WslContainerRuntime.cs b/src/src/Wsl/WslContainerRuntime.cs similarity index 86% rename from src/src/WslContainers/WslContainerRuntime.cs rename to src/src/Wsl/WslContainerRuntime.cs index 0415045..25f6450 100644 --- a/src/src/WslContainers/WslContainerRuntime.cs +++ b/src/src/Wsl/WslContainerRuntime.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.WSL.Containers; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// /// The process-wide WSL Containers runtime. Owns a single lazily started WSLC session. @@ -18,6 +18,15 @@ public sealed class WslContainerRuntime : IContainerRuntime { const int ErrorSharingViolation = unchecked((int)0x80070020); + /// + /// Environment variable that overrides the session storage path for every runtime that does not set + /// . + /// + public const string StoragePathEnvironmentVariable = "PURVIEW_CONTAINERS_STORAGE_PATH"; + + /// The pre-rename storage path variable, still honoured as a fallback. + public const string LegacyStoragePathEnvironmentVariable = "WSL_CONTAINERS_STORAGE_PATH"; + static readonly Lazy InstanceHolder = new(() => new WslContainerRuntime()); readonly WslContainerRuntimeOptions _options; @@ -180,7 +189,7 @@ string ResolveStoragePath(string sessionName) return _options.StoragePath; } - var envPath = Environment.GetEnvironmentVariable("WSL_CONTAINERS_STORAGE_PATH"); + var envPath = StoragePathFromEnvironment(); if (!string.IsNullOrEmpty(envPath)) { return envPath; @@ -198,10 +207,22 @@ string ResolveStoragePath(string sessionName) bool IsUsingDefaultSharedStore() { return _options.StoragePath is null - && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_CONTAINERS_STORAGE_PATH")) + && string.IsNullOrEmpty(StoragePathFromEnvironment()) && _options.StorageMode == StorageMode.Shared; } + /// + /// The storage path configured through the environment: + /// first, then the pre-rename . + /// + static string? StoragePathFromEnvironment() + { + var configured = Environment.GetEnvironmentVariable(StoragePathEnvironmentVariable); + return string.IsNullOrEmpty(configured) + ? Environment.GetEnvironmentVariable(LegacyStoragePathEnvironmentVariable) + : configured; + } + static string NextSessionName() { return $"wslc-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..8]}"; diff --git a/src/src/WslContainers/WslContainerRuntimeInfo.cs b/src/src/Wsl/WslContainerRuntimeInfo.cs similarity index 86% rename from src/src/WslContainers/WslContainerRuntimeInfo.cs rename to src/src/Wsl/WslContainerRuntimeInfo.cs index aa76ac1..a68ad6a 100644 --- a/src/src/WslContainers/WslContainerRuntimeInfo.cs +++ b/src/src/Wsl/WslContainerRuntimeInfo.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// Information about the installed WSL Containers runtime. public sealed record WslContainerRuntimeInfo( diff --git a/src/src/WslContainers/WslContainerRuntimeOptions.cs b/src/src/Wsl/WslContainerRuntimeOptions.cs similarity index 83% rename from src/src/WslContainers/WslContainerRuntimeOptions.cs rename to src/src/Wsl/WslContainerRuntimeOptions.cs index 896f76b..7f7c562 100644 --- a/src/src/WslContainers/WslContainerRuntimeOptions.cs +++ b/src/src/Wsl/WslContainerRuntimeOptions.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// Configuration for the WSLC session owned by a runtime. public sealed record WslContainerRuntimeOptions @@ -20,8 +20,10 @@ public sealed record WslContainerRuntimeOptions /// /// Storage path for the session VHD and image store. When unset and is /// , the default shared image directory is used - /// (%LOCALAPPDATA%\Purview\WslContainers\images). The WSL_CONTAINERS_STORAGE_PATH - /// environment variable overrides the default when this is unset. + /// (%LOCALAPPDATA%\Purview\WslContainers\images). The + /// environment variable overrides the + /// default when this is unset; the pre-rename WSL_CONTAINERS_STORAGE_PATH is still honoured as a + /// fallback. /// public string? StoragePath { get; init; } diff --git a/src/src/WslContainers/WslContainerSession.cs b/src/src/Wsl/WslContainerSession.cs similarity index 89% rename from src/src/WslContainers/WslContainerSession.cs rename to src/src/Wsl/WslContainerSession.cs index 64bcbef..4138570 100644 --- a/src/src/WslContainers/WslContainerSession.cs +++ b/src/src/Wsl/WslContainerSession.cs @@ -1,13 +1,13 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using Microsoft.WSL.Containers; -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Images; -using Purview.WslContainers.Runtime; +using Purview.Containers.Images; +using Purview.Containers.Wsl.Diagnostics; using Windows.Foundation; +using MsContainer = Microsoft.WSL.Containers.Container; using Process = Microsoft.WSL.Containers.Process; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [SuppressMessage( "Design", @@ -52,7 +52,7 @@ public WslContainerSession( public async Task EnsureStartedAsync(CancellationToken cancellationToken) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.session.start"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.session.start"); activity?.SetTag("session.name", Name); ThrowIfTerminated(); if (Volatile.Read(ref _started) == 1) @@ -201,9 +201,12 @@ internal void DisposeSession() _lifecycleGate.Dispose(); } - internal async Task CreateContainerAsync(ContainerSettings settings, CancellationToken cancellationToken) + internal async Task CreateContainerAsync( + ContainerSettings settings, + CancellationToken cancellationToken + ) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.create"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.container.create"); activity?.SetTag("container.name", settings.Name); await EnsureStartedAsync(cancellationToken).ConfigureAwait(false); await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -218,9 +221,9 @@ internal async Task CreateContainerAsync(ContainerSettings settings, } } - internal async Task StartContainerAsync(Container container, CancellationToken cancellationToken) + internal async Task StartContainerAsync(MsContainer container, CancellationToken cancellationToken) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.start"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.container.start"); activity?.SetTag("container.id", container.Id); await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -235,13 +238,13 @@ internal async Task StartContainerAsync(Container container, CancellationToken c } internal async Task StopContainerAsync( - Container container, + MsContainer container, Signal signal, TimeSpan timeout, CancellationToken cancellationToken ) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.stop"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.container.stop"); activity?.SetTag("container.id", container.Id); await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -256,12 +259,12 @@ CancellationToken cancellationToken } internal async Task DeleteContainerAsync( - Container container, + MsContainer container, DeleteContainerOption option, CancellationToken cancellationToken ) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.delete"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.container.delete"); activity?.SetTag("container.id", container.Id); await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -276,12 +279,12 @@ CancellationToken cancellationToken } internal async Task CreateProcessAsync( - Container container, + MsContainer container, ProcessSettings settings, CancellationToken cancellationToken ) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.exec.create"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.exec.create"); activity?.SetTag("container.id", container.Id); await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try @@ -354,7 +357,7 @@ async Task PullCoreAsync( CancellationToken cancellationToken ) { - using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.image.pull"); + using var activity = WslContainerActivity.Source.StartActivity("wslcontainer.image.pull"); activity?.SetTag("image", reference.FullReference); PullImageOptions options = new(reference.FullReference); if (credentials is not null) diff --git a/src/src/WslContainers/WslInspectParser.cs b/src/src/Wsl/WslInspectParser.cs similarity index 98% rename from src/src/WslContainers/WslInspectParser.cs rename to src/src/Wsl/WslInspectParser.cs index dc460bf..21b1504 100644 --- a/src/src/WslContainers/WslInspectParser.cs +++ b/src/src/Wsl/WslInspectParser.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// Parses the Docker-compatible JSON returned by Container.Inspect(). static class WslInspectParser diff --git a/src/src/WslContainers/WslSettingsMapper.cs b/src/src/Wsl/WslSettingsMapper.cs similarity index 95% rename from src/src/WslContainers/WslSettingsMapper.cs rename to src/src/Wsl/WslSettingsMapper.cs index cd14133..9cc595e 100644 --- a/src/src/WslContainers/WslSettingsMapper.cs +++ b/src/src/Wsl/WslSettingsMapper.cs @@ -1,9 +1,9 @@ using Microsoft.WSL.Containers; -using Purview.WslContainers.Mounts; -using Purview.WslContainers.Networking; +using Purview.Containers.Mounts; +using Purview.Containers.Networking; using Windows.Networking; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// Maps the public configuration model onto the WSLC settings types. static class WslSettingsMapper diff --git a/src/src/WslContainers/Diagnostics/WslContainersActivity.cs b/src/src/WslContainers/Diagnostics/WslContainersActivity.cs deleted file mode 100644 index 66deb2e..0000000 --- a/src/src/WslContainers/Diagnostics/WslContainersActivity.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics; - -namespace Purview.WslContainers.Diagnostics; - -/// Activity source for WSL Containers operations. Listener names: Purview.WslContainers. -static class WslContainersActivity -{ - /// Activities: session.start, image.pull, container.create/start/stop/delete, exec.create, wait. - public static readonly ActivitySource Source = new("Purview.WslContainers"); -} diff --git a/src/src/WslContainers/Runtime/WslContainerException.cs b/src/src/WslContainers/Runtime/WslContainerException.cs deleted file mode 100644 index 344d5de..0000000 --- a/src/src/WslContainers/Runtime/WslContainerException.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Purview.WslContainers.Runtime; - -#pragma warning disable CA1032 // Implement standard exception constructors - -/// Base exception for all errors raised by the WSL Containers runtime. -public class WslContainerException : Exception -{ - public WslContainerException(string message) - : base(message) { } - - public WslContainerException(string message, Exception innerException) - : base(message, innerException) { } -} - -/// Invalid container configuration detected before any WSLC operation. -public class WslContainerConfigurationException : WslContainerException -{ - public WslContainerConfigurationException(string message) - : base(message) { } - - public WslContainerConfigurationException(string message, Exception innerException) - : base(message, innerException) { } -} - -/// WSL/WSL Containers prerequisites are missing or incompatible. -public class WslContainerPrerequisiteException(string message) : WslContainerException(message) { } - -/// A requested feature is not supported by the current WSLC API. -public class WslContainerNotSupportedException(string message) : WslContainerException(message) { } - -/// Starting the container failed (image, OCI runtime, or environment). -public class WslContainerStartupException(string message, Exception innerException) - : WslContainerException(message, innerException) { } - -/// An operation did not complete within its configured timeout. -public class WslContainerTimeoutException(string message) : WslContainerException(message) { } - -/// The backing WSLC session terminated unexpectedly. -public class WslContainerSessionTerminatedException(string message) : WslContainerException(message) { } diff --git a/src/src/WslContainers/WslContainers.csproj b/src/src/WslContainers/WslContainers.csproj deleted file mode 100644 index abeb3e0..0000000 --- a/src/src/WslContainers/WslContainers.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - true - WSLC-native throwaway Linux containers for .NET integration testing on Microsoft WSL Containers. - wsl;containers;linux;windows;testing;integration - MIT - Purview - - - - - - diff --git a/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs b/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs index 1d7365a..c7d3623 100644 --- a/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs +++ b/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers.Azurite; +namespace Purview.Containers.Azurite; public class AzuriteIntegrationTests { diff --git a/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs b/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs index e2600e3..f1e1997 100644 --- a/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs +++ b/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.Azurite; +namespace Purview.Containers.Azurite; public class AzuriteBuilderTests { diff --git a/src/tests/Docker.IntegrationTests/Docker.IntegrationTests.csproj b/src/tests/Docker.IntegrationTests/Docker.IntegrationTests.csproj new file mode 100644 index 0000000..f33052a --- /dev/null +++ b/src/tests/Docker.IntegrationTests/Docker.IntegrationTests.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs b/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs new file mode 100644 index 0000000..fba5272 --- /dev/null +++ b/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs @@ -0,0 +1,60 @@ +namespace Purview.Containers.Docker; + +/// +/// Covers backend selection with a real daemon: the generated registration analogue (explicit +/// registration), a named selection, and a container produced by the generic builder through the +/// registry. The registry is process-wide static state, so this class is not run in parallel. +/// +[NotInParallel] +public class DockerBackendSelectionTests +{ + static Task SkipIfUnavailableAsync() => DockerTest.SkipIfUnavailableAsync(); + + [Test] + public async Task GenericBuilder_WithTheDockerBackendSelected_StartsAContainer() + { + await SkipIfUnavailableAsync(); + ContainerBackends.Reset(); + try + { + ContainerBackends.Register(DockerContainerBackend.Create()); + ContainerBackends.Use(ContainerBackendSelection.Named("docker")); + + await using var container = new ContainerBuilder() + .WithImage("alpine:3.19") + .WithCommand("/bin/echo", "selected") + .Build(); + + await container.StartAsync(); + + var logs = await container.GetLogsAsync(); + + await Assert.That(container.State).IsEqualTo(ContainerState.Running); + await Assert.That(logs).Contains("selected"); + } + finally + { + ContainerBackends.Reset(); + } + } + + [Test] + public async Task ResolveAsync_WithAutomaticDetection_SelectsTheRegisteredDockerBackend() + { + await SkipIfUnavailableAsync(); + ContainerBackends.Reset(); + try + { + var backend = DockerContainerBackend.Create(); + ContainerBackends.Register(backend); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("docker"); + } + finally + { + ContainerBackends.Reset(); + } + } +} diff --git a/src/tests/Docker.IntegrationTests/DockerContainerTests.cs b/src/tests/Docker.IntegrationTests/DockerContainerTests.cs new file mode 100644 index 0000000..5dda8e8 --- /dev/null +++ b/src/tests/Docker.IntegrationTests/DockerContainerTests.cs @@ -0,0 +1,105 @@ +using Purview.Containers.Waiting; + +namespace Purview.Containers.Docker; + +/// +/// End-to-end coverage of the Docker backend against a real daemon. These tests are integration tests, so +/// the shared pipeline (filtered to [Category=Unit]) never runs them; they skip themselves when no +/// Docker daemon is reachable. +/// +public class DockerContainerTests +{ + const string Image = "alpine:3.19"; + + static Task SkipIfUnavailableAsync() => DockerTest.SkipIfUnavailableAsync(); + + static Container Build(ushort? mappedPort = null, IWaitStrategy? wait = null) + { + var builder = new ContainerBuilder().WithImage(Image).WithCommand("/bin/sh", "-c", "echo ready && sleep 60"); + + if (mappedPort is ushort port) + { + builder = builder.WithPortBinding(port, assignRandomHostPort: true); + } + + if (wait is not null) + { + builder = builder.WithWaitStrategy(wait); + } + + return builder.Build(); + } + + [Test] + public async Task GetInfoAsync_ReportsTheDockerServerVersion() + { + await SkipIfUnavailableAsync(); + + var info = await DockerContainerBackend.Create().GetInfoAsync(); + + await Assert.That(info.Name).IsEqualTo("docker"); + await Assert.That(info.IsUsable).IsTrue(); + await Assert.That(info.Version).IsNotEmpty(); + } + + [Test] + public async Task Start_ExecAndLogs_RoundTrip() + { + await SkipIfUnavailableAsync(); + + await using var container = Build(); + await container.StartAsync(); + + await Assert.That(container.State).IsEqualTo(ContainerState.Running); + await Assert.That(container.Id).IsNotEmpty(); + + var executed = await container.ExecAsync(["/bin/echo", "hello-from-docker"]); + await Assert.That(executed.IsSuccess).IsTrue(); + await Assert.That(executed.Stdout).Contains("hello-from-docker"); + + var logs = await container.GetLogsAsync(); + await Assert.That(logs).Contains("ready"); + + await container.StopAsync(); + + await Assert.That(container.State).IsEqualTo(ContainerState.Exited); + } + + [Test] + public async Task Start_MapsRandomHostPorts() + { + await SkipIfUnavailableAsync(); + + await using var container = Build(mappedPort: 8080); + await container.StartAsync(); + + var hostPort = container.GetMappedPublicPort(8080); + + await Assert.That(hostPort).IsGreaterThan((ushort)0); + await Assert.That(container.GetMappedPublicPorts()[8080]).IsEqualTo(hostPort); + } + + [Test] + public async Task Start_HonoursTheSharedWaitStrategy() + { + await SkipIfUnavailableAsync(); + + await using var container = Build(wait: Wait.ForLogMessage("ready")); + await container.StartAsync(); + + await Assert.That(container.State).IsEqualTo(ContainerState.Running); + } + + [Test] + public async Task DisposeAsync_IsIdempotent() + { + await SkipIfUnavailableAsync(); + + var container = Build(); + await container.StartAsync(); + await container.DisposeAsync(); + await container.DisposeAsync(); + + await Assert.That(container.State).IsNotEqualTo(ContainerState.Running); + } +} diff --git a/src/tests/Docker.IntegrationTests/DockerTest.cs b/src/tests/Docker.IntegrationTests/DockerTest.cs new file mode 100644 index 0000000..3181527 --- /dev/null +++ b/src/tests/Docker.IntegrationTests/DockerTest.cs @@ -0,0 +1,33 @@ +using TUnit.Core.Exceptions; + +namespace Purview.Containers.Docker; + +/// +/// Shared setup for the Docker integration tests. A package consumer gets backend registration from the +/// generated module initializer in Purview.Containers.Backends.targets; this project references the +/// backend as a project, so it registers explicitly here. Idempotent. +/// +public static class DockerTest +{ + static int BackendRegistered; + + /// Registers the Docker backend with the shared registry. + public static void EnsureBackendRegistered() + { + if (Interlocked.Exchange(ref BackendRegistered, 1) == 0) + { + ContainerBackends.Register(DockerContainerBackend.Create()); + } + } + + /// Skips the current test when no Docker daemon is reachable. + public static async Task SkipIfUnavailableAsync() + { + EnsureBackendRegistered(); + var info = await DockerContainerBackend.Create().GetInfoAsync(); + if (!info.IsUsable) + { + throw new SkipTestException($"Docker is unavailable: {string.Join("; ", info.MissingComponents)}"); + } + } +} diff --git a/src/tests/Docker.UnitTests/Docker.UnitTests.csproj b/src/tests/Docker.UnitTests/Docker.UnitTests.csproj new file mode 100644 index 0000000..f33052a --- /dev/null +++ b/src/tests/Docker.UnitTests/Docker.UnitTests.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/src/tests/Docker.UnitTests/DockerContainerBackendTests.cs b/src/tests/Docker.UnitTests/DockerContainerBackendTests.cs new file mode 100644 index 0000000..68525ab --- /dev/null +++ b/src/tests/Docker.UnitTests/DockerContainerBackendTests.cs @@ -0,0 +1,42 @@ +namespace Purview.Containers.Docker; + +/// +/// Unit coverage for the Docker backend that needs no daemon: identity, registration metadata and the +/// configuration translation. Anything that talks to a daemon lives in Docker.IntegrationTests. +/// +public class DockerContainerBackendTests +{ + [Test] + public async Task Name_IsDocker() + { + await Assert.That(DockerContainerBackend.Create().Name).IsEqualTo("docker"); + } + + [Test] + public async Task Create_ReturnsANewBackend() + { + await Assert.That(DockerContainerBackend.Create()).IsNotNull(); + } + + [Test] + public async Task CreateContainer_AdaptsTheConfiguredImageAndName() + { + ContainerConfiguration configuration = new() { Image = "alpine:3.19", Name = "purview-unit-test" }; + + var container = new DockerContainerBackend().CreateContainer(configuration); + + await Assert.That(container).IsTypeOf(); + await Assert.That(container.Image).IsEqualTo("alpine:3.19"); + await Assert.That(container.Name).IsEqualTo("purview-unit-test"); + } + + [Test] + public async Task CreateContainer_GeneratesNoName_WhenTheConfigurationDoesNotSetOne() + { + ContainerConfiguration configuration = new() { Image = "alpine:3.19" }; + + var container = new DockerContainerBackend().CreateContainer(configuration); + + await Assert.That(container.Name).IsNotEmpty(); + } +} diff --git a/src/tests/Modules.DockerIntegrationTests/ModuleDockerTests.cs b/src/tests/Modules.DockerIntegrationTests/ModuleDockerTests.cs new file mode 100644 index 0000000..847ef86 --- /dev/null +++ b/src/tests/Modules.DockerIntegrationTests/ModuleDockerTests.cs @@ -0,0 +1,130 @@ +using System.Globalization; +using Purview.Containers.Docker; +using TUnit.Core.Exceptions; + +namespace Purview.Containers.Modules; + +/// +/// Every service module on the Docker backend. The WSLC suites prove the modules against WSL Containers; +/// this suite proves the same module packages produce working containers on Docker, which is what makes a +/// developer machine (WSLC) and a Linux CI runner (Docker) interchangeable. +/// +/// +/// Readiness is the module's own strategy, so a passing test means the service was really up: PostgreSQL +/// waits for pg_isready, Redis for redis-cli ping, SQL Server and MySQL for a real client +/// connection, and RabbitMQ, Azurite and NATS for their startup log lines. Each test skips itself when no +/// Docker daemon is reachable. +/// +public class ModuleDockerTests +{ + static int BackendRegistered; + + static async Task SkipIfUnavailableAsync() + { + if (Interlocked.Exchange(ref BackendRegistered, 1) == 0) + { + ContainerBackends.Register(DockerContainerBackend.Create()); + } + + var info = await DockerContainerBackend.Create().GetInfoAsync(); + if (!info.IsUsable) + { + throw new SkipTestException($"Docker is unavailable: {string.Join("; ", info.MissingComponents)}"); + } + } + + static async Task AssertStartedAsync(IContainer container, ushort port) + { + await container.StartAsync(); + + await Assert.That(container.State).IsEqualTo(ContainerState.Running); + await Assert.That(container.GetMappedPublicPort(port)).IsGreaterThan((ushort)0); + } + + [Test] + public async Task PostgreSql_StartsAndMapsItsPort() + { + await SkipIfUnavailableAsync(); + + await using var postgres = new PostgreSql.PostgreSqlBuilder() + .WithDatabase("docker_tests") + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + + await AssertStartedAsync(postgres, PostgreSql.PostgreSqlBuilder.PostgreSqlPort); + await Assert.That(postgres.GetConnectionString()).Contains("docker_tests"); + } + + [Test] + public async Task Redis_StartsAndMapsItsPort() + { + await SkipIfUnavailableAsync(); + + await using var redis = new Redis.RedisBuilder().Build(); + + await AssertStartedAsync(redis, Redis.RedisBuilder.RedisPort); + + // The connection string points at the random host port, not the container port. + var hostPort = redis.GetMappedPublicPort(Redis.RedisBuilder.RedisPort); + await Assert.That(redis.GetConnectionString()).Contains(hostPort.ToString(CultureInfo.InvariantCulture)); + } + + [Test] + public async Task MsSql_StartsAndMapsItsPort() + { + await SkipIfUnavailableAsync(); + + await using var sqlServer = new MsSql.MsSqlBuilder() + .WithPassword("SomeStrong!Password1") + .AcceptLicense() + .Build(); + + await AssertStartedAsync(sqlServer, MsSql.MsSqlBuilder.MsSqlPort); + await Assert.That(sqlServer.GetConnectionString()).Contains("Password=SomeStrong!Password1"); + } + + [Test] + public async Task MySql_StartsAndMapsItsPort() + { + await SkipIfUnavailableAsync(); + + await using var mySql = new MySql.MySqlBuilder().WithPassword("SomeStrong!Password1").Build(); + + await AssertStartedAsync(mySql, MySql.MySqlBuilder.MySqlPort); + await Assert.That(mySql.GetConnectionString()).IsNotEmpty(); + } + + [Test] + public async Task RabbitMq_StartsAndMapsItsPorts() + { + await SkipIfUnavailableAsync(); + + await using var rabbitMq = new RabbitMq.RabbitMqBuilder().WithPassword("guest").Build(); + + await AssertStartedAsync(rabbitMq, RabbitMq.RabbitMqBuilder.AmqpPort); + await Assert.That(rabbitMq.GetAmqpEndpoint().Port).IsGreaterThan(0); + } + + [Test] + public async Task Azurite_StartsAndMapsItsPort() + { + await SkipIfUnavailableAsync(); + + await using var azurite = new Azurite.AzuriteBuilder().Build(); + + await AssertStartedAsync(azurite, Azurite.AzuriteBuilder.BlobPort); + await Assert.That(azurite.GetBlobEndpoint().Port).IsGreaterThan(0); + } + + [Test] + public async Task Nats_StartsAndMapsItsPort() + { + await SkipIfUnavailableAsync(); + + await using var nats = new Nats.NatsBuilder().Build(); + + await AssertStartedAsync(nats, Nats.NatsBuilder.ClientPort); + await Assert.That(nats.GetClientEndpoint().Port).IsGreaterThan(0); + } +} diff --git a/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj b/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj new file mode 100644 index 0000000..1186d0c --- /dev/null +++ b/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs b/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs index fe21b4f..46a8159 100644 --- a/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs +++ b/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs @@ -1,6 +1,6 @@ using Microsoft.Data.SqlClient; -namespace Purview.WslContainers.MsSql; +namespace Purview.Containers.MsSql; public class MsSqlIntegrationTests { diff --git a/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs index 173a10e..4c21657 100644 --- a/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs +++ b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Runtime; +using Purview.Containers.Runtime; -namespace Purview.WslContainers.MsSql; +namespace Purview.Containers.MsSql; public class MsSqlBuilderTests { @@ -9,7 +9,7 @@ public async Task Build_WithoutAcceptLicense_Throws() { var builder = new MsSqlBuilder().WithPassword("SomeStrong!Password1"); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] @@ -17,7 +17,7 @@ public async Task Build_WithWeakPassword_Throws() { var builder = new MsSqlBuilder().WithPassword("short").AcceptLicense(); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] diff --git a/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs b/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs index a88f51d..9550be4 100644 --- a/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs +++ b/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs @@ -1,6 +1,6 @@ using MySqlConnector; -namespace Purview.WslContainers.MySql; +namespace Purview.Containers.MySql; public class MySqlIntegrationTests { diff --git a/src/tests/MySql.UnitTests/MySqlBuilderTests.cs b/src/tests/MySql.UnitTests/MySqlBuilderTests.cs index 8f3e5f0..f629502 100644 --- a/src/tests/MySql.UnitTests/MySqlBuilderTests.cs +++ b/src/tests/MySql.UnitTests/MySqlBuilderTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Runtime; +using Purview.Containers.Runtime; -namespace Purview.WslContainers.MySql; +namespace Purview.Containers.MySql; public class MySqlBuilderTests { @@ -26,6 +26,6 @@ public async Task BuildConfig_EmptyPassword_Throws() { var builder = new MySqlBuilder().WithPassword(string.Empty); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } } diff --git a/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs b/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs index 3e72d1e..49a2610 100644 --- a/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs +++ b/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs @@ -1,6 +1,6 @@ using NATS.Client.Core; -namespace Purview.WslContainers.Nats; +namespace Purview.Containers.Nats; public class NatsIntegrationTests { diff --git a/src/tests/Nats.UnitTests/NatsBuilderTests.cs b/src/tests/Nats.UnitTests/NatsBuilderTests.cs index 96e4ea1..1e4019f 100644 --- a/src/tests/Nats.UnitTests/NatsBuilderTests.cs +++ b/src/tests/Nats.UnitTests/NatsBuilderTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.Nats; +namespace Purview.Containers.Nats; public class NatsBuilderTests { diff --git a/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs b/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs index ebefacf..81cda55 100644 --- a/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs +++ b/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs @@ -1,7 +1,7 @@ using Npgsql; -using PurviewContainerState = Purview.WslContainers.Containers.ContainerState; +using PurviewContainerState = Purview.Containers.ContainerState; -namespace Purview.WslContainers.PostgreSql; +namespace Purview.Containers.PostgreSql; public class PostgreSqlIntegrationTests { diff --git a/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs b/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs index 12c93dc..7f0039a 100644 --- a/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs +++ b/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs @@ -1,8 +1,8 @@ -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Diagnostics; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.PostgreSql; +namespace Purview.Containers.PostgreSql; public class PostgreSqlBuilderTests { @@ -47,7 +47,7 @@ public async Task BuildConfig_EmptyPassword_Throws() { var builder = new PostgreSqlBuilder().WithPassword(string.Empty); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] diff --git a/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs b/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs index 837944f..e4e3d30 100644 --- a/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs +++ b/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs @@ -1,7 +1,7 @@ using System.Text; using RabbitMQ.Client; -namespace Purview.WslContainers.RabbitMq; +namespace Purview.Containers.RabbitMq; public class RabbitMqIntegrationTests { diff --git a/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs b/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs index a4524d6..af100f8 100644 --- a/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs +++ b/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs @@ -1,7 +1,7 @@ -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.RabbitMq; +namespace Purview.Containers.RabbitMq; public class RabbitMqBuilderTests { @@ -43,6 +43,6 @@ public async Task BuildConfig_EmptyPassword_Throws() { var builder = new RabbitMqBuilder().WithPassword(string.Empty); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } } diff --git a/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs b/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs index 3c47a9d..f322702 100644 --- a/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs +++ b/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs @@ -1,6 +1,6 @@ using StackExchange.Redis; -namespace Purview.WslContainers.Redis; +namespace Purview.Containers.Redis; public class RedisIntegrationTests { diff --git a/src/tests/Redis.UnitTests/RedisBuilderTests.cs b/src/tests/Redis.UnitTests/RedisBuilderTests.cs index 4629e9f..09ac167 100644 --- a/src/tests/Redis.UnitTests/RedisBuilderTests.cs +++ b/src/tests/Redis.UnitTests/RedisBuilderTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers.Redis; +namespace Purview.Containers.Redis; public class RedisBuilderTests { diff --git a/src/tests/SharedTestingFramework/SharedTestingFramework.csproj b/src/tests/SharedTestingFramework/SharedTestingFramework.csproj index 1107f7f..47f0c99 100644 --- a/src/tests/SharedTestingFramework/SharedTestingFramework.csproj +++ b/src/tests/SharedTestingFramework/SharedTestingFramework.csproj @@ -1,5 +1,5 @@ - + diff --git a/src/tests/SharedTestingFramework/WslcTest.cs b/src/tests/SharedTestingFramework/WslcTest.cs index c867ef0..f5c3dcb 100644 --- a/src/tests/SharedTestingFramework/WslcTest.cs +++ b/src/tests/SharedTestingFramework/WslcTest.cs @@ -1,13 +1,30 @@ +using Purview.Containers.Wsl; using TUnit.Core.Exceptions; -namespace Purview.WslContainers; +namespace Purview.Containers; /// Shared helpers for integration tests that exercise real WSL Containers. public static class WslcTest { + static int BackendRegistered; + + /// + /// Registers the WSL Containers backend. Package consumers get this from the generated module + /// initializer in Purview.Containers.Backends.targets; these test projects reference the + /// backend as a project, so they register it explicitly here. Idempotent. + /// + public static void EnsureBackendRegistered() + { + if (Interlocked.Exchange(ref BackendRegistered, 1) == 0) + { + ContainerBackends.Register(WslContainerBackend.Create()); + } + } + /// Skips the current test when WSL Containers prerequisites are missing. public static async Task SkipIfUnavailableAsync() { + EnsureBackendRegistered(); var info = await WslContainerRuntime.Instance.GetInfoAsync(); if (!info.IsAvailable || !info.IsCompatible) { diff --git a/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs b/src/tests/Wsl.IntegrationTests/AlpineLifecycleTests.cs similarity index 96% rename from src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs rename to src/tests/Wsl.IntegrationTests/AlpineLifecycleTests.cs index 5556c5d..3ce61ab 100644 --- a/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs +++ b/src/tests/Wsl.IntegrationTests/AlpineLifecycleTests.cs @@ -1,6 +1,4 @@ -using Purview.WslContainers.Containers; - -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class AlpineLifecycleTests diff --git a/src/tests/WslContainers.IntegrationTests/CleanupTests.cs b/src/tests/Wsl.IntegrationTests/CleanupTests.cs similarity index 88% rename from src/tests/WslContainers.IntegrationTests/CleanupTests.cs rename to src/tests/Wsl.IntegrationTests/CleanupTests.cs index be011ca..79a0230 100644 --- a/src/tests/WslContainers.IntegrationTests/CleanupTests.cs +++ b/src/tests/Wsl.IntegrationTests/CleanupTests.cs @@ -1,7 +1,7 @@ -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class CleanupTests @@ -17,7 +17,7 @@ public async Task Dispose_AfterWaitTimeout_IsIdempotent() .WithWaitStrategy(Wait.ForLogMessage("this never appears").WithTimeout(TimeSpan.FromSeconds(3))) .Build(); - await Assert.That(() => container.StartAsync()).Throws(); + await Assert.That(() => container.StartAsync()).Throws(); await container.DisposeAsync(); await container.DisposeAsync(); } diff --git a/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs b/src/tests/Wsl.IntegrationTests/ConcurrencyStressTests.cs similarity index 89% rename from src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs rename to src/tests/Wsl.IntegrationTests/ConcurrencyStressTests.cs index caf78f4..153e292 100644 --- a/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs +++ b/src/tests/Wsl.IntegrationTests/ConcurrencyStressTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class ConcurrencyStressTests @@ -11,7 +11,7 @@ public async Task ConcurrentContainers_GetDistinctUsablePorts() await WslcTest.SkipIfUnavailableAsync(); const int count = 5; - List containers = [with(count)]; + List containers = [with(count)]; try { for (var i = 0; i < count; i++) diff --git a/src/tests/WslContainers.IntegrationTests/ExecTests.cs b/src/tests/Wsl.IntegrationTests/ExecTests.cs similarity index 96% rename from src/tests/WslContainers.IntegrationTests/ExecTests.cs rename to src/tests/Wsl.IntegrationTests/ExecTests.cs index 405ee13..7e241a8 100644 --- a/src/tests/WslContainers.IntegrationTests/ExecTests.cs +++ b/src/tests/Wsl.IntegrationTests/ExecTests.cs @@ -1,6 +1,4 @@ -using Purview.WslContainers.Containers; - -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class ExecTests diff --git a/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs b/src/tests/Wsl.IntegrationTests/PerformanceTests.cs similarity index 96% rename from src/tests/WslContainers.IntegrationTests/PerformanceTests.cs rename to src/tests/Wsl.IntegrationTests/PerformanceTests.cs index c38891f..74e40ee 100644 --- a/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs +++ b/src/tests/Wsl.IntegrationTests/PerformanceTests.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class PerformanceTests diff --git a/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs b/src/tests/Wsl.IntegrationTests/PortMappingTests.cs similarity index 96% rename from src/tests/WslContainers.IntegrationTests/PortMappingTests.cs rename to src/tests/Wsl.IntegrationTests/PortMappingTests.cs index 6ec39f1..23fb4db 100644 --- a/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs +++ b/src/tests/Wsl.IntegrationTests/PortMappingTests.cs @@ -1,8 +1,7 @@ using System.Net; using System.Net.Sockets; -using Purview.WslContainers.Containers; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class PortMappingTests diff --git a/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs b/src/tests/Wsl.IntegrationTests/RuntimeInfoTests.cs similarity index 91% rename from src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs rename to src/tests/Wsl.IntegrationTests/RuntimeInfoTests.cs index 83f625c..b71a4dc 100644 --- a/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs +++ b/src/tests/Wsl.IntegrationTests/RuntimeInfoTests.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class RuntimeInfoTests diff --git a/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs b/src/tests/Wsl.IntegrationTests/SharedStorageTests.cs similarity index 91% rename from src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs rename to src/tests/Wsl.IntegrationTests/SharedStorageTests.cs index fb5b134..61631a6 100644 --- a/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs +++ b/src/tests/Wsl.IntegrationTests/SharedStorageTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Images; +using Purview.Containers.Images; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class SharedStorageTests @@ -21,7 +21,7 @@ public async Task SharedStore_IsReusedAcrossSequentialRuntimes() ) ) { - await using var container = new ContainerBuilder(runtime1) + await using var container = new ContainerBuilder(new WslContainerBackend(runtime1)) .WithImage("alpine:latest") .WithCommand("/bin/echo", "one") .Build(); diff --git a/src/tests/WslContainers.IntegrationTests/VolumeTests.cs b/src/tests/Wsl.IntegrationTests/VolumeTests.cs similarity index 98% rename from src/tests/WslContainers.IntegrationTests/VolumeTests.cs rename to src/tests/Wsl.IntegrationTests/VolumeTests.cs index d17d656..c392bd1 100644 --- a/src/tests/WslContainers.IntegrationTests/VolumeTests.cs +++ b/src/tests/Wsl.IntegrationTests/VolumeTests.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class VolumeTests diff --git a/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs b/src/tests/Wsl.IntegrationTests/WaitStrategyTests.cs similarity index 92% rename from src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs rename to src/tests/Wsl.IntegrationTests/WaitStrategyTests.cs index c57787a..1f042fc 100644 --- a/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs +++ b/src/tests/Wsl.IntegrationTests/WaitStrategyTests.cs @@ -1,9 +1,8 @@ using System.Net; -using Purview.WslContainers.Containers; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; [Explicit] public class WaitStrategyTests @@ -105,12 +104,12 @@ public async Task Timeout_ThrowsWithDiagnosticsAndIsDisposable() ) .Build(); - WslContainerTimeoutException? thrown = null; + ContainerTimeoutException? thrown = null; try { await container.StartAsync(); } - catch (WslContainerTimeoutException ex) + catch (ContainerTimeoutException ex) { thrown = ex; } diff --git a/src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj b/src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj similarity index 100% rename from src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj rename to src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj diff --git a/src/tests/WslContainers.UnitTests/ConsumerRequirementsTests.cs b/src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs similarity index 97% rename from src/tests/WslContainers.UnitTests/ConsumerRequirementsTests.cs rename to src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs index eb7723a..f5cf10d 100644 --- a/src/tests/WslContainers.UnitTests/ConsumerRequirementsTests.cs +++ b/src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Runtime.Versioning; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; /// /// Guards the consumer contract documented in docs/wiki/Consumer-Requirements.md: every diff --git a/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs b/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs new file mode 100644 index 0000000..32e49b4 --- /dev/null +++ b/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs @@ -0,0 +1,265 @@ +using Purview.Containers.Runtime; + +namespace Purview.Containers.Wsl; + +/// +/// Covers the backend registry, the selection policy and the resolution rules the builders rely on. The +/// registry and the selection are process-wide static state, so the class opts out of parallel execution +/// and every test starts from a clean registry. +/// +[NotInParallel] +public class ContainerBackendsTests +{ + [Test] + public async Task WslBackend_ReportsItsName() + { + await Assert.That(WslContainerBackend.Create().Name).IsEqualTo("wsl"); + } + + [Test] + public async Task WslBackend_CreatesAWslContainerForADerivedConfiguration() + { + var container = new WslContainerBackend().CreateContainer( + new DerivedConfiguration { Image = "alpine:3.19", Extra = "x" } + ); + + await Assert.That(container).IsTypeOf(); + await Assert.That(container.Image).IsEqualTo("alpine:3.19"); + } + + [Test] + public async Task GenericBuilder_Build_DefersTheBackendResolution() + { + using RegistryScope scope = new(); + + // No backend is registered, yet Build() succeeds: the backend is resolved when the container starts. + ContainerBuilder builder = new(); + var container = builder.WithImage("alpine").Build(); + + await Assert.That(container).IsTypeOf(); + await Assert.That(container.Name).StartsWith("alpine-"); + await Assert.That(container.State).IsEqualTo(ContainerState.Created); + } + + [Test] + public async Task ResolveAsync_WithoutARegisteredBackend_ThrowsAnActionableError() + { + using RegistryScope scope = new(); + + var exception = await ThrowsAsync(() => ContainerBackends.ResolveAsync()); + + await Assert.That(exception.Message).Contains("Purview.Containers.Wsl"); + await Assert.That(exception.Message).Contains("Purview.Containers.Docker"); + } + + [Test] + public async Task ResolveAsync_ReturnsTheFirstUsableBackend() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one")); + ContainerBackends.Register(new FakeBackend("two")); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("one"); + } + + [Test] + public async Task ResolveAsync_SkipsAnUnusableBackend() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one", isAvailable: false)); + ContainerBackends.Register(new FakeBackend("two")); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("two"); + } + + [Test] + public async Task ResolveAsync_WithoutAUsableBackend_ReportsEveryBackend() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one", isAvailable: false)); + ContainerBackends.Register(new ThrowingBackend("two")); + + var exception = await ThrowsAsync(() => ContainerBackends.ResolveAsync()); + + await Assert.That(exception.Message).Contains("one: unavailable"); + await Assert.That(exception.Message).Contains("two: unavailable"); + await Assert.That(exception.Message).Contains(ContainerBackends.SelectionEnvironmentVariable); + } + + [Test] + public async Task ResolveAsync_CachesTheSuccessfulResolution() + { + using RegistryScope scope = new(); + FakeBackend backend = new("one"); + ContainerBackends.Register(backend); + + await ContainerBackends.ResolveAsync(); + await ContainerBackends.ResolveAsync(); + + await Assert.That(backend.Probes).IsEqualTo(1); + } + + [Test] + public async Task ResolveAsync_WithANamedBackend_PrefersItOverTheRegisteredOrder() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one")); + ContainerBackends.Register(new FakeBackend("two")); + ContainerBackends.Use(ContainerBackendSelection.Named("two")); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("two"); + } + + [Test] + public async Task ResolveAsync_WithTheEnvironmentVariable_SelectsThatBackend() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one")); + ContainerBackends.Register(new FakeBackend("two")); + using EnvironmentScope environment = new(ContainerBackends.SelectionEnvironmentVariable, "two"); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("two"); + } + + [Test] + public async Task ResolveAsync_WithANamedBackendThatIsNotRegistered_ListsTheRegisteredOnes() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one")); + ContainerBackends.Use(ContainerBackendSelection.Named("docker")); + + var exception = await ThrowsAsync(() => ContainerBackends.ResolveAsync()); + + await Assert.That(exception.Message).Contains("'docker' is not registered"); + await Assert.That(exception.Message).Contains("one"); + } + + [Test] + public async Task ResolveAsync_WithANamedUnusableBackend_DoesNotFallBack() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one")); + ContainerBackends.Register(new FakeBackend("two", isAvailable: false)); + ContainerBackends.Use(ContainerBackendSelection.Named("two")); + + var exception = await ThrowsAsync(() => ContainerBackends.ResolveAsync()); + + await Assert.That(exception.Message).Contains("'two' is not usable"); + } + + [Test] + public async Task Use_PinsABackendInstanceOverEverythingElse() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("registered")); + ContainerBackends.Use(new FakeBackend("pinned")); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("pinned"); + } + + [Test] + public async Task Use_WithAPinnedUnusableBackend_ReportsIt() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("registered")); + ContainerBackends.Use(new FakeBackend("pinned", isCompatible: false)); + + var exception = await ThrowsAsync(() => ContainerBackends.ResolveAsync()); + + await Assert.That(exception.Message).Contains("The pinned container backend 'pinned' is not usable"); + } + + [Test] + public async Task ProbeAllAsync_ReportsEveryRegisteredBackend() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("one")); + ContainerBackends.Register(new FakeBackend("two", isAvailable: false)); + + var probes = await ContainerBackends.ProbeAllAsync(); + + await Assert.That(probes.Count).IsEqualTo(2); + await Assert.That(probes[0].IsUsable).IsTrue(); + await Assert.That(probes[1].IsUsable).IsFalse(); + } + + static async Task ThrowsAsync(Func action) + where TException : Exception + { + var exception = await Assert.ThrowsAsync(action); + ArgumentNullException.ThrowIfNull(exception); + return exception; + } + + sealed record DerivedConfiguration : ContainerConfiguration + { + public string Extra { get; init; } = string.Empty; + } + + sealed class FakeBackend(string name, bool isAvailable = true, bool isCompatible = true) : IContainerBackend + { + public string Name { get; } = name; + + public int Probes { get; private set; } + + public IContainer CreateContainer(IContainerConfiguration configuration) => throw new NotSupportedException(); + + public Task GetInfoAsync(CancellationToken cancellationToken = default) + { + Probes++; + return Task.FromResult( + new ContainerBackendInfo( + Name, + isAvailable, + isCompatible, + "1.0", + isAvailable && isCompatible ? [] : ["missing"] + ) + ); + } + } + + sealed class ThrowingBackend(string name) : IContainerBackend + { + public string Name { get; } = name; + + public IContainer CreateContainer(IContainerConfiguration configuration) => throw new NotSupportedException(); + + public Task GetInfoAsync(CancellationToken cancellationToken = default) => + throw new InvalidOperationException("probe failed"); + } + + /// Clears the backend registry for the duration of a test and resets it on dispose. + sealed class RegistryScope : IDisposable + { + public RegistryScope() => ContainerBackends.Reset(); + + public void Dispose() => ContainerBackends.Reset(); + } + + /// Sets an environment variable for the duration of a test and restores it on dispose. + sealed class EnvironmentScope : IDisposable + { + readonly string _name; + readonly string? _previous; + + public EnvironmentScope(string name, string? value) + { + _name = name; + _previous = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + } + + public void Dispose() => Environment.SetEnvironmentVariable(_name, _previous); + } +} diff --git a/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs b/src/tests/Wsl.UnitTests/ContainerBuilderTests.cs similarity index 72% rename from src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs rename to src/tests/Wsl.UnitTests/ContainerBuilderTests.cs index 2ef605e2..1ea9846 100644 --- a/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs +++ b/src/tests/Wsl.UnitTests/ContainerBuilderTests.cs @@ -1,8 +1,8 @@ -using Purview.WslContainers.Images; -using Purview.WslContainers.Networking; -using Purview.WslContainers.Runtime; +using Purview.Containers.Images; +using Purview.Containers.Networking; +using Purview.Containers.Runtime; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class ContainerBuilderTests { @@ -56,49 +56,57 @@ public async Task BuildConfiguration_PopulatesAllCommonFields() [Test] public async Task Build_WithoutImage_Throws() { - await Assert.That(() => new TestBuilder().Build()).Throws(); + await Assert.That(() => new TestBuilder().Build()).Throws(); } [Test] - public async Task Build_WithUdpPort_ThrowsNotSupported() + public async Task CreateContainer_WithUdpPort_ThrowsNotSupported() { - var builder = new TestBuilder().WithImage("alpine").WithPortBinding(53, 53, PortProtocol.Udp); - await Assert.That(() => builder.Build()).Throws(); + // UDP is a backend capability, so the neutral builder accepts the configuration and the WSL + // Containers backend rejects it (the Docker backend supports UDP). + var configuration = new TestBuilder() + .WithImage("alpine") + .WithPortBinding(53, 53, PortProtocol.Udp) + .BuildConfig(); + + await Assert + .That(() => new WslContainerBackend().CreateContainer(configuration)) + .Throws(); } [Test] public async Task Build_WithContainerPortZero_Throws() { var builder = new TestBuilder().WithImage("alpine").WithPortBinding(0, assignRandomHostPort: true); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithHostPortZero_Throws() { var builder = new TestBuilder().WithImage("alpine").WithPortBinding(80, 0); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithEmptyBindMount_Throws() { var builder = new TestBuilder().WithImage("alpine").WithBindMount(string.Empty, "/data"); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithZeroStartupTimeout_Throws() { var builder = new TestBuilder().WithImage("alpine").WithStartupTimeout(TimeSpan.Zero); - await Assert.That(() => builder.Build()).Throws(); + await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task GenericContainerBuilder_GeneratesUniqueName() { - var first = new ContainerBuilder().WithImage("alpine").Build(); - var second = new ContainerBuilder().WithImage("alpine").Build(); + var first = new ContainerBuilder(new WslContainerBackend()).WithImage("alpine").Build(); + var second = new ContainerBuilder(new WslContainerBackend()).WithImage("alpine").Build(); await Assert.That(first.Name).IsNotEqualTo(second.Name); await Assert.That(first.Image).IsEqualTo("docker.io/library/alpine:latest"); @@ -109,10 +117,10 @@ public async Task WithImage_InvalidReference_ThrowsImmediately() { await Assert .That(() => new ContainerBuilder().WithImage("bad image name")) - .Throws(); + .Throws(); await Assert .That(() => new ContainerBuilder().WithImage(string.Empty)) - .Throws(); + .Throws(); } [Test] @@ -126,7 +134,7 @@ public async Task WithTag_AppliesTagToConfiguredImage() [Test] public async Task WithTag_WithoutImage_Throws() { - await Assert.That(() => new ContainerBuilder().WithTag("7.4")).Throws(); + await Assert.That(() => new ContainerBuilder().WithTag("7.4")).Throws(); } [Test] @@ -134,6 +142,6 @@ public async Task WithTag_InvalidTag_Throws() { await Assert .That(() => new ContainerBuilder().WithImage("redis").WithTag("bad tag")) - .Throws(); + .Throws(); } } diff --git a/src/tests/WslContainers.UnitTests/FakeContainer.cs b/src/tests/Wsl.UnitTests/FakeContainer.cs similarity index 95% rename from src/tests/WslContainers.UnitTests/FakeContainer.cs rename to src/tests/Wsl.UnitTests/FakeContainer.cs index 2651bb7..564731a 100644 --- a/src/tests/WslContainers.UnitTests/FakeContainer.cs +++ b/src/tests/Wsl.UnitTests/FakeContainer.cs @@ -1,6 +1,4 @@ -using Purview.WslContainers.Containers; - -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public sealed class FakeContainer : IContainer { diff --git a/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs b/src/tests/Wsl.UnitTests/HttpWaitStrategyTests.cs similarity index 98% rename from src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs rename to src/tests/Wsl.UnitTests/HttpWaitStrategyTests.cs index 2cd6749..f8aaaa8 100644 --- a/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs +++ b/src/tests/Wsl.UnitTests/HttpWaitStrategyTests.cs @@ -1,9 +1,9 @@ using System.Net; using System.Net.Sockets; using System.Text; -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class HttpWaitStrategyTests { diff --git a/src/tests/WslContainers.UnitTests/ImageTests.cs b/src/tests/Wsl.UnitTests/ImageTests.cs similarity index 97% rename from src/tests/WslContainers.UnitTests/ImageTests.cs rename to src/tests/Wsl.UnitTests/ImageTests.cs index ae97d1a..36eaf5f 100644 --- a/src/tests/WslContainers.UnitTests/ImageTests.cs +++ b/src/tests/Wsl.UnitTests/ImageTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Images; +using Purview.Containers.Images; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class ImageTests { diff --git a/src/tests/WslContainers.UnitTests/SecretTests.cs b/src/tests/Wsl.UnitTests/SecretTests.cs similarity index 88% rename from src/tests/WslContainers.UnitTests/SecretTests.cs rename to src/tests/Wsl.UnitTests/SecretTests.cs index 6d15f67..d92f162 100644 --- a/src/tests/WslContainers.UnitTests/SecretTests.cs +++ b/src/tests/Wsl.UnitTests/SecretTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Diagnostics; +using Purview.Containers.Diagnostics; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class SecretTests { diff --git a/src/tests/WslContainers.UnitTests/StorageModeTests.cs b/src/tests/Wsl.UnitTests/StorageModeTests.cs similarity index 95% rename from src/tests/WslContainers.UnitTests/StorageModeTests.cs rename to src/tests/Wsl.UnitTests/StorageModeTests.cs index f980eb2..14f7330 100644 --- a/src/tests/WslContainers.UnitTests/StorageModeTests.cs +++ b/src/tests/Wsl.UnitTests/StorageModeTests.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class StorageModeTests { diff --git a/src/tests/WslContainers.UnitTests/WaitContextTests.cs b/src/tests/Wsl.UnitTests/WaitContextTests.cs similarity index 94% rename from src/tests/WslContainers.UnitTests/WaitContextTests.cs rename to src/tests/Wsl.UnitTests/WaitContextTests.cs index 9bf29f3..ac8aacc 100644 --- a/src/tests/WslContainers.UnitTests/WaitContextTests.cs +++ b/src/tests/Wsl.UnitTests/WaitContextTests.cs @@ -1,6 +1,6 @@ -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class WaitContextTests { diff --git a/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs b/src/tests/Wsl.UnitTests/WaitStrategyRunnerTests.cs similarity index 91% rename from src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs rename to src/tests/Wsl.UnitTests/WaitStrategyRunnerTests.cs index ad95ae0..32a1601 100644 --- a/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs +++ b/src/tests/Wsl.UnitTests/WaitStrategyRunnerTests.cs @@ -1,7 +1,7 @@ -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; +using Purview.Containers.Runtime; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class WaitStrategyRunnerTests { @@ -38,7 +38,7 @@ public async Task RunAsync_TimesOut_ThrowsWithDiagnostics(CancellationToken canc { await WaitStrategyRunner.RunAsync(context, new[] { strategy }, TimeSpan.FromSeconds(5), cancellationToken); } - catch (WslContainerTimeoutException ex) + catch (ContainerTimeoutException ex) { thrown = ex; } diff --git a/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs b/src/tests/Wsl.UnitTests/WaitStrategyTests.cs similarity index 97% rename from src/tests/WslContainers.UnitTests/WaitStrategyTests.cs rename to src/tests/Wsl.UnitTests/WaitStrategyTests.cs index e6b2498..abe576f 100644 --- a/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs +++ b/src/tests/Wsl.UnitTests/WaitStrategyTests.cs @@ -1,8 +1,7 @@ using System.Text.RegularExpressions; -using Purview.WslContainers.Containers; -using Purview.WslContainers.Waiting; +using Purview.Containers.Waiting; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class WaitStrategyTests { diff --git a/src/tests/WslContainers.UnitTests/WslContainers.UnitTests.csproj b/src/tests/Wsl.UnitTests/Wsl.UnitTests.csproj similarity index 100% rename from src/tests/WslContainers.UnitTests/WslContainers.UnitTests.csproj rename to src/tests/Wsl.UnitTests/Wsl.UnitTests.csproj diff --git a/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs b/src/tests/Wsl.UnitTests/WslContainerRuntimeTests.cs similarity index 97% rename from src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs rename to src/tests/Wsl.UnitTests/WslContainerRuntimeTests.cs index f716143..974f99b 100644 --- a/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs +++ b/src/tests/Wsl.UnitTests/WslContainerRuntimeTests.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class WslContainerRuntimeTests { diff --git a/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs b/src/tests/Wsl.UnitTests/WslInspectParserTests.cs similarity index 97% rename from src/tests/WslContainers.UnitTests/WslInspectParserTests.cs rename to src/tests/Wsl.UnitTests/WslInspectParserTests.cs index 87d12c0..e845d91 100644 --- a/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs +++ b/src/tests/Wsl.UnitTests/WslInspectParserTests.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.Containers.Wsl; public class WslInspectParserTests { From b093835a7faf245b14e31561c14a099fa6115348 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 20:06:19 +0100 Subject: [PATCH 02/11] feat(wsl): run wslc from portable net10.0 projects via a runtime facade Purview.Containers.Wsl is now multi-target: net10.0 ships a portable facade and net10.0-windows10.0.19041.0 ships the implementation (compiled against the Microsoft.WSL.Containers projection, which is a net8.0-windows asset). A platform-neutral net10.0 project can therefore reference the WSL backend and get WSLC on a Windows host and Docker elsewhere through auto - no target-framework or configuration change. This also corrects the docs, which claimed WSLC required a .NET 11 Windows project. - facade (WslPayload) loads the Windows build from a wslc/ payload into a dedicated AssemblyLoadContext and delegates through IContainerBackend; it reports wsl unavailable on non-Windows or when the payload is absent, so selection falls through to Docker. Windows-targeting projects bind the implementation directly and are unchanged. - packaging: the implementation, the WSLC projection, its Windows SDK dependencies and the native SDK are packed under payload/win-{x64,arm64}; the buildTransitive targets copy the matching folder for a platform-neutral consumer on Windows. - guards: PCC0001 now accepts any .NET 10+ target (Windows 10.0.19041.0+ or platform-neutral); PCC0002 still rejects a 32-bit Windows consumer. - selection: a new optional IContainerBackendPreference.AutoPriority makes auto prefer WSLC (0) over Docker (100) deterministically, instead of relying on the MSBuild props-import order. - spikes: DynamicLoadSpike (phase 0 feasibility) and PortableConsumerSpike (acceptance) under spikes/. - tests/docs: consumer contract, packaging, backends and module READMEs updated; verify-consumers gains a portable case and updates the changed expectations. Validated: build 0 warnings; unit 98/98; Wsl integration 27/27; verify-consumers 19/19; pipeline-pack-validate 20/20 packages valid; csharpier clean. --- .agents/agents/sdk-consumer-setup.md | 8 +- .agents/agents/sdk-repository-rationaliser.md | 38 ++++ .../prompts/sdk-review-repository-shape.md | 33 ++++ .../sdk-engineering-principles/.gitignore | 8 + AGENTS.md | 41 ++-- Directory.Packages.props | 4 +- README.md | 16 +- docs/wiki/Architecture.md | 12 +- docs/wiki/Backends.md | 55 +++--- docs/wiki/Consumer-Requirements.md | 107 ++++++---- docs/wiki/Contributing-Modules.md | 2 +- docs/wiki/Contributing.md | 5 +- docs/wiki/Getting-Started.md | 12 +- docs/wiki/Home.md | 9 +- docs/wiki/Packaging.md | 9 +- global.json | 4 +- purview-build.json | 14 +- scripts/verify-consumers.ps1 | 18 +- .../DynamicLoadSpike/DynamicLoadSpike.csproj | 61 ++++++ spikes/DynamicLoadSpike/DynamicLoader.cs | 187 ++++++++++++++++++ spikes/DynamicLoadSpike/PayloadInspector.cs | 106 ++++++++++ spikes/DynamicLoadSpike/Program.cs | 30 +++ .../PortableConsumerSpike.csproj | 52 +++++ spikes/PortableConsumerSpike/Program.cs | 50 +++++ src/src/Azurite/Sdk/README.md | 10 +- src/src/Containers/Container.cs | 9 +- .../Containers/ContainerBackendSelection.cs | 13 +- src/src/Containers/ContainerBackends.cs | 21 +- src/src/Containers/ContainerBase.cs | 16 +- .../Containers/IContainerBackendPreference.cs | 26 +++ src/src/Containers/Sdk/README.md | 4 +- src/src/Containers/Waiting/WaitContext.cs | 25 +-- src/src/Docker/DockerContainer.cs | 4 +- src/src/Docker/DockerContainerBackend.cs | 10 +- src/src/MsSql/Sdk/README.md | 10 +- src/src/MySql/Sdk/README.md | 10 +- src/src/Nats/Sdk/README.md | 10 +- src/src/PostgreSql/Sdk/README.md | 10 +- src/src/RabbitMq/Sdk/README.md | 10 +- src/src/Redis/Sdk/README.md | 10 +- src/src/Wsl/Sdk/README.md | 31 +-- .../Purview.Containers.Wsl.props | 2 +- .../Purview.Containers.Wsl.targets | 85 ++++++-- src/src/Wsl/Wsl.csproj | 133 ++++++++++++- src/src/Wsl/WslContainerBackend.Facade.cs | 65 ++++++ src/src/Wsl/WslContainerBackend.cs | 13 +- src/src/Wsl/WslContainerRuntime.Facade.cs | 70 +++++++ src/src/Wsl/WslPayload.cs | 147 ++++++++++++++ .../ConsumerRequirementsTests.cs | 14 +- .../Wsl.UnitTests/ContainerBackendsTests.cs | 54 ++++- 50 files changed, 1436 insertions(+), 257 deletions(-) create mode 100644 .agents/agents/sdk-repository-rationaliser.md create mode 100644 .agents/prompts/sdk-review-repository-shape.md create mode 100644 .agents/skills/sdk-engineering-principles/.gitignore create mode 100644 spikes/DynamicLoadSpike/DynamicLoadSpike.csproj create mode 100644 spikes/DynamicLoadSpike/DynamicLoader.cs create mode 100644 spikes/DynamicLoadSpike/PayloadInspector.cs create mode 100644 spikes/DynamicLoadSpike/Program.cs create mode 100644 spikes/PortableConsumerSpike/PortableConsumerSpike.csproj create mode 100644 spikes/PortableConsumerSpike/Program.cs create mode 100644 src/src/Containers/IContainerBackendPreference.cs create mode 100644 src/src/Wsl/WslContainerBackend.Facade.cs create mode 100644 src/src/Wsl/WslContainerRuntime.Facade.cs create mode 100644 src/src/Wsl/WslPayload.cs diff --git a/.agents/agents/sdk-consumer-setup.md b/.agents/agents/sdk-consumer-setup.md index 8e5066c..c3cc84f 100644 --- a/.agents/agents/sdk-consumer-setup.md +++ b/.agents/agents/sdk-consumer-setup.md @@ -14,11 +14,14 @@ Help a consuming repository adopt or troubleshoot `Purview.BuildSdk` correctly, variables, `.git` root, or a nearby `package.json`. `UsePackageJsonVersion=Strict` fails fast instead of silently skipping resolution. 4. If the bundled `.agents/**` content isn't appearing in the repo root, check `EnableAgentFolderInPackage` - (default `true`) and `AgentPackDestinationFolder` (default `.agents`) — the copy runs before build via + (default `true`) and `AgentPackDestinationFolder` (default `.agents`) - the copy runs before build via `EnsureAgentFolderInPackageTarget`. 5. For test-framework or project-shape questions, confirm the project follows repo naming and placement conventions the SDK expects, rather than introducing bespoke structure. -6. Re-run `dotnet build` (or the repo's canonical build command) after each configuration change to confirm +6. For naming, layout, and test readability questions, start from the engineering principles: naming and + placement are configuration, short project names are preferred, the detected test type becomes the baseline + category, and subject-based tests should be named for the subject they own. +7. Re-run `dotnet build` (or the repo's canonical build command) after each configuration change to confirm the fix. ## Constraints @@ -32,3 +35,4 @@ Help a consuming repository adopt or troubleshoot `Purview.BuildSdk` correctly, ## Related skill See `../skills/sdk-configuration-reference/SKILL.md` for the full property reference. +See `../skills/sdk-engineering-principles/SKILL.md` for the higher-level repository conventions. diff --git a/.agents/agents/sdk-repository-rationaliser.md b/.agents/agents/sdk-repository-rationaliser.md new file mode 100644 index 0000000..d41060b --- /dev/null +++ b/.agents/agents/sdk-repository-rationaliser.md @@ -0,0 +1,38 @@ +# sdk-repository-rationaliser (generic agent spec) + +## Goal + +Help a repository move toward `Purview.BuildSdk` conventions without unnecessary churn. + +## Workflow + +1. Start by reading the repo's `Directory.Build.props`, `Directory.Build.targets`, solution entry point, + and current project layout. +2. Identify the current `NamespacePrefix`, project naming scheme, and test project suffixes in use. +3. Compare the repo's structure against the engineering principles: + - short project names + - source/test split where practical + - exact shared/shared-testing names when SDK behavior is expected + - readable, scalable test naming +4. Separate findings into three groups: + - already aligned + - misaligned but harmless + - misaligned and blocking SDK automatic behavior +5. Prefer the smallest sequence of changes that improves predictability without forcing broad renames. +6. When recommending test changes, preserve readable behavior-oriented suites while tightening subject-based + suites toward `{SubjectName}Tests` and `{SubjectOrMemberUnderTest}_{Scenario}_{Expectation}`. +7. Confirm whether specialized dependencies such as `TUnit.Aspire` or `Testcontainers` are genuinely needed + rather than treating them as universal defaults. + +## Constraints + +- Do not assume every older repo should be renamed wholesale. +- Preserve meaningful established structure unless it interferes with SDK inference. +- Prefer explaining the effect on `RootNamespace`, `AssemblyName`, `PackageId`, `TestingType`, and + `TargetProjectName` instead of arguing from taste. + +## Related skills + +- `../skills/sdk-engineering-principles/SKILL.md` +- `../skills/project-placement-defaults/SKILL.md` +- `../skills/sdk-project-behavior-and-detection/SKILL.md` diff --git a/.agents/prompts/sdk-review-repository-shape.md b/.agents/prompts/sdk-review-repository-shape.md new file mode 100644 index 0000000..eb089c9 --- /dev/null +++ b/.agents/prompts/sdk-review-repository-shape.md @@ -0,0 +1,33 @@ +# sdk-review-repository-shape (generic prompt spec) + +Review a repository that uses `Purview.BuildSdk` for naming, placement, and test-structure alignment. + +## Required behaviour + +1. Inspect the repository's `Directory.Build.props`, `Directory.Build.targets`, solution entry point, and + project layout before making assumptions. +2. Identify whether the repository follows the SDK-friendly structure: + - source projects under `src/` + - test projects under `tests/` + - `.csproj` filenames matching directory names + - short project names with `NamespacePrefix` carrying the repo identity +3. Check whether test project names use recognised `*Tests` suffixes and whether shared/shared-testing + projects use exact SDK-recognised names. +4. Explain the consequences of deviations in terms of automatic `RootNamespace`, `AssemblyName`, + `PackageId`, `TargetProjectName`, and automatic project references. +5. Review test readability conventions: + - subject-based `{SubjectName}Tests` + - subject-based `{SubjectOrMemberUnderTest}_{Scenario}_{Expectation}` method names + - non-subject-based suites named clearly for their broader role + - appropriate use of TUnit categories and display names +6. Distinguish between: + - acceptable existing variance worth preserving + - structural debt that blocks the SDK's automatic behavior + - incremental rationalisation opportunities + +## Suggested output + +- A concise summary of whether the repo broadly fits the SDK conventions. +- A list of concrete mismatches, ordered by impact. +- A list of low-risk rationalisation steps for naming, placement, identity, or test readability. +- Explicit note of which behaviors are already automatic defaults and which require manual configuration. diff --git a/.agents/skills/sdk-engineering-principles/.gitignore b/.agents/skills/sdk-engineering-principles/.gitignore new file mode 100644 index 0000000..2799754 --- /dev/null +++ b/.agents/skills/sdk-engineering-principles/.gitignore @@ -0,0 +1,8 @@ +# Ignore all files +* + +# Don't ignore directories, so Git can traverse them +!*/ + +# Keep this file +!.gitignore \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index bd3a5e4..7779e39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,12 +24,14 @@ whenever a target framework, a backend package or the `buildTransitive` assets c | --- | --- | | `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack | | `src/src/Containers` | Backend-neutral abstractions (`Purview.Containers`, `net10.0`): `IContainer`/`ContainerConfiguration`, `ContainerBuilder`, `ContainerBase`, `IContainerBackend`/`ContainerBackends`, wait strategies, images, networking, mounts, diagnostics | -| `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`, `net11.0-windows10.0.19041.0`): `WslContainerBackend`, the shared session runtime, `WslContainer`, `WslContainerSession` | +| `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`, multi-target `net10.0` facade + `net10.0-windows10.0.19041.0` implementation): `WslContainerBackend`, the shared session runtime, `WslContainer`, `WslContainerSession`, and the portable facade (`WslPayload`, `*.Facade.cs`) that loads the implementation at run time | | `src/src/Docker` | Docker backend (`Purview.Containers.Docker`, `net10.0`): `DockerContainerBackend` + `DockerContainer` over Testcontainers | | `src/src/` | Service modules (`Purview.Containers.`, `net10.0`); each is a thin layer over the abstractions and carries a bespoke `Sdk/README.md` | | `src/src//Sdk` | Package-only assets. `Sdk/README.md` is packed as the package README (suppressing the repo-root README); `Sdk/buildTransitive/**` ships MSBuild assets to consumers (the abstractions' backend registration, each backend package's own registration entry, and the WSL backend's consumer defaults and guards) | | `src/tests` | TUnit unit (`*.UnitTests`) and container integration (`*.IntegrationTests`) projects: the WSLC suites need a WSLC host, `Docker.IntegrationTests` and `Modules.DockerIntegrationTests` need a Docker daemon | | `spikes/WslcSpikes` | Phase 0 investigation harness (`s1`..`s14` probes); not part of the test run | +| `spikes/DynamicLoadSpike` | Phase 0 feasibility probe for the portable facade: loads the WSLC projection from a local payload and drives a session from a plain `net10.0` process; not part of the test run | +| `spikes/PortableConsumerSpike` | Acceptance probe for the portable facade: a `net10.0` project that binds the facade and runs a real WSLC container through a `wslc/` payload; not part of the test run | | `docs/wiki` | User-facing documentation wiki, aggregated by the purview-dev website. `Backends.md` is the consumer guide to choosing and configuring WSLC or Docker | | `samples/getting-started` | Runnable consumer-shaped samples (WSLC and Docker) built with the solution; `just sample-wsl` / `just sample-docker` | | `mkdocs.yml` | Wiki site configuration (`docs_dir: docs/wiki`) | @@ -65,10 +67,11 @@ whenever a target framework, a backend package or the `buildTransitive` assets c (`ContainerNotSupportedException`). - **One backend registry per process** (`ContainerBackends`): backend packages register themselves through the generated module initializer, `ResolveAsync()` returns the pinned backend, the - `PURVIEW_CONTAINERS_BACKEND`-selected backend, or the first *usable* registered backend (probing each - one), and an empty/unusable registry throws `ContainerBackendUnavailableException` naming the fix. A - named or pinned backend never silently falls back. A module must never reference a backend package — it - resolves through the registry. + `PURVIEW_CONTAINERS_BACKEND`-selected backend, or the first *usable* registered backend in + auto-priority order (`IContainerBackendPreference.AutoPriority`, lowest first; `wsl` is 0 and `docker` + is 100, so WSLC is preferred when both are usable), and an empty/unusable registry throws + `ContainerBackendUnavailableException` naming the fix. A named or pinned backend never silently falls + back. A module must never reference a backend package — it resolves through the registry. - **`DisposeAsync` is idempotent** and never terminates the shared session; a process-exit hook disposes the session so its name is released. - **Secrets use `Secret`** and must never reach logs, names, `ToString()` or diagnostics (they are redacted). @@ -76,18 +79,20 @@ whenever a target framework, a backend package or the `buildTransitive` assets c ## Consumer requirements and compatibility - The packages split by framework: `Purview.Containers` (abstractions) and the service modules target - **`net10.0`** on any platform, while `Purview.Containers.Wsl` is the **.NET 11, Windows-only** backend - (`net11.0-windows10.0.19041.0`). `src/src/Directory.Build.props` sets the `net10.0` subtree default and - `Wsl.csproj` overrides it; `src/tests` keeps the Windows target because the tests drive WSLC. The - contract, the failures that enforce it and the CI workarounds are documented in + **`net10.0`** on any platform. `Purview.Containers.Wsl` is **multi-target**: `net10.0` (a portable + facade) and `net10.0-windows10.0.19041.0` (the WSLC implementation). `src/src/Directory.Build.props` + sets the `net10.0` subtree default and `Wsl.csproj` adds the Windows build; `src/tests` keeps the + Windows target because the tests drive WSLC and therefore bind the implementation. The contract, the + failures that enforce it and the CI workarounds are documented in [Consumer Requirements](docs/wiki/Consumer-Requirements.md); update that page whenever a target - framework, the `buildTransitive` defaults or the `PCC0001`/`PCC0002` guards change. + framework, the `buildTransitive` defaults, the payload layout or the `PCC0001`/`PCC0002` guards change. - `Purview.Containers.Wsl` ships `Sdk/buildTransitive/Purview.Containers.Wsl.{props,targets}` so consumers - inherit `WindowsSdkPackageVersion`/`PlatformTarget` defaults and a clear error for an unsupported - target framework (`PCC0001`) or a 32-bit consumer (`PCC0002`). Those assets are framework-agnostic, so - NuGet no longer raises `NU1202` at restore time — the guards are the fail-fast path. Keep them, and - keep the `RequiredContent` entries that declare them. **These assets must never flow to a `net10.0` - consumer** (a Docker-backed one) or `PCC0001` fires on Linux; they belong to the WSL backend package only. + inherit `WindowsSdkPackageVersion`/`PlatformTarget` defaults, a clear error for an unsupported target + framework (`PCC0001`) or a 32-bit Windows consumer (`PCC0002`), and — for a platform-neutral consumer + on a Windows build host — a copy of the WSLC implementation payload (`wslc/`) that the `net10.0` facade + loads at run time. Those assets are framework-agnostic, so NuGet never raises `NU1202` at restore time; + the guards are the fail-fast path. Keep them, and keep the `RequiredContent` entries (including + `payload/**`) that declare them. They belong to the WSL backend package only. - `Purview.Containers` ships `Sdk/buildTransitive/Purview.Containers.{props,targets}`, which turn the `PurviewContainersBackends` list (each backend package appends its own entry) into a generated module initializer in the consuming assembly. That is how a package consumer's process discovers its @@ -140,9 +145,9 @@ whenever a target framework, a backend package or the `buildTransitive` assets c exclusively locks its image-store VHD, so parallel modules fail with `0x80070020`. - Use the shared `WslcTest` helper in `src/tests/SharedTestingFramework` for integration skip/availability checks. Unit tests may use the modules' `BuildConfigurationForTesting()` internal hook. -- `Wsl.UnitTests/ConsumerRequirementsTests.cs` guards the published target framework - (`.NETCoreApp,Version=v11.0` plus `Windows10.0.19041.0`); keep it in step with - [Consumer Requirements](docs/wiki/Consumer-Requirements.md). +- `Wsl.UnitTests/ConsumerRequirementsTests.cs` guards the published target framework of the Windows + build (`.NETCoreApp,Version=v10.0` plus `Windows10.0.19041.0`), which the test project binds; keep it + in step with [Consumer Requirements](docs/wiki/Consumer-Requirements.md). - `just verify-consumers` packs and then builds throwaway consumer projects to assert the documented guard errors and workarounds. It restores from nuget.org, so it stays out of the `[Category=Unit]` filter and is a local/explicit step. diff --git a/Directory.Packages.props b/Directory.Packages.props index 621c8fb..641e840 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,17 +8,15 @@ - - - + diff --git a/README.md b/README.md index 4116269..5004aac 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,11 @@ A Testcontainers-style library for .NET that runs throwaway Linux containers for > **Two backends, one API.** Containers are created through the backend-neutral `Purview.Containers` > abstractions, so the same test suite runs on **WSL Containers** (`Purview.Containers.Wsl`) or > **Docker** (`Purview.Containers.Docker`, driven by Testcontainers). Selection is automatic by default -> and can be pinned with `PURVIEW_CONTAINERS_BACKEND` — which is how a developer machine uses WSLC and a -> Linux CI runner uses Docker without changing a line of test code. +> and can be pinned with `PURVIEW_CONTAINERS_BACKEND`. A plain `net10.0` project — no Windows target +> framework needed — references `Purview.Containers.Wsl` and gets WSLC on a Windows developer machine +> and Docker on a Linux CI runner **without changing a line of test code or configuration**: the package +> is multi-target and its `net10.0` facade loads the WSLC implementation at run time on Windows and +> reports `wsl` as unavailable everywhere else. > > **[Backends: WSLC or Docker](docs/wiki/Backends.md)** — comparison, side-by-side project setup, CI > example and troubleshooting. @@ -36,9 +39,10 @@ Pick a backend — see [Backends: WSLC or Docker](docs/wiki/Backends.md) for the - Windows 10/11 with **WSL Containers**, installed via `wsl --install --no-distribution` (verified against WSL 3.0.1.0). -- A consuming project that is a **.NET 11 project targeting Windows specifically** — - `net11.0-windows10.0.19041.0`, x64 or arm64. The `Purview.Containers.Wsl` package ships MSBuild defaults - for the supporting settings. +- A consuming project that is **.NET 10 or later**. A platform-neutral `net10.0` project binds the + portable facade and gets automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package ships MSBuild defaults for the supporting settings. **Docker backend** @@ -171,7 +175,7 @@ The project documentation lives in [`docs/wiki`](docs/wiki/Home.md) and is publi - [Getting Started](docs/wiki/Getting-Started.md) — prerequisites, first container, first module. - [Backends: WSLC or Docker](docs/wiki/Backends.md) — how to choose, side-by-side project setup, CI example, troubleshooting. -- [Consumer Requirements](docs/wiki/Consumer-Requirements.md) — the .NET 11 + Windows target framework contract, the `PCC0001`/`PCC0002` guards, and the CI workarounds. +- [Consumer Requirements](docs/wiki/Consumer-Requirements.md) — the target-framework contract, the `PCC0001`/`PCC0002` guards, and the CI workarounds. - [Architecture](docs/wiki/Architecture.md) — the shared session model, concurrency and cleanup decisions. - [Lifecycle](docs/wiki/Lifecycle.md), [Networking](docs/wiki/Networking.md), [Wait Strategies](docs/wiki/Wait-Strategies.md). - [Modules](docs/wiki/Modules.md) — the module contract and every shipped module. diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md index f216c8a..7360994 100644 --- a/docs/wiki/Architecture.md +++ b/docs/wiki/Architecture.md @@ -15,7 +15,7 @@ Purview.Containers (net10.0, portable) ├─ Waiting / Images / Mounts / Networking / Diagnostics readiness, model, secrets └─ Runtime/ContainerException neutral error taxonomy -Purview.Containers.Wsl (net11.0-windows10.0.19041.0) +Purview.Containers.Wsl (net10.0 facade + net10.0-windows10.0.19041.0 implementation) ├─ WslContainerBackend : IContainerBackend registers itself as "wsl" ├─ WslContainerRuntime : IContainerRuntime process singleton, owns the shared session │ ├─ SessionSettings (name, storagePath, cpu/mem, gpu, timeout) @@ -47,10 +47,12 @@ precedence: 2. **Named backend** — `PURVIEW_CONTAINERS_BACKEND=wsl|docker|` (or `Use(ContainerBackendSelection.Named(...))`). The named backend is probed: an unknown name lists what is registered, and an unusable one fails with its own diagnostics. There is no fallback. -3. **Automatic detection** — every registered backend is probed in registration order and the first - *available and compatible* one wins. When none is usable the exception lists each backend's - availability, version and missing components, plus the `PURVIEW_CONTAINERS_BACKEND` values that would - work. +3. **Automatic detection** — every registered backend is probed in **auto-priority** order and the first + *available and compatible* one wins. A backend positions itself with `IContainerBackendPreference` + (lower `AutoPriority` first; a backend without it counts as 0, so registration order is preserved for + ties). The WSL Containers backend declares a lower priority than Docker, so a machine that can run both + prefers WSLC. When none is usable the exception lists each backend's availability, version and missing + components, plus the `PURVIEW_CONTAINERS_BACKEND` values that would work. Resolution is cached per process, so probes run once; `Reset()` (tests) and `Register`/`Use` invalidate the cache. Package consumers get registration from the generated module initializer in diff --git a/docs/wiki/Backends.md b/docs/wiki/Backends.md index 6b8bb38..137e930 100644 --- a/docs/wiki/Backends.md +++ b/docs/wiki/Backends.md @@ -17,7 +17,7 @@ environment. | **Package** | `Purview.Containers.Wsl` | `Purview.Containers.Docker` | | **Prerequisite** | Windows 10/11 with WSL Containers (`wsl --install --no-distribution`) | any reachable Docker daemon (Docker Desktop, Docker Engine in WSL2, a VM, or a CI runner) | | **Host OS** | Windows only | Windows, Linux, macOS | -| **Project target framework** | `net11.0-windows10.0.19041.0`, x64 or arm64 | `net10.0` or later, any platform | +| **Project target framework** | `net10.0` or later, any platform (portable facade), or `net10.0-windows10.0.19041.0`, x64 or arm64 (implementation bound directly) | `net10.0` or later, any platform | | **How containers run** | the `Microsoft.WSL.Containers` managed API (daemonless) | the Docker Engine API via Testcontainers | | **Images** | a shared store (`%LOCALAPPDATA%\Purview\WslContainers\images`) reused across runs | the daemon's own image store | | **Leak protection** | session disposal plus a process-exit hook | the Testcontainers resource reaper (Ryuk) | @@ -117,7 +117,7 @@ public class CacheTests The **package reference** decides which runtime executes it. Three project shapes cover every case. -### Option 1 — WSLC on a Windows machine +### Option 1 — WSLC on a Windows machine, Docker elsewhere (one project) ```bash dotnet add package Purview.Containers.Wsl @@ -126,8 +126,7 @@ dotnet add package Purview.Containers.Wsl ```xml - net11.0-windows10.0.19041.0 - x64 + net10.0 enable enable @@ -137,8 +136,10 @@ dotnet add package Purview.Containers.Wsl ``` -`WindowsSdkPackageVersion` is supplied by the package. A project that targets anything other than a .NET 11 -Windows framework fails the build with `PCC0001`, and a 32-bit consumer with `PCC0002`; see +A platform-neutral `net10.0` project binds the portable facade, so it selects WSLC on a Windows host +and Docker everywhere else. A Windows target framework (`net10.0-windows10.0.19041.0`, x64 or arm64) +binds the implementation directly. `WindowsSdkPackageVersion` is supplied by the package. A project +older than .NET 10 fails the build with `PCC0001`, and a 32-bit Windows consumer with `PCC0002`; see [Consumer Requirements](Consumer-Requirements.md). ### Option 2 — Docker anywhere @@ -163,9 +164,10 @@ dotnet add package Purview.Containers.Docker That project restores and builds on Linux, macOS and Windows — no Windows target framework, no `WindowsSdkPackageVersion`, no Docker Desktop licence requirement beyond the daemon you already run. -### Option 3 — one project, both backends +### Option 3 — one project, both backends (auto) -Multi-target, and reference each backend only for the framework it supports: +Reference both backends from a single **platform-neutral** project. `auto` selects WSLC on a machine that +can run it and Docker otherwise — no multi-targeting and no conditional references: ```bash dotnet add package Purview.Containers.Wsl @@ -175,32 +177,25 @@ dotnet add package Purview.Containers.Docker ```xml - net11.0-windows10.0.19041.0;net10.0 + net10.0 enable enable - + - - ``` -On a Windows host that also runs Docker, reference **both** packages for the same framework and let the -selection policy choose (WSLC first, Docker next): - -```xml - - - - -``` +`auto` probes both in priority order (`wsl` before `docker`) and uses the first that is usable, so this +one project runs on WSLC on a developer's Windows machine and on Docker in a Linux CI job. A Windows +target framework is still supported when you want the implementation bound at compile time, but it is not +required. -If your code needs a backend-specific API (for example `WslContainerRuntime`, or -`WslContainerBackend(runtime)` to pin a specific WSLC session) guard it so the portable target still -compiles: +If your code needs a Windows-only API the portable facade does not expose (for example `WslContainer`, or +`WslContainerBackend(runtime)` to pin a specific WSLC session), guard it with `#if WINDOWS` — defined by +the SDK only for a Windows target framework — so a portable target still compiles: ```csharp #if WINDOWS @@ -248,7 +243,7 @@ Selection is resolved once per process, in this order: | --- | --- | --- | --- | | 1 | Pinned instance | `ContainerBackends.Use(new DockerContainerBackend())`, or `WithBackend(...)` on one builder | used as-is: never probed, never substituted | | 2 | Named backend | `PURVIEW_CONTAINERS_BACKEND=wsl\|docker\|`, or `ContainerBackends.Use(ContainerBackendSelection.Named("docker"))` | probed; a missing or unusable backend fails with its own diagnostics and **no fallback** | -| 3 | Automatic detection | the default (`auto`) | every registered backend is probed in registration order; the first *available and compatible* one wins (WSLC before Docker) | +| 3 | Automatic detection | the default (`auto`) | every registered backend is probed in auto-priority order (`wsl` at 0, `docker` at 100); the first *available and compatible* one wins, so WSLC is preferred over Docker | ```csharp using Purview.Containers; @@ -302,8 +297,10 @@ jobs: - run: dotnet test --configuration Release --no-build ``` -- Target `net10.0` or later and never `net11.0-windows…` on a Linux job; the Docker backend is portable and - the WSLC backend package is Windows-only. +- On a Linux job, target a platform-neutral framework (`net10.0` or later). The WSL Containers package is + portable — its `net10.0` facade loads the WSLC implementation on a Windows host and reports `wsl` as + unavailable elsewhere — so one test project can reference both backends and `auto` selects WSLC on a + Windows developer machine and Docker on the Linux runner. - Keep your integration tests in their own project or category if you want the fast unit tests to stay runtime-free; both can run in the same job. - Pinning `PURVIEW_CONTAINERS_BACKEND=docker` makes a runner without a usable daemon **fail loudly** with @@ -375,7 +372,7 @@ using the wrong runtime. backend whose package is not referenced by the project. **`PCC0001` / `PCC0002`** — a build-time guard from the WSL Containers backend package: the consuming project -does not target a .NET 11 Windows framework, or is 32-bit. See +is neither .NET 10+ (Windows or platform-neutral), or is a 32-bit Windows consumer. See [Consumer Requirements](Consumer-Requirements.md), or switch the project to the Docker backend. **The same image is pulled twice** — expected when both runtimes are used: WSLC and Docker keep separate @@ -384,7 +381,7 @@ image stores. ## Related - [Getting Started](Getting-Started.md) — install, first container, first typed module. -- [Consumer Requirements](Consumer-Requirements.md) — the .NET 11 Windows contract, the guards and the workarounds. +- [Consumer Requirements](Consumer-Requirements.md) — the target-framework contract, the guards and the workarounds. - [Architecture](Architecture.md#backend-selection) — how resolution and registration work internally. - [Modules](Modules.md) — the service modules and their readiness strategies. diff --git a/docs/wiki/Consumer-Requirements.md b/docs/wiki/Consumer-Requirements.md index eb79d23..187f6a0 100644 --- a/docs/wiki/Consumer-Requirements.md +++ b/docs/wiki/Consumer-Requirements.md @@ -5,10 +5,14 @@ > support guarantee. Treat every version as a preview and pin the exact package version you build > against. -The WSL Containers backend package targets `net11.0-windows10.0.19041.0` and is built on the -`Microsoft.WSL.Containers` WinRT projection, so a consuming project **must be a .NET 11 (or later) -project that targets Windows specifically**. This page is the authoritative statement of that -contract, of the workarounds that exist for it, and of how each of them is verified. +The WSL Containers backend package is **multi-target**. It ships a Windows build +(`net10.0-windows10.0.19041.0`, the implementation, built on the `Microsoft.WSL.Containers` +projection) and a platform-neutral build (`net10.0`, a facade). A Windows-targeting project binds the +implementation directly; any other project binds the facade, which builds on **every** platform and, on +a Windows host, loads the implementation at run time. That is what lets the *same* `net10.0` test +project run on WSLC on a developer's Windows machine and on Docker in a Linux CI job with no +configuration change. This page is the authoritative statement of that contract, of the workarounds +that exist for it, and of how each of them is verified. ## Which package requires what @@ -16,19 +20,22 @@ contract, of the workarounds that exist for it, and of how each of them is verif | --- | --- | --- | | `Purview.Containers` | `net10.0`, any platform | Backend-neutral abstractions. | | `Purview.Containers.` | `net10.0`, any platform | Service modules. Restore anywhere; needs a backend package to actually run. | -| `Purview.Containers.Wsl` | `net11.0-windows10.0.19041.0` | The WSL Containers backend. **The requirements on this page are its contract.** | +| `Purview.Containers.Wsl` | `net10.0` and `net10.0-windows10.0.19041.0` | The WSL Containers backend: a portable facade plus the Windows implementation. **The requirements on this page are its contract.** | | `Purview.Containers.Docker` | `net10.0`, any platform | The Docker backend (Testcontainers). Needs a reachable Docker daemon, not a Windows target framework. | Everything below describes the **WSL Containers backend**. A consumer of a different backend (for -example `Purview.Containers.Docker`) needs neither .NET 11 nor a Windows target framework: the +example `Purview.Containers.Docker`) needs only a portable `net10.0` project: the abstractions and the service modules are portable `net10.0` assets. For how to use and switch between the backends, see [Backends: WSLC or Docker](Backends.md). ## Copy this into your project +Most projects need **nothing at all**: a `net10.0` project binds the facade and gets automatic backend +selection. A Windows-targeting project can be explicit: + ```xml - net11.0-windows10.0.19041.0 + net10.0-windows10.0.19041.0 x64 10.0.26100.80 @@ -43,33 +50,41 @@ file, and because the defaults only fill in a value when you have not chosen one | # | Requirement | Why it exists | | --- | --- | --- | -| 1 | **.NET 11 or later** (the WSL Containers backend only) | `Purview.Containers.Wsl` ships only `lib/net11.0-windows10.0.19041/` assets. `Purview.Containers` and the service modules ship `lib/net10.0/` and restore on any .NET 10+ project. | -| 2 | **A Windows-specific target framework that names the OS version** (`net11.0-windows10.0.19041.0` or later) | The `Microsoft.WSL.Containers` projection only ships Windows assets. `net11.0-windows` on its own is not enough. | +| 1 | **.NET 10 or later** | `Purview.Containers.Wsl` ships `lib/net10.0/` (facade) and `lib/net10.0-windows10.0.19041/` (implementation). `Purview.Containers` and the service modules ship `lib/net10.0/` too. | +| 2 | **A Windows-specific target framework that names the OS version** (`net10.0-windows10.0.19041.0` or later) — *only when you want the implementation bound directly; a platform-neutral `net10.0` project binds the facade and still runs WSLC on a Windows host* | The `Microsoft.WSL.Containers` projection only ships Windows assets. `net10.0-windows` on its own is not enough. | | 3 | **64-bit** (`PlatformTarget` `x64`/`arm64`, or a matching `RuntimeIdentifier`) | The native `wslcsdk.dll` ships for `win-x64` and `win-arm64` only. | | 4 | **`WindowsSdkPackageVersion` at least `10.0.26100.80`** | The projection is compiled against `Microsoft.Windows.SDK.NET` `10.0.26100.79`; the pack the SDK resolves for a `10.0.19041.0` target framework is older. | | 5 | **A Windows 10/11 host with WSL Containers** to actually run containers | The library drives WSLC; it never installs or updates WSL for you. | -### 1. .NET 11 or later +### 1. .NET 10 or later + +`Purview.Containers.Wsl` ships a `net10.0` facade and a `net10.0-windows10.0.19041.0` implementation, +so it restores on any .NET 10+ project. The repository's own SDK is .NET 11 (`global.json` pins +`11.0.100-rc.1.26425.128`), but that is a build-time detail, not a consumer requirement. + +### 2. A Windows-specific target framework (optional) -The repository, the packages and the tests all target .NET 11 (`global.json` pins -`11.0.100-rc.1.26425.128`). A `net10.0-windows…` or `net8.0-windows…` project cannot consume the -packages; see [Non-.NET-11 Windows projects](#workaround-consuming-from-a-non-net-11-windows-project). +The **recommended** choice is a platform-neutral target framework. It binds the portable facade and +gives you the automatic "WSLC on a Windows machine, Docker everywhere else" behaviour: + +```xml +net10.0 +``` -### 2. A Windows-specific target framework +A Windows target framework binds the implementation directly (no run-time load): ```xml -net11.0-windows10.0.19041.0 +net10.0-windows10.0.19041.0 ``` ```xml -net11.0-windows -net11.0 +net10.0-windows ``` -`net11.0-windows` resolves its platform version to `7.0`, which is older than the `10.0.19041.0` -the packages were built against, so NuGet selects no compile assets for it. Always spell out the +`net10.0-windows` resolves its platform version to `7.0`, which is older than the `10.0.19041.0` +the implementation was built against, so NuGet selects no compile assets for it. Always spell out the version. (The `Microsoft.WSL.Containers` package itself is `net8.0-windows10.0.19041.0`, which a -`net11.0-windows10.0.19041.0` project consumes without issue.) +`net10.0-windows10.0.19041.0` project consumes without issue.) ### 3. 64-bit consumers only @@ -83,7 +98,7 @@ error PCC0002: Purview.Containers.Wsl requires a 64-bit (x64 or arm64) consumer ### 4. Windows SDK targeting pack version `Microsoft.WSL.Containers` 3.0.1's projection references `Microsoft.Windows.SDK.NET` -`10.0.26100.79`. Targeting `net11.0-windows10.0.19041.0` resolves a lower pack +`10.0.26100.79`. Targeting a Windows target framework (`net10.0-windows10.0.19041.0`) resolves a lower pack (`10.0.19041.x`) by default and the compiler rejects the mismatch: ```text @@ -113,18 +128,20 @@ project that does — gets them automatically. | Package path | Effect | | --- | --- | -| `buildTransitive/Purview.Containers.Wsl.props` | Defaults `WindowsSdkPackageVersion` to `10.0.26100.80` when the consumer has not set it. | -| `buildTransitive/Purview.Containers.Wsl.targets` | Defaults `PlatformTarget` to `x64` when it is unset (or `AnyCPU`) and no `RuntimeIdentifier` is selected, and fails the build with `PCC0001`/`PCC0002` when the target framework or the platform cannot be supported. | +| `buildTransitive/Purview.Containers.Wsl.props` | Defaults `WindowsSdkPackageVersion` to `10.0.26100.80` when the consumer has not set it, and registers the `wsl` backend. | +| `buildTransitive/Purview.Containers.Wsl.targets` | For a **Windows** consumer: defaults `PlatformTarget` to `x64` when it is unset (or `AnyCPU`) and no `RuntimeIdentifier` is selected, and fails the build with `PCC0001`/`PCC0002` when the target framework or the platform cannot be supported. For a **platform-neutral** consumer: copies the Windows implementation payload (the implementation, the WSLC projection, its Windows SDK dependencies and the native SDK) into `wslc/` next to the output on a Windows build host, so the facade can load it at run time. | -A consumer therefore only has to choose a target framework: +A consumer therefore only has to choose a target framework — and the portable one is best, because it +runs on WSLC locally and Docker in CI with no further configuration: ```xml - net11.0-windows10.0.19041.0 + net10.0 + ``` @@ -141,7 +158,7 @@ always wins over these defaults. | Error | Raised by | Meaning | Fix | | --- | --- | --- | --- | -| `PCC0001` | the shipped guard target | The consumer's target framework is not .NET 11+ targeting Windows 10.0.19041.0+ | Retarget as above, or make the reference conditional when multi-targeting. | +| `PCC0001` | the shipped guard target | The consumer's target framework is neither .NET 10+ on Windows 10.0.19041.0+ nor a platform-neutral .NET 10+ framework | Retarget as above. | | `PCC0002` | the shipped guard target | The consumer is not 64-bit | Remove your `PlatformTarget`, set it to `x64`/`arm64`, or choose a matching `RuntimeIdentifier`. | | `CS1705` | the C# compiler | `WindowsSdkPackageVersion` is older than the projection requires | Set `WindowsSdkPackageVersion` to `10.0.26100.80` or later. | | `CS8012` | the C# compiler | A referenced assembly targets a different processor (seen when a consumer forces `x86`) | Use a 64-bit consumer. | @@ -150,7 +167,7 @@ always wins over these defaults. ## Workaround: building on a non-Windows CI agent -`net11.0-windows…` projects can be restored, compiled and unit-tested on Linux or macOS, provided +`net*-windows…` projects can be restored, compiled and unit-tested on Linux or macOS, provided the build opts in: ```xml @@ -173,9 +190,9 @@ What it does and does not do: - ❌ does not provide WSL Containers — the WSLC integration suites need a Windows host; - ❌ does not make `wslcsdk.dll` loadable; anything that opens a session must run on Windows. -## Workaround: consuming from a non-.NET-11 Windows project +## Workaround: consuming from a project older than .NET 10 -**There is none.** A project that targets Windows but not .NET 11 (for example +**There is none.** A project that targets Windows but not .NET 10 (for example `net8.0-windows10.0.19041.0`) cannot use these packages, and the repository verifies that no escape hatch exists. @@ -185,7 +202,7 @@ target framework for the project's package references: ```xml net8.0-windows10.0.19041.0 - net11.0-windows10.0.19041.0 + net10.0-windows10.0.19041.0 ``` @@ -193,34 +210,35 @@ It does **not** work here. `AssetTargetFallback` is not applied to `netcoreapp`- the package still contributes no compile assets and the build fails in your own code with `CS0234`/`CS0246`. `PCC0001` fires as well. -Even if it did compile, it would not help: the `lib/` assemblies are .NET 11 assemblies, so they can -only be loaded by a .NET 11 runtime. A consumer would have to move to .NET 11 to run them anyway. +Even if it did compile, it would not help: the `lib/` assemblies target .NET 10, so they can only be +loaded by a .NET 10+ runtime. A consumer would have to move to .NET 10 to run them anyway. If you need this library from a project you cannot retarget, keep the container work in a small -**.NET 11 Windows test project** (which can reference the older project) rather than trying to +**.NET 10+ test project** (which can reference the older project) rather than trying to consume the packages from the older project. ## Multi-targeting -Only the Windows inner build may reference the packages, so make the reference conditional: +Multi-targeting is **no longer required** for the WSL Containers backend: a platform-neutral `net10.0` +target binds the portable facade. If you multi-target for other reasons, reference the package +unconditionally — both inner builds are supported: ```xml - net11.0;net11.0-windows10.0.19041.0 + net10.0;net10.0-windows10.0.19041.0 - + ``` -An unconditional reference fails the whole build: the `net11.0` inner build reports `PCC0001`. -Keep any code that uses the library behind the same condition — -`#if WINDOWS` (defined by the SDK for Windows target frameworks) or a conditional `Compile` item — -so the non-Windows inner build can still compile. +The Windows inner build binds the implementation; the portable inner build binds the facade. The +historical pattern — multi-targeting with the reference only on the Windows inner build — still works, +because the package keeps a `net10.0-windows10.0.19041` build, but the condition is no longer needed. ## Verifying these requirements -`just verify-consumers` packs the solution and builds sixteen throwaway consumer projects against the +`just verify-consumers` packs the solution and builds nineteen throwaway consumer projects against the produced packages, asserting every claim on this page: | Case | Consumer | Expected outcome | @@ -232,15 +250,18 @@ produced packages, asserting every claim on this page: | 05 | a module package plus the WSL Containers backend, with no settings | builds; `wslcsdk.dll` copied (the defaults are transitive through the backend) | | 06 | `net8.0-windows` + `AssetTargetFallback` | `PCC0001` — the escape hatch does not work | | 07 | `net8.0-windows` | `PCC0001` | -| 08 | plain `net11.0` | `PCC0001` | +| 08 | plain `net11.0` (platform-neutral facade) | builds; the `wsl` registration is generated | | 09 | …with `PlatformTarget=AnyCPU` | builds (corrected to `x64`) | | 10 | the `net11.0-windows` shorthand (no OS version) | `PCC0001` | | 11 | multi-targeting with a conditional `PackageReference` | builds | -| 12 | multi-targeting with an unconditional `PackageReference` | `PCC0001` | +| 12 | multi-targeting with an unconditional `PackageReference` | builds (both inner builds are supported) | | 13 | a `net10.0` consumer of the portable abstractions | builds | | 14 | a `net10.0` consumer of the Docker backend | builds; the backend registration is generated into the consumer | | 15 | a `net10.0` consumer of a service module, with no backend package | builds | | 16 | a `.NET 11` Windows consumer with **both** backends referenced | builds; both registrations are generated | +| 17 | the documented backend example on WSL Containers | builds | +| 18 | the documented backend example on Docker (`net10.0`) | builds | +| 19 | a plain `net10.0` consumer of the WSL Containers backend | builds; the `wsl` registration is generated (the portable facade) | The script is `scripts/verify-consumers.ps1` and it only writes to the temp folder. It needs network access (it restores transitive dependencies from nuget.org), so it is a local/CI-explicit step diff --git a/docs/wiki/Contributing-Modules.md b/docs/wiki/Contributing-Modules.md index e767c9c..ab7e901 100644 --- a/docs/wiki/Contributing-Modules.md +++ b/docs/wiki/Contributing-Modules.md @@ -74,5 +74,5 @@ public class MyServiceBuilder : ContainerBuilder.dll` | The built assembly: `net10.0` for `Purview.Containers` and the service modules, `net11.0-windows10.0.19041` for the WSL Containers backend. | +| `lib/$(TFM)/Purview.Containers.*.dll` | The built assembly: `net10.0` for `Purview.Containers` and the service modules; `Purview.Containers.Wsl` ships `net10.0` (the portable facade) and `net10.0-windows10.0.19041` (the implementation). | | `lib/$(TFM)/Purview.Containers.Wsl.xml` / `lib/$(TFM)/Purview.Containers..xml` | XML documentation, generated because `GenerateDocumentationFile` is on for packable projects. | | `README.md` | The package's bespoke `Sdk/README.md` (see below). | | `purview-logo-light.png` | The shared Purview package icon (`assets/images/purview-logo-light.png`). | | `buildTransitive/Purview.Containers.props` / `.targets` | **Abstractions package.** Declares the `PurviewContainersBackends` list and turns it into a generated backend initializer in the consuming assembly. | | `buildTransitive/Purview.Containers.Wsl.props` / `.targets` | **WSL Containers backend only.** Defaults `WindowsSdkPackageVersion`/`PlatformTarget` and raises `PCC0001`/`PCC0002` for unsupported consumers. | | `buildTransitive/Purview.Containers.Docker.props` | **Docker backend only.** Adds the Docker backend to the `PurviewContainersBackends` list. | +| `payload/win-x64/*`, `payload/win-arm64/*` | **WSL Containers backend only.** The Windows implementation, the WSLC projection, its Windows SDK dependencies and the native SDK. The `net10.0` facade loads them at run time on a Windows host; the `buildTransitive` targets copy the matching folder to the consumer output. | Portable PDBs are delivered through the `.snupkg`, never inside the `.nupkg`. `$(TFM)` is expanded per shipped framework: `Purview.Containers`, `Purview.Containers.Docker` and the -service modules ship `lib/net10.0/`, and `Purview.Containers.Wsl` ships -`lib/net11.0-windows10.0.19041/`, so only the WSL Containers backend is a **.NET 11, Windows-only** -package. Consumers must target a matching framework; see +service modules ship `lib/net10.0/`; `Purview.Containers.Wsl` ships both `lib/net10.0/` (the facade) +and `lib/net10.0-windows10.0.19041/` (the implementation), with the `payload/` folders alongside. +Every package therefore restores on a portable `net10.0` project. Consumers must target .NET 10+; see [Consumer Requirements](Consumer-Requirements.md). ## Package metadata diff --git a/global.json b/global.json index 186257a..d12cc2a 100644 --- a/global.json +++ b/global.json @@ -4,9 +4,9 @@ "allowPrerelease": true }, "msbuild-sdks": { - "Purview.BuildSdk": "1.0.2.0" + "Purview.BuildSdk": "1.0.2.2" }, "test": { "runner": "Microsoft.Testing.Platform" } -} \ No newline at end of file +} diff --git a/purview-build.json b/purview-build.json index a252b13..a5076d7 100644 --- a/purview-build.json +++ b/purview-build.json @@ -30,13 +30,21 @@ "purview-logo-light.png", "buildTransitive/Purview.Containers.Docker.props" ], + // Purview.Containers.Wsl is multi-target: lib/net10.0 is the portable facade, and + // lib/net10.0-windows10.0.19041 is the WSLC implementation. The portable consumer loads + // that implementation (plus the WSLC projection, its Windows SDK dependencies and the + // native SDK) from payload// via the buildTransitive targets. "purview.containers.wsl": [ - "lib/$(TFM)/Purview.Containers.Wsl.dll", - "lib/$(TFM)/Purview.Containers.Wsl.xml", + "lib/net10.0/Purview.Containers.Wsl.dll", + "lib/net10.0/Purview.Containers.Wsl.xml", + "lib/net10.0-windows10.0.19041/Purview.Containers.Wsl.dll", + "lib/net10.0-windows10.0.19041/Purview.Containers.Wsl.xml", "README.md", "purview-logo-light.png", "buildTransitive/Purview.Containers.Wsl.props", - "buildTransitive/Purview.Containers.Wsl.targets" + "buildTransitive/Purview.Containers.Wsl.targets", + "payload/win-x64/*", + "payload/win-arm64/*" ], "purview.containers.azurite": [ "lib/$(TFM)/Purview.Containers.Azurite.dll", diff --git a/scripts/verify-consumers.ps1 b/scripts/verify-consumers.ps1 index 75e265f..c640009 100644 --- a/scripts/verify-consumers.ps1 +++ b/scripts/verify-consumers.ps1 @@ -19,17 +19,18 @@ 05 a module package with no settings (defaults are transitive) -> builds 06 net8.0-windows + AssetTargetFallback escape hatch -> PCC0001 (the hatch does not work) 07 net8.0-windows without the escape hatch -> PCC0001 - 08 plain net11.0 (not Windows-specific) -> PCC0001 + 08 plain net11.0 (platform-neutral facade) -> builds, wsl registration generated 09 ...with PlatformTarget=AnyCPU (corrected to x64) -> builds, wslcsdk.dll copied 10 the net11.0-windows shorthand TFM (no OS version) -> PCC0001 11 multi-targeting with a conditional PackageReference -> builds - 12 multi-targeting with an unconditional PackageReference -> PCC0001 + 12 multi-targeting with an unconditional PackageReference -> builds (both inner builds supported) 13 net10.0 + portable abstractions -> builds 14 net10.0 + Docker backend -> builds, backend registration generated 15 net10.0 + service module (no backend package) -> builds 16 net11.0 windows + both backends (auto detection) -> builds, both registrations generated 17 the documented backend example, on WSL Containers -> builds 18 the documented backend example, on Docker (net10.0) -> builds + 19 plain net10.0 + WSL backend (portable facade, auto WSLC/Docker) -> builds, wsl registration generated .PARAMETER FeedPath Folder holding the packed .nupkg files. Defaults to /artifacts. @@ -319,9 +320,10 @@ $cases = @( -Framework "$net8Windows" ` -Properties "$sdkVersion$x64" -Expect 'PCC0001' - New-ConsumerCase -Id '08' -Name 'plain net11.0 (not Windows-specific)' ` + New-ConsumerCase -Id '08' -Name 'plain net11.0 (platform-neutral facade)' ` -Framework 'net11.0' ` - -Properties "$sdkVersion$x64" -Expect 'PCC0001' + -Sources @{ 'Smoke.Portable.cs' = $portableSource } ` + -RequireText 'WslContainerBackend.Create()' New-ConsumerCase -Id '09' -Name 'net11 windows + PlatformTarget=AnyCPU' ` -Properties "$sdkVersionAnyCPU" -RequireFile 'wslcsdk.dll' @@ -341,8 +343,7 @@ $cases = @( -Framework "net11.0;$net11Windows" ` -Properties 'false' ` -Items $multiTargetItems ` - -Sources @{ 'Smoke.Windows.cs' = $apiSource; 'Smoke.Portable.cs' = $portableSource } ` - -Expect 'PCC0001' + -Sources @{ 'Smoke.Windows.cs' = $apiSource; 'Smoke.Portable.cs' = $portableSource } New-ConsumerCase -Id '13' -Name 'net10 + portable abstractions' ` -Framework 'net10.0' ` @@ -374,6 +375,11 @@ $cases = @( -Framework 'net10.0' ` -Package 'Purview.Containers.Docker' ` -Sources @{ 'Smoke.cs' = $documentedBackendSource } + + New-ConsumerCase -Id '19' -Name 'plain net10.0 + WSL backend (portable facade, auto WSLC/Docker)' ` + -Framework 'net10.0' ` + -Sources @{ 'Smoke.Portable.cs' = $portableSource } ` + -RequireText 'WslContainerBackend.Create()' ) diff --git a/spikes/DynamicLoadSpike/DynamicLoadSpike.csproj b/spikes/DynamicLoadSpike/DynamicLoadSpike.csproj new file mode 100644 index 0000000..8c5d55d --- /dev/null +++ b/spikes/DynamicLoadSpike/DynamicLoadSpike.csproj @@ -0,0 +1,61 @@ + + + + Exe + false + net10.0 + enable + enable + x64 + true + $(NoWarn);CA1031;CA1304;CA1305;CA1307;CA1310;CA1849;CA2000;CA2027;CA1707;CA1822;CA1062;CA1515;CA1861;CA1819;IDE0040;IDE0060;IDE0074;IDE0007;IDE0305;CS4014 + + + + + $(NuGetPackageRoot)microsoft.wsl.containers/3.0.1 + $(NuGetPackageRoot)microsoft.windows.sdk.net.ref/10.0.26100.80 + + + + + + + + + diff --git a/spikes/DynamicLoadSpike/DynamicLoader.cs b/spikes/DynamicLoadSpike/DynamicLoader.cs new file mode 100644 index 0000000..c9a4b0d --- /dev/null +++ b/spikes/DynamicLoadSpike/DynamicLoader.cs @@ -0,0 +1,187 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; + +namespace DynamicLoadSpike; + +/// +/// Loads the WSLC projection (and the native SDK it P/Invokes) from the local payload folder and calls +/// into Microsoft.WSL.Containers.WslcService by reflection. This is the phase 0 proof that a +/// platform-neutral net10.0 process can drive WSLC on a Windows host. +/// +internal static class DynamicLoader +{ + const string ProjectionFile = "wslcsdkcs.dll"; + const string ServiceType = "Microsoft.WSL.Containers.WslcService"; + + public static Task RunAsync(string payloadDir) + { + if (!OperatingSystem.IsWindows()) + { + Console.WriteLine("[dynamic] skipped: this host is not Windows, so the WSLC payload is never loaded."); + Console.WriteLine( + "[dynamic] (a real build would report the 'wsl' backend as unavailable here and fall through to Docker)." + ); + return Task.FromResult(0); + } + + var projection = Path.Combine(payloadDir, ProjectionFile); + if (!File.Exists(projection)) + { + Console.WriteLine($"[dynamic] FAILED: projection not found at {projection}"); + return Task.FromResult(1); + } + + try + { + var context = new PayloadLoadContext(payloadDir); + context.Resolving += (_, name) => context.TryLoad(name); + + var assembly = context.LoadFromAssemblyPath(projection); + Console.WriteLine($"[dynamic] loaded assembly: {assembly.FullName}"); + + var service = assembly.GetType(ServiceType, throwOnError: true)!; + var version = service + .GetMethod("GetVersion", BindingFlags.Public | BindingFlags.Static)! + .Invoke(null, null); + Console.WriteLine($"[dynamic] {ServiceType}.GetVersion() = {Format(version)}"); + PrintProperties("version", version); + + var missing = service + .GetMethod("GetMissingComponents", BindingFlags.Public | BindingFlags.Static)! + .Invoke(null, null); + Console.WriteLine($"[dynamic] {ServiceType}.GetMissingComponents() = {Format(missing)}"); + var missingCount = PrintItems("missing", missing); + + // A statically-available runtime is not enough: the feature delegates real work through the + // WSLC projection, so prove a session can be created, started and released from here too. + Console.WriteLine( + missingCount == 0 + ? "[dynamic] host is WSLC-capable; probing a real session." + : "[dynamic] host is NOT WSLC-capable; skipping the session probe." + ); + var sessionOk = missingCount == 0 && ProbeSession(assembly); + + Console.WriteLine( + $"[dynamic] RESULT: {(sessionOk || missingCount != 0 ? "PASS" : "FAILED")} - a plain " + + $"{RuntimeInformation.FrameworkDescription} net10.0 process drove WSLC." + ); + return Task.FromResult(sessionOk || missingCount != 0 ? 0 : 1); + } + catch (Exception exception) + { + Console.WriteLine($"[dynamic] RESULT: FAILED - {exception.GetType().Name}: {exception.Message}"); + for (var inner = exception.InnerException; inner is not null; inner = inner.InnerException) + { + Console.WriteLine($"[dynamic] inner: {inner.GetType().Name} (0x{inner.HResult:X8}): {inner.Message}"); + } + + return Task.FromResult(1); + } + } + + static string Format(object? value) => value?.ToString() ?? ""; + + static void PrintProperties(string label, object? value) + { + if (value is null) + { + return; + } + + foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + Console.WriteLine($"[dynamic] {label}.{property.Name} = {Format(property.GetValue(value))}"); + } + } + + static int PrintItems(string label, object? value) + { + var count = 0; + if (value is System.Collections.IEnumerable items and not string) + { + foreach (var item in items) + { + Console.WriteLine($"[dynamic] {label}[*] = {Format(item)}"); + count++; + } + } + + return count; + } + + static bool ProbeSession(Assembly assembly) + { + var name = $"dynload-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..8]}"; + var storage = Path.Combine(Path.GetTempPath(), "dynload-spike", name); + object? session = null; + try + { + var settingsType = assembly.GetType("Microsoft.WSL.Containers.SessionSettings", throwOnError: true)!; + var settings = Activator.CreateInstance(settingsType, name, storage); + var sessionType = assembly.GetType("Microsoft.WSL.Containers.Session", throwOnError: true)!; + session = Activator.CreateInstance(sessionType, settings); + + sessionType.GetMethod("Start")!.Invoke(session, null); + Console.WriteLine($"[dynamic] session started: {name} (storage={storage})"); + PrintProperties("session", session); + + sessionType.GetMethod("Terminate")!.Invoke(session, null); + Console.WriteLine("[dynamic] session terminated."); + return true; + } + catch (Exception exception) + { + Console.WriteLine($"[dynamic] session probe FAILED: {exception.GetType().Name}: {exception.Message}"); + for (var inner = exception.InnerException; inner is not null; inner = inner.InnerException) + { + Console.WriteLine($"[dynamic] inner: {inner.GetType().Name} (0x{inner.HResult:X8}): {inner.Message}"); + } + + return false; + } + finally + { + (session as IDisposable)?.Dispose(); + TryDelete(storage); + } + } + + static void TryDelete(string directory) + { + try + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, recursive: true); + } + } + catch (Exception exception) + { + Console.WriteLine($"[dynamic] (cleanup) could not remove '{directory}': {exception.Message}"); + } + } +} + +/// +/// A dedicated load context so the projection and its Windows SDK dependencies resolve from the payload +/// folder (and the native wslcsdk.dll loads from there too), independently of the host app. +/// +internal sealed class PayloadLoadContext(string directory) : AssemblyLoadContext("wslc-payload", isCollectible: true) +{ + readonly string _directory = directory; + + public Assembly? TryLoad(AssemblyName name) + { + var candidate = Path.Combine(_directory, $"{name.Name}.dll"); + return File.Exists(candidate) ? LoadFromAssemblyPath(candidate) : null; + } + + protected override Assembly? Load(AssemblyName assemblyName) => TryLoad(assemblyName); + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var candidate = Path.Combine(_directory, $"{unmanagedDllName}.dll"); + return File.Exists(candidate) ? LoadUnmanagedDllFromPath(candidate) : IntPtr.Zero; + } +} diff --git a/spikes/DynamicLoadSpike/PayloadInspector.cs b/spikes/DynamicLoadSpike/PayloadInspector.cs new file mode 100644 index 0000000..c39ae7b --- /dev/null +++ b/spikes/DynamicLoadSpike/PayloadInspector.cs @@ -0,0 +1,106 @@ +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace DynamicLoadSpike; + +/// +/// Reads the WSLC projection's metadata without loading it, so the probe can report the projection's +/// real target framework and the exact dependency set (name + version) that a runtime payload has to +/// ship. Runs on any OS. +/// +internal static class PayloadInspector +{ + public static void Inspect(string path) + { + if (!File.Exists(path)) + { + Console.WriteLine($"[static] payload assembly not found: {path}"); + return; + } + + using var stream = File.OpenRead(path); + using var pe = new PEReader(stream); + if (!pe.HasMetadata) + { + Console.WriteLine("[static] payload has no CLI metadata (unexpected for a managed projection)."); + return; + } + + var md = pe.GetMetadataReader(); + var definition = md.GetAssemblyDefinition(); + Console.WriteLine($"[static] assembly : {md.GetString(definition.Name)} {definition.Version}"); + Console.WriteLine($"[static] file : {path}"); + + foreach (var handle in definition.GetCustomAttributes()) + { + var attribute = md.GetCustomAttribute(handle); + var name = AttributeTypeName(md, attribute); + if ( + !name.EndsWith("TargetFrameworkAttribute", StringComparison.Ordinal) + && !name.EndsWith("TargetPlatformAttribute", StringComparison.Ordinal) + ) + { + continue; + } + + var label = name[(name.LastIndexOf('.') + 1)..]; + Console.WriteLine($"[static] {label, -24}: {StringArgument(md, attribute) ?? ""}"); + } + + Console.WriteLine("[static] references :"); + foreach (var handle in md.AssemblyReferences) + { + var reference = md.GetAssemblyReference(handle); + Console.WriteLine($"[static] ref : {md.GetString(reference.Name)} {reference.Version}"); + } + } + + static string AttributeTypeName(MetadataReader md, CustomAttribute attribute) => + attribute.Constructor.Kind switch + { + HandleKind.MemberReference => TypeName( + md, + md.GetMemberReference((MemberReferenceHandle)attribute.Constructor).Parent + ), + HandleKind.MethodDefinition => TypeName( + md, + md.GetMethodDefinition((MethodDefinitionHandle)attribute.Constructor).GetDeclaringType() + ), + _ => "", + }; + + static string TypeName(MetadataReader md, EntityHandle handle) => + handle.Kind switch + { + HandleKind.TypeReference => Define(md, md.GetTypeReference((TypeReferenceHandle)handle)), + HandleKind.TypeDefinition => Define(md, md.GetTypeDefinition((TypeDefinitionHandle)handle)), + _ => "", + }; + + static string Define(MetadataReader md, TypeReference reference) + { + var ns = md.GetString(reference.Namespace); + return string.IsNullOrEmpty(ns) ? md.GetString(reference.Name) : $"{ns}.{md.GetString(reference.Name)}"; + } + + static string Define(MetadataReader md, TypeDefinition definition) + { + var ns = md.GetString(definition.Namespace); + return string.IsNullOrEmpty(ns) ? md.GetString(definition.Name) : $"{ns}.{md.GetString(definition.Name)}"; + } + + // The first fixed argument of TargetFrameworkAttribute/TargetPlatformAttribute is a string. + static string? StringArgument(MetadataReader md, CustomAttribute attribute) + { + try + { + var reader = md.GetBlobReader(attribute.Value); + reader.ReadUInt16(); // attribute prolog 0x0001 + return reader.ReadSerializedString(); + } + catch + { + return null; + } + } +} diff --git a/spikes/DynamicLoadSpike/Program.cs b/spikes/DynamicLoadSpike/Program.cs new file mode 100644 index 0000000..18c899f --- /dev/null +++ b/spikes/DynamicLoadSpike/Program.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.Versioning; +using DynamicLoadSpike; + +// Phase 0 feasibility probe for "dynamic WSLC from a platform-neutral TFM". +// +// dotnet run --project spikes/DynamicLoadSpike +// +// The static inspection runs on any OS; the dynamic load only runs on Windows. Exit code is 0 when +// the probe reached a verdict (even if the verdict is "WSLC unavailable here") and 1 only when the +// harness itself failed, so the run is useful on Linux CI as well. + +var payload = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "payload")); + +Console.WriteLine("[env] ---------------------------------------------------------------"); +Console.WriteLine( + $"[env] tfm : {typeof(Program).Assembly.GetCustomAttribute()?.FrameworkName}" +); +Console.WriteLine($"[env] runtime : {Environment.Version}"); +Console.WriteLine($"[env] isWindows : {OperatingSystem.IsWindows()}"); +Console.WriteLine($"[env] is64bit : {Environment.Is64BitProcess}"); +Console.WriteLine($"[env] payloadDir : {payload}"); +Console.WriteLine(); + +Console.WriteLine("[static] ------------------------------------------------------------"); +PayloadInspector.Inspect(Path.Combine(payload, "wslcsdkcs.dll")); +Console.WriteLine(); + +Console.WriteLine("[dynamic] -----------------------------------------------------------"); +return await DynamicLoader.RunAsync(payload); diff --git a/spikes/PortableConsumerSpike/PortableConsumerSpike.csproj b/spikes/PortableConsumerSpike/PortableConsumerSpike.csproj new file mode 100644 index 0000000..d6e8626 --- /dev/null +++ b/spikes/PortableConsumerSpike/PortableConsumerSpike.csproj @@ -0,0 +1,52 @@ + + + + Exe + false + net10.0 + enable + enable + x64 + true + $(NoWarn);CA1031;CA1304;CA1305;CA1307;CA1310;CA1849;CA2000;CA2027;CA1707;CA1822;CA1062;CA1515;CA1861;CA1819;IDE0040;IDE0060;IDE0074;IDE0007;IDE0305;CS4014 + + + + + + + + + + $(MSBuildThisFileDirectory)../../src/src/Wsl/bin/$(Configuration)/net10.0-windows10.0.19041.0/ + $(NuGetPackageRoot)microsoft.wsl.containers/3.0.1 + $(NuGetPackageRoot)microsoft.windows.sdk.net.ref/10.0.26100.80 + + + + + + + + + + + + + + + diff --git a/spikes/PortableConsumerSpike/Program.cs b/spikes/PortableConsumerSpike/Program.cs new file mode 100644 index 0000000..8b0a39d --- /dev/null +++ b/spikes/PortableConsumerSpike/Program.cs @@ -0,0 +1,50 @@ +using Purview.Containers; +using Purview.Containers.Waiting; +using Purview.Containers.Wsl; + +// Phase 1 acceptance probe: this project is a plain net10.0 consumer (no Windows target framework). +// Everything below is the documented, backend-neutral API - the "auto" story the feature exists for. + +Console.WriteLine($"[env] tfm : {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}"); +Console.WriteLine($"[env] isWindows: {OperatingSystem.IsWindows()}"); +Console.WriteLine(); + +// A package consumer gets this from the generated module initializer; a project-reference consumer +// registers explicitly. +ContainerBackends.Register(WslContainerBackend.Create()); + +var info = await WslContainerRuntime.Instance.GetInfoAsync(); +Console.WriteLine($"[host] wsl available={info.IsAvailable} compatible={info.IsCompatible} version={info.Version}"); +foreach (var missing in info.MissingComponents) +{ + Console.WriteLine($"[host] missing: {missing}"); +} + +foreach (var backend in await ContainerBackends.ProbeAllAsync()) +{ + Console.WriteLine($"[probe] {backend.Name}: usable={backend.IsUsable} version={backend.Version}"); +} + +if (!info.IsAvailable || !info.IsCompatible) +{ + Console.WriteLine("[result] WSLC is not usable on this host; auto-selection would fall through to Docker."); + return 0; +} + +Console.WriteLine(); + +await using var container = new ContainerBuilder() + .WithImage("alpine:3.19") + .WithCommand("/bin/sh", "-c", "echo ready && sleep 20") + .WithPortBinding(8080, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForLogMessage("ready")) + .Build(); + +await container.StartAsync(); + +Console.WriteLine($"[container] name : {container.Name}"); +Console.WriteLine($"[container] id : {container.Id[..12]}"); +Console.WriteLine($"[container] port : {container.GetMappedPublicPort(8080)} -> 8080"); +Console.WriteLine($"[container] logs : {(await container.GetLogsAsync()).Trim()}"); +Console.WriteLine("[result] PASS - a net10.0 project ran a real WSLC container through the portable facade."); +return 0; diff --git a/src/src/Azurite/Sdk/README.md b/src/src/Azurite/Sdk/README.md index acb818e..4ee789a 100644 --- a/src/src/Azurite/Sdk/README.md +++ b/src/src/Azurite/Sdk/README.md @@ -13,10 +13,12 @@ See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/bl ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/Containers/Container.cs b/src/src/Containers/Container.cs index 8f658d4..db042f8 100644 --- a/src/src/Containers/Container.cs +++ b/src/src/Containers/Container.cs @@ -5,9 +5,6 @@ namespace Purview.Containers; /// resolved when the container starts, so a builder can be configured without knowing which runtime will /// run it. /// -public class Container : ContainerBase -{ - /// Creates a container for the configuration, optionally bound to a specific backend. - public Container(IContainerConfiguration configuration, IContainerBackend? backend = null) - : base(configuration, backend) { } -} +/// Creates a container for the configuration, optionally bound to a specific backend. +public class Container(IContainerConfiguration configuration, IContainerBackend? backend = null) + : ContainerBase(configuration, backend) { } diff --git a/src/src/Containers/ContainerBackendSelection.cs b/src/src/Containers/ContainerBackendSelection.cs index 7a97bc8..3922b7b 100644 --- a/src/src/Containers/ContainerBackendSelection.cs +++ b/src/src/Containers/ContainerBackendSelection.cs @@ -34,15 +34,10 @@ public static ContainerBackendSelection Named(string name) /// Parses a configuration value. null, empty, whitespace and auto (case-insensitive) /// mean automatic detection; any other value names a backend. /// - public static ContainerBackendSelection Parse(string? value) - { - if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "auto", StringComparison.OrdinalIgnoreCase)) - { - return Auto; - } - - return Named(value); - } + public static ContainerBackendSelection Parse(string? value) => + string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "auto", StringComparison.OrdinalIgnoreCase) + ? Auto + : Named(value); /// Reads the selection from the PURVIEW_CONTAINERS_BACKEND environment variable. public static ContainerBackendSelection FromEnvironment() => diff --git a/src/src/Containers/ContainerBackends.cs b/src/src/Containers/ContainerBackends.cs index 8b43505..24f2395 100644 --- a/src/src/Containers/ContainerBackends.cs +++ b/src/src/Containers/ContainerBackends.cs @@ -18,8 +18,9 @@ namespace Purview.Containers; /// The backend that actually runs is chosen by using this precedence: /// a backend pinned with , then the requested /// (set in code or read from ), then -/// automatic probing of every registered backend. A named backend never falls back to another one: the -/// failure carries that backend's own diagnostics. +/// automatic probing of every registered backend in +/// order (lowest first; registration order breaks ties). A named backend never falls back to another one: +/// the failure carries that backend's own diagnostics. /// /// public static class ContainerBackends @@ -166,7 +167,7 @@ public static async Task> ProbeAllAsync( CancellationToken cancellationToken = default ) { - List probes = new(); + List probes = []; foreach (var backend in All) { probes.Add(await ProbeAsync(backend, cancellationToken).ConfigureAwait(false)); @@ -213,6 +214,7 @@ CancellationToken cancellationToken ); } + // The pinned backend is usable, so return it even if another backend would also work. return pinned; } @@ -235,11 +237,12 @@ CancellationToken cancellationToken ); } + // The named backend is usable, so return it even if another backend would also work. return named; } StringBuilder report = new("No usable container backend was found (selection: auto)."); - foreach (var backend in registered) + foreach (var backend in OrderedForAutoSelection(registered)) { var info = await ProbeAsync(backend, cancellationToken).ConfigureAwait(false); report.Append(Environment.NewLine).Append(" ").Append(Describe(info)); @@ -259,6 +262,15 @@ CancellationToken cancellationToken throw new ContainerBackendUnavailableException(report.ToString()); } + /// + /// Orders the registered backends for automatic selection: lowest + /// first (backends without the interface count + /// as 0), with registration order preserved for ties, so the WSL Containers backend is preferred over + /// Docker on a machine that can run both. + /// + static IEnumerable OrderedForAutoSelection(IContainerBackend[] registered) => + registered.OrderBy(backend => backend is IContainerBackendPreference preference ? preference.AutoPriority : 0); + static async Task ProbeAsync(IContainerBackend backend, CancellationToken cancellationToken) { try @@ -288,6 +300,7 @@ static string Describe(ContainerBackendInfo info) + $"({string.Join("; ", info.MissingComponents)})"; } + // The backend is available and compatible, so the version is meaningful. return $"{info.Name}: available, version {info.Version}"; } } diff --git a/src/src/Containers/ContainerBase.cs b/src/src/Containers/ContainerBase.cs index 38886bf..84f37f2 100644 --- a/src/src/Containers/ContainerBase.cs +++ b/src/src/Containers/ContainerBase.cs @@ -7,8 +7,6 @@ namespace Purview.Containers; /// public abstract class ContainerBase : IContainer { - readonly IContainerConfiguration _configuration; - readonly IContainerBackend? _backend; IContainer? _container; int _started; int _disposed; @@ -21,8 +19,8 @@ public abstract class ContainerBase : IContainer protected ContainerBase(IContainerConfiguration configuration, IContainerBackend? backend = null) { ArgumentNullException.ThrowIfNull(configuration); - _configuration = configuration; - _backend = backend; + Configuration = configuration; + Backend = backend; Name = ContainerName.Generate(configuration); } @@ -30,10 +28,10 @@ protected ContainerBase(IContainerConfiguration configuration, IContainerBackend /// The backend that will create the container, or null to resolve it on /// . /// - protected IContainerBackend? Backend => _backend; + protected IContainerBackend? Backend { get; } /// The immutable configuration this container was built from. - protected IContainerConfiguration Configuration => _configuration; + protected IContainerConfiguration Configuration { get; } /// The started backend container. /// The container has not been started. @@ -50,7 +48,7 @@ protected ContainerBase(IContainerConfiguration configuration, IContainerBackend public ContainerState State => _container?.State ?? ContainerState.Created; /// - public string Image => _configuration.Image; + public string Image => Configuration.Image; /// public virtual async Task StartAsync(CancellationToken cancellationToken = default) @@ -61,7 +59,7 @@ public virtual async Task StartAsync(CancellationToken cancellationToken = defau return; } - var backend = _backend ?? await ContainerBackends.ResolveAsync(cancellationToken).ConfigureAwait(false); + var backend = Backend ?? await ContainerBackends.ResolveAsync(cancellationToken).ConfigureAwait(false); _container = backend.CreateContainer(WithResolvedName()); await _container.StartAsync(cancellationToken).ConfigureAwait(false); } @@ -128,6 +126,6 @@ public virtual async ValueTask DisposeAsync() /// IContainerConfiguration WithResolvedName() { - return _configuration is ContainerConfiguration concrete ? concrete with { Name = Name } : _configuration; + return Configuration is ContainerConfiguration concrete ? concrete with { Name = Name } : Configuration; } } diff --git a/src/src/Containers/IContainerBackendPreference.cs b/src/src/Containers/IContainerBackendPreference.cs new file mode 100644 index 0000000..6c598bf --- /dev/null +++ b/src/src/Containers/IContainerBackendPreference.cs @@ -0,0 +1,26 @@ +namespace Purview.Containers; + +/// +/// Optional backend metadata consulted by automatic backend selection. +/// +/// +/// +/// When several registered backends are usable, probes them +/// in order (lowest first) and returns the first usable one. A backend that +/// does not implement this interface is treated as priority 0, and equal priorities keep +/// registration order, so this is purely additive: existing backends behave exactly as before. +/// +/// +/// It exists so a documented preference — "on a machine that can run both, prefer WSL Containers over +/// Docker" — is expressed by the backends themselves rather than by the order NuGet happens to import +/// their buildTransitive props. A pinned instance or a named selection always overrides it. +/// +/// +public interface IContainerBackendPreference +{ + /// + /// Preference for automatic selection: a lower value wins. Only consulted for automatic + /// (auto) selection. + /// + int AutoPriority { get; } +} diff --git a/src/src/Containers/Sdk/README.md b/src/src/Containers/Sdk/README.md index 116ef9e..f038968 100644 --- a/src/src/Containers/Sdk/README.md +++ b/src/src/Containers/Sdk/README.md @@ -56,8 +56,8 @@ callers that want to apply their own policy. ## Requirements - **.NET 10 or later**, any platform (this package is portable). -- A **backend package**: `Purview.Containers.Wsl` (Windows, .NET 11) or `Purview.Containers.Docker` - (Docker Engine reachable from the test host). +- A **backend package**: `Purview.Containers.Wsl` (WSLC on a Windows host, Docker elsewhere) or + `Purview.Containers.Docker` (Docker Engine reachable from the test host). ## Related diff --git a/src/src/Containers/Waiting/WaitContext.cs b/src/src/Containers/Waiting/WaitContext.cs index bb1efa3..c0c6653 100644 --- a/src/src/Containers/Waiting/WaitContext.cs +++ b/src/src/Containers/Waiting/WaitContext.cs @@ -1,30 +1,25 @@ namespace Purview.Containers.Waiting; /// Readiness-check context: the container plus resolved runtime state (ports, IP). -public sealed class WaitContext +/// Creates the readiness context for a started container. +public sealed class WaitContext( + IContainer container, + IReadOnlyDictionary portMappings, + string? networkIp +) { - readonly IReadOnlyDictionary _portMappings; - - /// Creates the readiness context for a started container. - public WaitContext(IContainer container, IReadOnlyDictionary portMappings, string? networkIp) - { - Container = container; - _portMappings = portMappings; - NetworkIp = networkIp; - } - /// The container being checked. - public IContainer Container { get; } + public IContainer Container { get; } = container; /// The container bridge IP (Bridged networking only), if known. - public string? NetworkIp { get; } + public string? NetworkIp { get; } = networkIp; /// Returns the host port mapped to , or null when not mapped. public int? GetHostPort(ushort containerPort) { - return _portMappings.TryGetValue(containerPort, out var hostPort) ? hostPort : null; + return portMappings.TryGetValue(containerPort, out var hostPort) ? hostPort : null; } /// The first mapped host port, or null when no ports are mapped. - public int? FirstHostPort => _portMappings.Count > 0 ? _portMappings.Values.First() : null; + public int? FirstHostPort => portMappings.Count > 0 ? portMappings.Values.First() : null; } diff --git a/src/src/Docker/DockerContainer.cs b/src/src/Docker/DockerContainer.cs index 4180551..8e085a5 100644 --- a/src/src/Docker/DockerContainer.cs +++ b/src/src/Docker/DockerContainer.cs @@ -93,7 +93,7 @@ public async Task ExecAsync( // Testcontainers exposes a single argv exec overload, so a requested working directory is applied // by executing through the shell. var argv = options?.WorkingDirectory is string workingDirectory - ? new[] { "sh", "-c", $"cd {Quote(workingDirectory)} && {string.Join(' ', command.Select(Quote))}" } + ? ["sh", "-c", $"cd {Quote(workingDirectory)} && {string.Join(' ', command.Select(Quote))}"] : command; var result = await Handle.ExecAsync(argv, timeoutSource.Token).ConfigureAwait(false); @@ -213,6 +213,6 @@ static ContainerState ToState(TcStates state) => TcStates.Created => ContainerState.Created, TcStates.Running or TcStates.Paused or TcStates.Restarting => ContainerState.Running, TcStates.Exited or TcStates.Dead => ContainerState.Exited, - _ => ContainerState.Invalid, + TcStates.Undefined or _ => ContainerState.Invalid, }; } diff --git a/src/src/Docker/DockerContainerBackend.cs b/src/src/Docker/DockerContainerBackend.cs index 54d12f9..1f440fd 100644 --- a/src/src/Docker/DockerContainerBackend.cs +++ b/src/src/Docker/DockerContainerBackend.cs @@ -9,11 +9,17 @@ namespace Purview.Containers.Docker; /// Docker Engine inside WSL2, a remote daemon, or the daemon a CI runner provides. It drives the daemon /// through Testcontainers, so the resource reaper (Ryuk) still cleans up after a crashed test host. /// -public sealed class DockerContainerBackend : IContainerBackend +public sealed class DockerContainerBackend : IContainerBackend, IContainerBackendPreference { /// Stable backend identifier. public string Name => "docker"; + /// + /// Automatic selection preference: a higher value than WSLC, so a machine that can run both prefers + /// WSL Containers. + /// + public int AutoPriority => 100; + /// Factory used by the generated backend registration. public static DockerContainerBackend Create() => new(); @@ -65,7 +71,7 @@ static TcContainerBuilder CreateBuilder(IContainerConfiguration configuration) { Images.PullPolicy.Always => TcPullPolicy.Always, Images.PullPolicy.Never => TcPullPolicy.Never, - _ => TcPullPolicy.Missing, + Images.PullPolicy.Missing or _ => TcPullPolicy.Missing, } ); diff --git a/src/src/MsSql/Sdk/README.md b/src/src/MsSql/Sdk/README.md index f233742..7072772 100644 --- a/src/src/MsSql/Sdk/README.md +++ b/src/src/MsSql/Sdk/README.md @@ -12,10 +12,12 @@ connection-string generation and readiness probing. ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/MySql/Sdk/README.md b/src/src/MySql/Sdk/README.md index 668e47e..236e196 100644 --- a/src/src/MySql/Sdk/README.md +++ b/src/src/MySql/Sdk/README.md @@ -12,10 +12,12 @@ connection-string generation. ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/Nats/Sdk/README.md b/src/src/Nats/Sdk/README.md index e3b295e..cc712c7 100644 --- a/src/src/Nats/Sdk/README.md +++ b/src/src/Nats/Sdk/README.md @@ -12,10 +12,12 @@ package — bring your own client. ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/PostgreSql/Sdk/README.md b/src/src/PostgreSql/Sdk/README.md index e9ca121..50dfa06 100644 --- a/src/src/PostgreSql/Sdk/README.md +++ b/src/src/PostgreSql/Sdk/README.md @@ -12,10 +12,12 @@ See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/bl ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/RabbitMq/Sdk/README.md b/src/src/RabbitMq/Sdk/README.md index 358db1d..2e88061 100644 --- a/src/src/RabbitMq/Sdk/README.md +++ b/src/src/RabbitMq/Sdk/README.md @@ -12,10 +12,12 @@ bring your own client. ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/Redis/Sdk/README.md b/src/src/Redis/Sdk/README.md index 40f0e87..c2f68f0 100644 --- a/src/src/Redis/Sdk/README.md +++ b/src/src/Redis/Sdk/README.md @@ -12,10 +12,12 @@ package — bring your own client. ## Requirements - **WSL Containers backend:** Windows 10/11 with WSL Containers (`wsl --install --no-distribution`), and a - consuming project that is a .NET 11 project targeting Windows specifically - (`net11.0-windows10.0.19041.0`, x64 or arm64). The `Purview.Containers.Wsl` package is Windows-only and - supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget`, rejecting an - unsupported consumer with `PCC0001`/`PCC0002` — see the + `.NET 10` or later project. A platform-neutral `net10.0` project binds the portable facade and gets + automatic WSLC-or-Docker selection; a Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. The + `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for + `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build + host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. diff --git a/src/src/Wsl/Sdk/README.md b/src/src/Wsl/Sdk/README.md index db820ce..176cd5b 100644 --- a/src/src/Wsl/Sdk/README.md +++ b/src/src/Wsl/Sdk/README.md @@ -9,25 +9,32 @@ Testcontainers dependency. dotnet add package Purview.Containers.Wsl ``` -Reference this package (or a service module) and every container the abstractions create runs on WSLC; it -registers itself as the `wsl` backend in the consuming assembly. - -> **Running in CI, or on a machine without WSL Containers?** The same tests run on Docker through -> [`Purview.Containers.Docker`](https://www.nuget.org/packages/Purview.Containers.Docker). Select a backend -> with `PURVIEW_CONTAINERS_BACKEND=wsl|docker` or `ContainerBackends.Use(...)` — see +Reference this package (or a service module) and containers run on WSLC. The package is +**multi-target**: a `net10.0` build (a portable facade) and a `net10.0-windows10.0.19041.0` build (the +implementation). A Windows-targeting project binds the implementation; a `net10.0` project binds the +facade, which loads the implementation at run time on a Windows host and reports `wsl` as unavailable +elsewhere — so the **same** test project runs on WSLC on your Windows machine and on +[`Purview.Containers.Docker`](https://www.nuget.org/packages/Purview.Containers.Docker) in Linux CI +with no configuration change. It registers itself as the `wsl` backend in the consuming assembly. + +> **Running in CI, or on a machine without WSL Containers?** With `auto` (the default) the backend is +> reported unavailable and selection falls through to Docker. Pin instead with +> `PURVIEW_CONTAINERS_BACKEND=wsl|docker` or `ContainerBackends.Use(...)` — see > [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md). ## Requirements - Windows 10/11 with **WSL Containers** (`wsl --install --no-distribution`), verified against WSL 3.0.1.0. -- **A .NET 11 project targeting Windows specifically** — `net11.0-windows10.0.19041.0`, x64 or arm64. - A consuming project that targets anything else fails the build with `PCC0001` (target framework) or - `PCC0002` (platform), and without the packages' MSBuild defaults a stale +- **A .NET 10 project.** The recommended target framework is platform-neutral (`net10.0`): it binds the + facade and gives you automatic WSLC-or-Docker selection. A Windows target framework + (`net10.0-windows10.0.19041.0`, x64 or arm64) binds the implementation directly. Anything older than + .NET 10, or a Windows TFM below Windows 10.0.19041.0, fails the build with `PCC0001`; a 32-bit + Windows consumer fails with `PCC0002`; and without the packages' MSBuild defaults a stale `WindowsSdkPackageVersion` fails with `CS1705`. - This package supplies the `buildTransitive` defaults for `WindowsSdkPackageVersion` and - `PlatformTarget` that every module package inherits, so a consumer usually only chooses a target - framework. The full contract, the error reference and the `EnableWindowsTargeting` workaround for - non-Windows CI agents are in the + `PlatformTarget` that every module package inherits, and (for a platform-neutral consumer on a Windows + build host) copies the Windows implementation payload next to the output. The full contract, the error + reference and the `EnableWindowsTargeting` workaround for non-Windows CI agents are in the [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). - Verify the host with `wsl --version` and `wslc version`. The library never installs or updates WSL itself; `WslContainerRuntime.GetInfoAsync()` reports what is missing. diff --git a/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props index e0f58b3..9bf81c7 100644 --- a/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props +++ b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props @@ -9,7 +9,7 @@ Why: Microsoft.WSL.Containers 3.0.1 ships a C#/WinRT projection built against Microsoft.Windows.SDK.NET 10.0.26100.79. The Windows targeting pack resolved for - net11.0-windows10.0.19041.0 is older (10.0.19041.x), which fails to compile with CS1705 + a net10.0-windows10.0.19041.0 target is older (10.0.19041.x), which fails to compile with CS1705 (the projection "uses 'Microsoft.Windows.SDK.NET' which has a higher version than referenced assembly"). Pinning 10.0.26100.80 (the nearest published version) resolves it. NOTE: NuGet only auto-imports buildTransitive/.props and buildTransitive/.targets. diff --git a/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets index 94ee50c..394f45a 100644 --- a/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets +++ b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.targets @@ -22,16 +22,28 @@ NOTE: NuGet only auto-imports buildTransitive/.props and buildTransitive/.targets. --> - + + x64 - <_PurviewContainersWslPlatformVersionSupported + <_PurviewContainersWslIsWindows Condition="'$(TargetPlatformIdentifier)' == 'Windows'" + >true + <_PurviewContainersWslWindowsPlatformOk Condition="'$(TargetPlatformVersion)' != '' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetPlatformVersion), '10.0.19041.0'))" - >true - <_PurviewContainersWslTargetFrameworkSupported - Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '11.0')) AND '$(TargetPlatformIdentifier)' == 'Windows' AND '$(_PurviewContainersWslPlatformVersionSupported)' == 'true'" - >true + >true + <_PurviewContainersWslWindowsOk + Condition="'$(_PurviewContainersWslIsWindows)' == 'true' AND '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '10.0')) AND '$(_PurviewContainersWslWindowsPlatformOk)' == 'true'" + >true + <_PurviewContainersWslPortableOk + Condition="'$(_PurviewContainersWslIsWindows)' != 'true' AND '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '10.0'))" + >true + <_PurviewContainersWslSupported + Condition="'$(_PurviewContainersWslWindowsOk)' == 'true' OR '$(_PurviewContainersWslPortableOk)' == 'true'" + >true @@ -60,15 +80,50 @@ Condition="'$(TargetFramework)' != ''" > + <_PurviewContainersWslIsWindows Condition="'$(TargetPlatformIdentifier)' == 'Windows'" + >true <_PurviewContainersWslPlatformTargetSupported - Condition="'$(PlatformTarget)' == 'x64' OR '$(PlatformTarget)' == 'arm64' OR ('$(PlatformTarget)' == '' AND '$(RuntimeIdentifier)' != '')" + Condition="'$(_PurviewContainersWslIsWindows)' != 'true' OR '$(PlatformTarget)' == 'x64' OR '$(PlatformTarget)' == 'arm64' OR ('$(PlatformTarget)' == '' AND '$(RuntimeIdentifier)' != '')" >true + + + + + + <_PurviewContainersWslPayloadRid + Condition="'$(RuntimeIdentifier)' == 'win-arm64' OR '$(PlatformTarget)' == 'arm64'" + >win-arm64 + <_PurviewContainersWslPayloadRid Condition="'$(_PurviewContainersWslPayloadRid)' == ''" + >win-x64 + <_PurviewContainersWslPayloadSource>$(MSBuildThisFileDirectory)..\payload\$(_PurviewContainersWslPayloadRid)\ + + + + <_PurviewContainersWslPayload Include="$(_PurviewContainersWslPayloadSource)*.*" /> + + + diff --git a/src/src/Wsl/Wsl.csproj b/src/src/Wsl/Wsl.csproj index 1d7cc17..43185d4 100644 --- a/src/src/Wsl/Wsl.csproj +++ b/src/src/Wsl/Wsl.csproj @@ -1,21 +1,140 @@ true - - net11.0-windows10.0.19041.0 - x64 - 10.0.26100.80 + + + net10.0;net10.0-windows10.0.19041.0 WSL Containers backend for Purview.Containers: WSLC-native throwaway Linux containers for .NET integration testing. wsl;containers;linux;windows;testing;integration MIT Purview + + $(NoWarn);NU5100 + + + + + x64 + 10.0.26100.80 + + + + + + + + + + + + + + + + + + + + + + + + + + $(TargetsForTfmSpecificContentInPackage);PurviewContainersWslAddPayload + + + + + <_WslWindowsBinDir>$(MSBuildThisFileDirectory)bin/$(Configuration)/net10.0-windows10.0.19041.0/ + <_WslcPayloadSource>$(NuGetPackageRoot)microsoft.wsl.containers/3.0.1 + <_WindowsSdkPayloadSource>$(NuGetPackageRoot)microsoft.windows.sdk.net.ref/10.0.26100.80 + + + + + + + + + + + + + + + + diff --git a/src/src/Wsl/WslContainerBackend.Facade.cs b/src/src/Wsl/WslContainerBackend.Facade.cs new file mode 100644 index 0000000..ca278c3 --- /dev/null +++ b/src/src/Wsl/WslContainerBackend.Facade.cs @@ -0,0 +1,65 @@ +namespace Purview.Containers.Wsl; + +/// +/// The WSL Containers (WSLC) backend as seen from a platform-neutral (net10.0) consumer. +/// +/// +/// +/// On a Windows host this facade loads the Windows build of the backend from the local payload and +/// delegates to it; the container it returns is a real WSLC-backed . On any +/// other host - or when the payload is absent - it reports wsl as unavailable, so +/// auto-selection moves on to the next registered backend (Docker). +/// +/// +/// A Windows-targeting consumer never sees this type: it binds the Windows build of the assembly, +/// where WslContainerBackend is the implementation itself. +/// +/// +public sealed class WslContainerBackend : IContainerBackend, IContainerBackendPreference +{ + /// Stable backend identifier. + public string Name => "wsl"; + + /// + /// Automatic selection preference: WSLC is preferred over Docker on a machine that can run both. + /// + public int AutoPriority => 0; + + /// Factory used by the generated backend registration. + public static WslContainerBackend Create() => new(); + + /// + public IContainer CreateContainer(IContainerConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + return Resolve().CreateContainer(configuration); + } + + /// + public async Task GetInfoAsync(CancellationToken cancellationToken = default) + { + var backend = WslPayload.TryCreateBackend(); + if (backend is null) + { + return Unavailable(WslPayload.FailureReason); + } + + try + { + return await backend.GetInfoAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + return Unavailable($"{exception.GetType().Name}: {exception.Message}"); + } + } + + static IContainerBackend Resolve() => + WslPayload.TryCreateBackend() + ?? throw new WslContainerPrerequisiteException( + $"The WSL Containers backend cannot run here. {WslPayload.FailureReason}" + ); + + static ContainerBackendInfo Unavailable(string reason) => + new("wsl", IsAvailable: false, IsCompatible: false, Version: string.Empty, MissingComponents: [reason]); +} diff --git a/src/src/Wsl/WslContainerBackend.cs b/src/src/Wsl/WslContainerBackend.cs index 2c627c2..4a387f9 100644 --- a/src/src/Wsl/WslContainerBackend.cs +++ b/src/src/Wsl/WslContainerBackend.cs @@ -6,10 +6,8 @@ namespace Purview.Containers.Wsl; /// The WSL Containers (WSLC) backend: it creates containers on the process-wide WSLC session and reports /// the installed WSL Containers components. No Docker installation is involved. /// -public sealed class WslContainerBackend : IContainerBackend +public sealed class WslContainerBackend : IContainerBackend, IContainerBackendPreference { - readonly IContainerRuntime? _runtime; - /// Creates the backend over the process-wide . public WslContainerBackend() { } @@ -17,16 +15,21 @@ public WslContainerBackend() { } public WslContainerBackend(IContainerRuntime runtime) { ArgumentNullException.ThrowIfNull(runtime); - _runtime = runtime; + Runtime = runtime; } /// Stable backend identifier. public string Name => "wsl"; + /// + /// Automatic selection preference: WSLC is preferred over Docker on a machine that can run both. + /// + public int AutoPriority => 0; + /// Factory used by the generated backend registration. public static WslContainerBackend Create() => new(); - IContainerRuntime Runtime => _runtime ?? WslContainerRuntime.Instance; + IContainerRuntime Runtime => field ?? WslContainerRuntime.Instance; /// public IContainer CreateContainer(IContainerConfiguration configuration) diff --git a/src/src/Wsl/WslContainerRuntime.Facade.cs b/src/src/Wsl/WslContainerRuntime.Facade.cs new file mode 100644 index 0000000..03a941e --- /dev/null +++ b/src/src/Wsl/WslContainerRuntime.Facade.cs @@ -0,0 +1,70 @@ +namespace Purview.Containers.Wsl; + +/// +/// The process-wide WSL Containers runtime as seen from a platform-neutral (net10.0) consumer. +/// +/// +/// +/// The facade forwards host diagnostics to the Windows implementation loaded from the payload and +/// reports "unavailable" when this host cannot run WSLC. It exists mainly for the documented host +/// check (WslContainerRuntime.Instance.GetInfoAsync()); container work always goes through the +/// backend-neutral builders. +/// +/// +/// Session-level access () is not exposed from the facade, because the +/// WSLC session handle is a Windows-only type. A Windows-targeting consumer keeps the full runtime. +/// +/// +public sealed class WslContainerRuntime : IContainerRuntime +{ + /// + /// Environment variable that overrides the session storage path for the Windows implementation. + /// + public const string StoragePathEnvironmentVariable = "PURVIEW_CONTAINERS_STORAGE_PATH"; + + /// The pre-rename storage path variable, still honoured as a fallback. + public const string LegacyStoragePathEnvironmentVariable = "WSL_CONTAINERS_STORAGE_PATH"; + + static readonly Lazy InstanceHolder = new(() => new WslContainerRuntime()); + + /// The default process-wide runtime. + public static WslContainerRuntime Instance => InstanceHolder.Value; + + /// Creates a runtime with default options. + public WslContainerRuntime() { } + + /// Creates a runtime with the given options (retained for API parity; applied when WSLC runs). + public WslContainerRuntime(WslContainerRuntimeOptions options) => + Options = options ?? WslContainerRuntimeOptions.Default; + + /// The options this runtime was created with, if any. + public WslContainerRuntimeOptions? Options { get; } + + /// + public async Task GetInfoAsync(CancellationToken cancellationToken = default) + { + var backend = WslPayload.TryCreateBackend(); + if (backend is null) + { + return new WslContainerRuntimeInfo( + Version: string.Empty, + MissingComponents: [WslPayload.FailureReason], + IsAvailable: false, + IsCompatible: false + ); + } + + var info = await backend.GetInfoAsync(cancellationToken).ConfigureAwait(false); + return new WslContainerRuntimeInfo(info.Version, info.MissingComponents, info.IsAvailable, info.IsCompatible); + } + + /// + public Task GetSessionAsync(CancellationToken cancellationToken = default) => + throw new WslContainerPrerequisiteException( + "Direct WSLC session access is only available from a Windows target framework " + + "(net10.0-windows10.0.19041.0 or later). Use the backend-neutral container API instead." + ); + + /// + public ValueTask DisposeAsync() => ValueTask.CompletedTask; +} diff --git a/src/src/Wsl/WslPayload.cs b/src/src/Wsl/WslPayload.cs new file mode 100644 index 0000000..f036785 --- /dev/null +++ b/src/src/Wsl/WslPayload.cs @@ -0,0 +1,147 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Purview.Containers.Wsl; + +/// +/// Loads the Windows build of Purview.Containers.Wsl at runtime so a platform-neutral +/// (net10.0) process on a Windows host can still run containers on WSL Containers. +/// +/// +/// +/// This is the portable half of the backend. It never references Microsoft.WSL.Containers at +/// compile time; instead it loads the Windows build of this same assembly (shipped as a payload, +/// under wslc/) into a dedicated and hands back its +/// . Only the platform-neutral +/// contract crosses that boundary, so no WSLC type is reflected over. +/// +/// +/// Everywhere else - a non-Windows host, or a missing payload - the probe simply fails, so +/// ContainerBackends auto-selection falls through to Docker. That is what lets the same test +/// project run on WSLC on a developer's Windows machine and on Docker in a Linux CI job without a +/// single configuration change. +/// +/// +static class WslPayload +{ + /// Optional override for the payload folder (advanced hosting scenarios). + const string DirectoryEnvironmentVariable = "PURVIEW_CONTAINERS_WSL_PAYLOAD"; + + // The default payload folder, copied next to the app by the package's buildTransitive targets. + const string DirectoryName = "wslc"; + const string ImplementationFileName = "Purview.Containers.Wsl.dll"; + const string BackendTypeName = "Purview.Containers.Wsl.WslContainerBackend"; + + static readonly Lock Sync = new(); + static bool Attempted; + static IContainerBackend? Resolved; + static string? Failure; + + /// True when this host could possibly run WSL Containers. + internal static bool IsHostSupported => OperatingSystem.IsWindows(); + + /// + /// Returns the Windows implementation's backend, or null when this host cannot run WSLC + /// (non-Windows), the payload is absent, or it failed to load. The reason is available from + /// . + /// + internal static IContainerBackend? TryCreateBackend() + { + if (!IsHostSupported) + { + Failure = "WSL Containers requires a Windows host."; + return null; + } + + lock (Sync) + { + if (!Attempted) + { + Attempted = true; + Resolved = Create(); + } + + return Resolved; + } + } + + /// The reason the last returned null. + internal static string FailureReason => Failure ?? "The WSL Containers implementation is unavailable."; + + static IContainerBackend? Create() + { + foreach (var directory in CandidateDirectories()) + { + if (!Directory.Exists(directory)) + { + continue; + } + + var candidate = Path.Combine(directory, ImplementationFileName); + if (!File.Exists(candidate)) + { + continue; + } + + try + { + PayloadLoadContext context = new(directory); + var assembly = context.LoadFromAssemblyPath(candidate); + var factory = assembly + .GetType(BackendTypeName, throwOnError: false) + ?.GetMethod("Create", BindingFlags.Public | BindingFlags.Static); + if (factory?.Invoke(null, null) is IContainerBackend backend) + { + return backend; + } + + Failure = $"The WSL Containers implementation at '{candidate}' did not expose a backend."; + } + catch (Exception exception) when (exception is not OperationCanceledException) + { + Failure = + $"The WSL Containers implementation at '{candidate}' failed to load: " + + $"{exception.GetType().Name}: {exception.Message}"; + } + } + + Failure ??= + "The WSL Containers implementation payload was not found. Reference 'Purview.Containers.Wsl' " + + $"with the Windows payload deployed (looked in '{string.Join("', '", CandidateDirectories())}')."; + return null; + } + + static IEnumerable CandidateDirectories() + { + var configured = Environment.GetEnvironmentVariable(DirectoryEnvironmentVariable); + if (!string.IsNullOrEmpty(configured)) + { + yield return configured; + } + + yield return Path.Combine(AppContext.BaseDirectory, DirectoryName); + } +} + +/// +/// Isolates the WSLC implementation and its Windows SDK dependencies from the host app. Anything not +/// present in the payload folder (notably Purview.Containers) resolves from the default load +/// context, so the shared contract keeps a single identity. +/// +sealed class PayloadLoadContext(string directory) + : AssemblyLoadContext("Purview.Containers.Wsl.Payload", isCollectible: false) +{ + readonly string _directory = directory; + + protected override Assembly? Load(AssemblyName assemblyName) + { + var candidate = Path.Combine(_directory, $"{assemblyName.Name}.dll"); + return File.Exists(candidate) ? LoadFromAssemblyPath(candidate) : null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + var candidate = Path.Combine(_directory, $"{unmanagedDllName}.dll"); + return File.Exists(candidate) ? LoadUnmanagedDllFromPath(candidate) : IntPtr.Zero; + } +} diff --git a/src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs b/src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs index f5cf10d..a7ef770 100644 --- a/src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs +++ b/src/tests/Wsl.UnitTests/ConsumerRequirementsTests.cs @@ -4,10 +4,12 @@ namespace Purview.Containers.Wsl; /// -/// Guards the consumer contract documented in docs/wiki/Consumer-Requirements.md: every -/// package is a .NET 11 project that targets Windows specifically. A consumer cannot restore the -/// package from anything else, so a drift here has to fail the build rather than quietly -/// invalidate the documentation and the shipped buildTransitive defaults. +/// Guards the consumer contract documented in docs/wiki/Consumer-Requirements.md. The WSL +/// Containers backend is a multi-target package: a Windows build (the implementation, targeting +/// Windows specifically) and a platform-neutral build (the facade). A Windows-targeting consumer +/// binds the Windows build, which is what this project references, so a drift here has to fail the +/// build rather than quietly invalidate the documentation and the shipped buildTransitive +/// defaults. /// public class ConsumerRequirementsTests { @@ -15,12 +17,12 @@ public class ConsumerRequirementsTests static readonly string[] WindowsPlatform = ["Windows10.0.19041.0"]; [Test] - public async Task LibraryTargetsNet11() + public async Task LibraryTargetsNet10() { var targetFramework = Library.GetCustomAttribute(); await Assert.That(targetFramework).IsNotNull(); - await Assert.That(targetFramework!.FrameworkName).IsEqualTo(".NETCoreApp,Version=v11.0"); + await Assert.That(targetFramework!.FrameworkName).IsEqualTo(".NETCoreApp,Version=v10.0"); } [Test] diff --git a/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs b/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs index 32e49b4..091979b 100644 --- a/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs +++ b/src/tests/Wsl.UnitTests/ContainerBackendsTests.cs @@ -64,6 +64,43 @@ public async Task ResolveAsync_ReturnsTheFirstUsableBackend() await Assert.That(resolved.Name).IsEqualTo("one"); } + [Test] + public async Task ResolveAsync_PrefersTheLowestAutoPriority() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("deprioritized", autoPriority: 100)); + ContainerBackends.Register(new FakeBackend("preferred", autoPriority: 0)); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("preferred"); + } + + [Test] + public async Task ResolveAsync_TreatsABackendWithoutAPreferenceAsNeutral() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("deprioritized", autoPriority: 100)); + ContainerBackends.Register(new PlainBackend("neutral")); + + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("neutral"); + } + + [Test] + public async Task ResolveAsync_NamedSelectionIgnoresAutoPriority() + { + using RegistryScope scope = new(); + ContainerBackends.Register(new FakeBackend("preferred", autoPriority: 0)); + ContainerBackends.Register(new FakeBackend("deprioritized", autoPriority: 100)); + + ContainerBackends.Use(ContainerBackendSelection.Named("deprioritized")); + var resolved = await ContainerBackends.ResolveAsync(); + + await Assert.That(resolved.Name).IsEqualTo("deprioritized"); + } + [Test] public async Task ResolveAsync_SkipsAnUnusableBackend() { @@ -206,10 +243,14 @@ sealed record DerivedConfiguration : ContainerConfiguration public string Extra { get; init; } = string.Empty; } - sealed class FakeBackend(string name, bool isAvailable = true, bool isCompatible = true) : IContainerBackend + sealed class FakeBackend(string name, bool isAvailable = true, bool isCompatible = true, int autoPriority = 0) + : IContainerBackend, + IContainerBackendPreference { public string Name { get; } = name; + public int AutoPriority { get; } = autoPriority; + public int Probes { get; private set; } public IContainer CreateContainer(IContainerConfiguration configuration) => throw new NotSupportedException(); @@ -229,6 +270,17 @@ public Task GetInfoAsync(CancellationToken cancellationTok } } + /// A backend that does not implement . + sealed class PlainBackend(string name) : IContainerBackend + { + public string Name { get; } = name; + + public IContainer CreateContainer(IContainerConfiguration configuration) => throw new NotSupportedException(); + + public Task GetInfoAsync(CancellationToken cancellationToken = default) => + Task.FromResult(new ContainerBackendInfo(Name, IsAvailable: true, IsCompatible: true, "1.0", [])); + } + sealed class ThrowingBackend(string name) : IContainerBackend { public string Name { get; } = name; From db31737eb5a6682d9331847ca3eeed0a10467a9b Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 20:06:46 +0100 Subject: [PATCH 03/11] chore: bumped version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fca5f41..150ec31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "purview-wsl-containers", - "version": "1.0.0-prerelease.1", + "version": "1.0.0-prerelease.2", "license": "MIT", "author": { "name": "Kieron Lanning", @@ -14,4 +14,4 @@ "type": "git", "url": "git+https://github.com/purview-dev/wsl-containers.git" } -} \ No newline at end of file +} From 0f3af3da788b856c1a52711c47f9f801383578a5 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 20:12:23 +0100 Subject: [PATCH 04/11] feat(mssql): include the initial catalog in the connection string Match the Testcontainers SQL Server module: the generated connection string now sets Database=master by default, configurable with WithDatabase(...). Previously it omitted the catalog entirely, so consumers that expect Database=master (as Testcontainers produces) had to add it themselves. --- docs/wiki/Modules.md | 15 ++++++++++++++- src/src/MsSql/MsSql.csproj | 4 ++-- src/src/MsSql/MsSqlBuilder.cs | 14 ++++++++++++++ src/src/MsSql/MsSqlConfiguration.cs | 6 ++++++ src/src/MsSql/MsSqlContainer.cs | 1 + src/src/MsSql/Sdk/README.md | 11 ++++++----- src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs | 17 +++++++++++++++++ 7 files changed, 60 insertions(+), 8 deletions(-) diff --git a/docs/wiki/Modules.md b/docs/wiki/Modules.md index 8d7aa5e..a90ee2b 100644 --- a/docs/wiki/Modules.md +++ b/docs/wiki/Modules.md @@ -122,13 +122,26 @@ public sealed class PostgreSqlContainer : ContainerBase - Prefer client connection-string builders: `NpgsqlConnectionStringBuilder`, `SqlConnectionStringBuilder`, `UriBuilder`, etc. Avoid handcrafted escaping. - Credentials are stored as `Secret` in the module configuration; diagnostics and `ToString()` never reveal them. +Every module exposes `GetConnectionString()` with the same shape it has in Testcontainers, so test code +that leans on the Testcontainers modules ports across unchanged: + +| Module | `GetConnectionString()` | Extra accessors | +|---|---|---| +| PostgreSQL | Npgsql string: `Host`, `Port`, `Database`, `Username`, `Password` | — | +| Redis | `host:port` (e.g. `localhost:6379`) | — | +| SQL Server | `SqlConnectionStringBuilder`: `Data Source=host,port`, `Database` (default `master`, set with `WithDatabase`), `User Id=sa`, `Password`, `TrustServerCertificate=True` | — | +| MySQL | `MySqlConnectionStringBuilder`: `Server`, `Port`, `Database`, `User ID`, `Password` | — | +| RabbitMQ | `amqp://user:pass@host:port/vhost` | `GetAmqpEndpoint()`, `GetManagementEndpoint()` | +| Azurite | Azure Storage string: `DefaultEndpointsProtocol=http`, `AccountName`, `AccountKey`, `Blob/Queue/TableEndpoint` | `GetBlobEndpoint()`, `GetQueueEndpoint()`, `GetTableEndpoint()` | +| NATS | `nats://host:port` | `GetClientEndpoint()`, `GetMonitoringEndpoint()` | + ## Module status | Module | Image | Readiness | Client | Status | |---|---|---|---|---| | PostgreSQL | `postgres:17` | `pg_isready` | Npgsql | ✅ implemented (Phase 3) | | Redis | `redis:7` | `redis-cli ping` | StackExchange.Redis | ✅ implemented (Phase 4) — also usable with Valkey/Garnet via `WithImage` | -| SQL Server | `mcr.microsoft.com/mssql/server:2022-latest` | host `SqlClient` connection | Microsoft.Data.SqlClient | ✅ implemented (Phase 5) — requires `.AcceptLicense()`; session needs ≥ 2000 MB memory | +| SQL Server | `mcr.microsoft.com/mssql/server:2022-latest` | host `SqlClient` connection | Microsoft.Data.SqlClient | ✅ implemented (Phase 5) — requires `.AcceptLicense()`; the connection string defaults to `Database=master` (`WithDatabase(...)` to change it); session needs ≥ 2000 MB memory | | RabbitMQ | `rabbitmq:3-management` | log `"Server startup complete"` | RabbitMQ.Client | ✅ implemented (Phase 6) — AMQP + management endpoints | | Azurite | `mcr.microsoft.com/azure-storage/azurite` | log `"successfully listening"` | Azure.Storage.* | ✅ implemented — blob/queue/table endpoints; the well-known `devstoreaccount1` key is a placeholder in `AzuriteAccount.Key` until the consuming repo supplies it | | NATS | `nats:2` | log `"Listening for client connections"` | NATS.Client.Core | ✅ implemented — client + monitoring endpoints | diff --git a/src/src/MsSql/MsSql.csproj b/src/src/MsSql/MsSql.csproj index dbe5f2c..7d36bec 100644 --- a/src/src/MsSql/MsSql.csproj +++ b/src/src/MsSql/MsSql.csproj @@ -1,8 +1,8 @@ true - Microsoft SQL Server module for Purview.Containers: throwaway SQL Server databases on WSL Containers. - wsl;containers;sqlserver;sql;mssql;testing;integration + Microsoft SQL Server module for Purview.Containers: throwaway SQL Server databases on WSL Containers or Docker. + wsl;containers;docker;sqlserver;sql;mssql;testing;integration MIT Purview diff --git a/src/src/MsSql/MsSqlBuilder.cs b/src/src/MsSql/MsSqlBuilder.cs index dd8a622..d65c59e 100644 --- a/src/src/MsSql/MsSqlBuilder.cs +++ b/src/src/MsSql/MsSqlBuilder.cs @@ -15,6 +15,7 @@ public class MsSqlBuilder : ContainerBuilderCreates a builder with the default image. @@ -42,6 +43,17 @@ public MsSqlBuilder WithPassword(string password) return this; } + /// + /// Sets the initial catalog the generated connection string points at (default master). The + /// database is not created by the module; it must already exist on the server. + /// + public MsSqlBuilder WithDatabase(string database) + { + ArgumentException.ThrowIfNullOrWhiteSpace(database); + _database = database; + return this; + } + /// /// Explicitly accepts the SQL Server EULA (ACCEPT_EULA=Y). Required before Build; the library never /// accepts licensing terms on the caller's behalf. @@ -75,6 +87,7 @@ protected override MsSqlConfiguration BuildConfiguration() return configuration with { Password = _password, + Database = _database, AcceptLicense = _acceptLicense, Environment = environment, WaitStrategies = waitStrategies, @@ -121,6 +134,7 @@ WaitStrategy BuildReadinessWait() // 127.0.0.1 is required: WSLC maps IPv4 loopback only, and Microsoft.Data.SqlClient // hangs on the IPv6 ::1 address that 'localhost' resolves to. DataSource = $"127.0.0.1,{hostPort}", + InitialCatalog = _database, UserID = "sa", Password = _password.Value, TrustServerCertificate = true, diff --git a/src/src/MsSql/MsSqlConfiguration.cs b/src/src/MsSql/MsSqlConfiguration.cs index b19a121..678f4c2 100644 --- a/src/src/MsSql/MsSqlConfiguration.cs +++ b/src/src/MsSql/MsSqlConfiguration.cs @@ -8,6 +8,12 @@ public sealed record MsSqlConfiguration : ContainerConfiguration /// SA password (MSSQL_SA_PASSWORD). Rendered as redacted. public Secret Password { get; init; } = Secret.From("YourStrong!Passw0rd"); + /// + /// Initial catalog the generated connection string points at (default master, matching the + /// Testcontainers SQL Server module). Set with . + /// + public string Database { get; init; } = "master"; + /// True when the SQL Server EULA has been explicitly accepted. public bool AcceptLicense { get; init; } } diff --git a/src/src/MsSql/MsSqlContainer.cs b/src/src/MsSql/MsSqlContainer.cs index cd75be8..bc2fa66 100644 --- a/src/src/MsSql/MsSqlContainer.cs +++ b/src/src/MsSql/MsSqlContainer.cs @@ -21,6 +21,7 @@ public string GetConnectionString() // 127.0.0.1 is required: WSLC maps IPv4 loopback only, and Microsoft.Data.SqlClient // hangs on the IPv6 ::1 address that 'localhost' resolves to. DataSource = $"127.0.0.1,{GetMappedPublicPort(MsSqlBuilder.MsSqlPort)}", + InitialCatalog = _configuration.Database, UserID = "sa", Password = _configuration.Password.Value, TrustServerCertificate = true, diff --git a/src/src/MsSql/Sdk/README.md b/src/src/MsSql/Sdk/README.md index 7072772..8f19c99 100644 --- a/src/src/MsSql/Sdk/README.md +++ b/src/src/MsSql/Sdk/README.md @@ -18,7 +18,7 @@ connection-string generation and readiness probing. `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -48,8 +48,9 @@ await connection.OpenAsync(); | `MsSqlBuilder()` / `MsSqlBuilder(string image)` | Default image `mcr.microsoft.com/mssql/server:2022-latest`, or a custom image. | | `MsSqlBuilder.MsSqlPort` (1433) | Container port, mapped to a random host port. | | `WithPassword(string)` | Sets `MSSQL_SA_PASSWORD` (default `YourStrong!Passw0rd`); stored as a redacted `Secret`. | +| `WithDatabase(string)` | Sets the initial catalog the connection string points at (default `master`, matching Testcontainers). The database must already exist. | | `AcceptLicense()` | Sets `ACCEPT_EULA=Y`. Required — the library never accepts licensing terms on your behalf. | -| `MsSqlContainer.GetConnectionString()` | `SqlConnectionStringBuilder` connection string for the mapped host port. | +| `MsSqlContainer.GetConnectionString()` | `SqlConnectionStringBuilder` connection string for the mapped host port (`Database=master` by default, `TrustServerCertificate=True`). | ## Behaviour and constraints @@ -65,6 +66,6 @@ await connection.OpenAsync(); ## Documentation -- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. -- [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. -- [Getting Started](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md) — prerequisites and first-container walkthrough. +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Getting Started](https://github.com/purview-dev/containers/blob/main/docs/wiki/Getting-Started.md) — prerequisites and first-container walkthrough. diff --git a/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs index 4c21657..463fb2f 100644 --- a/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs +++ b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs @@ -27,6 +27,7 @@ public async Task BuildConfig_AppliesDefaults() await Assert.That(configuration.Image).IsEqualTo("mcr.microsoft.com/mssql/server:2022-latest"); await Assert.That(configuration.Password.Value).IsEqualTo("YourStrong!Passw0rd"); + await Assert.That(configuration.Database).IsEqualTo("master"); await Assert.That(configuration.AcceptLicense).IsFalse(); await Assert.That(configuration.PortBindings.Count).IsEqualTo(1); await Assert.That(configuration.PortBindings[0].ContainerPort).IsEqualTo((ushort)1433); @@ -34,6 +35,22 @@ public async Task BuildConfig_AppliesDefaults() await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); } + [Test] + public async Task WithDatabase_OverridesTheInitialCatalog() + { + var configuration = new MsSqlBuilder().WithDatabase("app").BuildConfigurationForTesting(); + + await Assert.That(configuration.Database).IsEqualTo("app"); + } + + [Test] + public async Task WithDatabase_WithABlankName_Throws() + { + MsSqlBuilder builder = new(); + + await Assert.That(() => builder.WithDatabase(" ")).Throws(); + } + [Test] public async Task BuildConfig_HonoursModuleSetters() { From adf4f7205dffecb957a4eb907bef64af2e76bc8f Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 20:12:29 +0100 Subject: [PATCH 05/11] chore: rename the repository to containers and refresh package metadata - rename every repository reference from purview-dev/wsl-containers to purview-dev/containers (package.json, mkdocs.yml, README, docs, the package READMEs and PackageProjectUrl) - describe the service modules as running on WSL Containers or Docker (they are backend-neutral) and refresh the WSL, Docker and abstractions descriptions - broaden the package tags (docker for the modules, wslc/testcontainers for the WSL backend, docker/wsl for the abstractions) - rename the npm package to purview-containers and point homepage/bugs there --- README.md | 2 +- docs/wiki/Packaging.md | 2 +- docs/wiki/index.md | 2 +- mkdocs.yml | 6 +++--- package.json | 8 ++++---- src/Directory.Build.props | 2 +- src/src/Azurite/Azurite.csproj | 4 ++-- src/src/Azurite/Sdk/README.md | 6 +++--- src/src/Containers/Containers.csproj | 4 ++-- src/src/Containers/Sdk/README.md | 2 +- src/src/Docker/Docker.csproj | 2 +- src/src/MySql/MySql.csproj | 4 ++-- src/src/MySql/Sdk/README.md | 8 ++++---- src/src/Nats/Nats.csproj | 4 ++-- src/src/Nats/Sdk/README.md | 8 ++++---- src/src/PostgreSql/PostgreSql.csproj | 4 ++-- src/src/PostgreSql/Sdk/README.md | 10 +++++----- src/src/RabbitMq/RabbitMq.csproj | 4 ++-- src/src/RabbitMq/Sdk/README.md | 10 +++++----- src/src/Redis/Redis.csproj | 4 ++-- src/src/Redis/Sdk/README.md | 8 ++++---- src/src/Wsl/Sdk/README.md | 16 ++++++++-------- src/src/Wsl/Wsl.csproj | 4 ++-- 23 files changed, 62 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 5004aac..2b3e70d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Purview.Containers [![NuGet version](https://img.shields.io/nuget/v/Purview.Containers.Wsl.svg)](https://www.nuget.org/packages/Purview.Containers.Wsl) -[![Release](https://github.com/purview-dev/wsl-containers/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/wsl-containers/actions/workflows/release.yml) +[![Release](https://github.com/purview-dev/containers/actions/workflows/release.yml/badge.svg)](https://github.com/purview-dev/containers/actions/workflows/release.yml) A Testcontainers-style library for .NET that runs throwaway Linux containers for integration testing — on **Microsoft WSL Containers (WSLC)** with no Docker installation, or on **Docker** through Testcontainers. diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index 55e41b9..7517325 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -36,7 +36,7 @@ Metadata is split by where its source of truth lives: | `version`, `repository`, `bugs` | `package.json` (applied by `Purview.BuildSdk`) | | icon, package README, **project site** | `src/Directory.Build.props`, for every packable project | -The **project site** (`PackageProjectUrl`) is `https://github.com/purview-dev/wsl-containers`, the +The **project site** (`PackageProjectUrl`) is `https://github.com/purview-dev/containers`, the repository that hosts this documentation wiki; it shows as *Project Site* on nuget.org. Repository and commit metadata come from SourceLink, so the `repository` entry in a locally packed `.nupkg` points at the branch and commit that produced it. diff --git a/docs/wiki/index.md b/docs/wiki/index.md index 6769104..2a7eb2a 100644 --- a/docs/wiki/index.md +++ b/docs/wiki/index.md @@ -1,4 +1,4 @@ -# Purview WSL Test Containers +# Purview Containers Throwaway Linux containers for .NET integration testing — Testcontainers-style APIs for **Microsoft WSL Containers (WSLC)** with no Docker installation, and for **Docker** through Testcontainers. The same test diff --git a/mkdocs.yml b/mkdocs.yml index 5151edb..ee2e8c8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ -site_name: Purview WSL Test Containers -site_description: Developer documentation for Purview WSL Test Containers -repo_url: https://github.com/purview-dev/wsl-containers +site_name: Purview Containers +site_description: Developer documentation for Purview Containers +repo_url: https://github.com/purview-dev/containers edit_uri: edit/main/docs/wiki/ docs_dir: docs/wiki diff --git a/package.json b/package.json index 150ec31..603cc1c 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,17 @@ { - "name": "purview-wsl-containers", + "name": "purview-containers", "version": "1.0.0-prerelease.2", "license": "MIT", "author": { "name": "Kieron Lanning", "url": "https://kieronlanning.dev/" }, - "homepage": "https://purview.dev/projects/wsl-containers/", + "homepage": "https://purview.dev/projects/containers/", "bugs": { - "url": "https://github.com/purview-dev/wsl-containers/issues" + "url": "https://github.com/purview-dev/containers/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/purview-dev/wsl-containers.git" + "url": "git+https://github.com/purview-dev/containers.git" } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 043be86..cf18964 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,7 @@ - https://github.com/purview-dev/wsl-containers + https://github.com/purview-dev/containers diff --git a/src/src/Azurite/Azurite.csproj b/src/src/Azurite/Azurite.csproj index be5a7f7..739cdc4 100644 --- a/src/src/Azurite/Azurite.csproj +++ b/src/src/Azurite/Azurite.csproj @@ -1,8 +1,8 @@ true - Azurite module for Purview.Containers: throwaway Azure Storage emulators (blob/queue/table) on WSL Containers. - wsl;containers;azurite;azure;storage;testing;integration + Azurite module for Purview.Containers: throwaway Azure Storage emulators (blob/queue/table) on WSL Containers or Docker. + wsl;containers;docker;azurite;azure;storage;testing;integration MIT Purview diff --git a/src/src/Azurite/Sdk/README.md b/src/src/Azurite/Sdk/README.md index 4ee789a..a00a6c2 100644 --- a/src/src/Azurite/Sdk/README.md +++ b/src/src/Azurite/Sdk/README.md @@ -8,7 +8,7 @@ dotnet add package Purview.Containers.Azurite ``` Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). -See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md). +See the [Getting Started guide](https://github.com/purview-dev/containers/blob/main/docs/wiki/Getting-Started.md). ## Requirements @@ -19,7 +19,7 @@ See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/bl `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -57,5 +57,5 @@ for the `successfully listening` log signal. Endpoints and the connection string The well-known `devstoreaccount1` key is a published constant of the emulator, but this library does not embed it: `AzuriteAccount.Key` holds a placeholder. Supply the real key in your test infrastructure before exercising authenticated operations (anonymous/local development paths are unaffected when the client does -not require the key). See [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) +not require the key). See [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) for the module contract. diff --git a/src/src/Containers/Containers.csproj b/src/src/Containers/Containers.csproj index 570d3e0..e919dd5 100644 --- a/src/src/Containers/Containers.csproj +++ b/src/src/Containers/Containers.csproj @@ -1,8 +1,8 @@ true - Backend-neutral container testing abstractions for .NET: a Testcontainers-style API that can run on WSL Containers or Docker. - containers;testing;integration;testcontainers;abstractions + Backend-neutral container testing abstractions for .NET: a Testcontainers-style API that runs the same test code on WSL Containers or Docker. + containers;testing;integration;testcontainers;abstractions;docker;wsl MIT Purview diff --git a/src/src/Containers/Sdk/README.md b/src/src/Containers/Sdk/README.md index f038968..ae38fd8 100644 --- a/src/src/Containers/Sdk/README.md +++ b/src/src/Containers/Sdk/README.md @@ -62,7 +62,7 @@ callers that want to apply their own policy. ## Related - [Purview.Containers.Wsl](https://www.nuget.org/packages/Purview.Containers.Wsl) — WSL Containers backend. -- Documentation wiki: . +- Documentation wiki: . > **Experimental.** The public API, defaults and packaging rules can change between prereleases, and > there is no production support guarantee. Pin the exact package version you build against. diff --git a/src/src/Docker/Docker.csproj b/src/src/Docker/Docker.csproj index 8fe2837..7d53bce 100644 --- a/src/src/Docker/Docker.csproj +++ b/src/src/Docker/Docker.csproj @@ -2,7 +2,7 @@ true Docker backend for Purview.Containers: throwaway containers from any reachable Docker daemon, driven by Testcontainers. - docker;containers;testing;integration;testcontainers + docker;containers;testing;integration;testcontainers;dotnet MIT Purview diff --git a/src/src/MySql/MySql.csproj b/src/src/MySql/MySql.csproj index 4d15704..fbba328 100644 --- a/src/src/MySql/MySql.csproj +++ b/src/src/MySql/MySql.csproj @@ -1,8 +1,8 @@ true - MySQL module for Purview.Containers: throwaway MySQL databases on WSL Containers. - wsl;containers;mysql;database;testing;integration + MySQL module for Purview.Containers: throwaway MySQL databases on WSL Containers or Docker. + wsl;containers;docker;mysql;database;testing;integration MIT Purview diff --git a/src/src/MySql/Sdk/README.md b/src/src/MySql/Sdk/README.md index 236e196..5480449 100644 --- a/src/src/MySql/Sdk/README.md +++ b/src/src/MySql/Sdk/README.md @@ -18,7 +18,7 @@ connection-string generation. `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -65,6 +65,6 @@ strategy with `WithWaitStrategy(...)` if you need different behaviour. ## Documentation -- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. -- [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. -- [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Nats/Nats.csproj b/src/src/Nats/Nats.csproj index e1ac2ac..6377eb2 100644 --- a/src/src/Nats/Nats.csproj +++ b/src/src/Nats/Nats.csproj @@ -1,8 +1,8 @@ true - NATS module for Purview.Containers: throwaway NATS message brokers on WSL Containers. - wsl;containers;nats;messaging;testing;integration + NATS module for Purview.Containers: throwaway NATS message brokers on WSL Containers or Docker. + wsl;containers;docker;nats;messaging;testing;integration MIT Purview diff --git a/src/src/Nats/Sdk/README.md b/src/src/Nats/Sdk/README.md index cc712c7..4d0d9e2 100644 --- a/src/src/Nats/Sdk/README.md +++ b/src/src/Nats/Sdk/README.md @@ -18,7 +18,7 @@ package — bring your own client. `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -54,6 +54,6 @@ with `WithWaitStrategy(...)`. Endpoint accessors resolve the mapped host ports, ## Documentation -- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. -- [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. -- [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/PostgreSql/PostgreSql.csproj b/src/src/PostgreSql/PostgreSql.csproj index 0def7a7..934168f 100644 --- a/src/src/PostgreSql/PostgreSql.csproj +++ b/src/src/PostgreSql/PostgreSql.csproj @@ -1,8 +1,8 @@ true - PostgreSQL module for Purview.Containers: throwaway PostgreSQL databases on WSL Containers. - wsl;containers;postgresql;postgres;testing;integration + PostgreSQL module for Purview.Containers: throwaway PostgreSQL databases on WSL Containers or Docker. + wsl;containers;docker;postgresql;postgres;testing;integration MIT Purview diff --git a/src/src/PostgreSql/Sdk/README.md b/src/src/PostgreSql/Sdk/README.md index 50dfa06..9dd1b4c 100644 --- a/src/src/PostgreSql/Sdk/README.md +++ b/src/src/PostgreSql/Sdk/README.md @@ -7,7 +7,7 @@ dotnet add package Purview.Containers.PostgreSql ``` Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Npgsql` for connection-string generation. -See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md). +See the [Getting Started guide](https://github.com/purview-dev/containers/blob/main/docs/wiki/Getting-Started.md). ## Requirements @@ -18,7 +18,7 @@ See the [Getting Started guide](https://github.com/purview-dev/wsl-containers/bl `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -59,6 +59,6 @@ your own with `WithWaitStrategy(...)`. Call `GetConnectionString()` after `Start ## Documentation -- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. -- [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. -- [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/RabbitMq/RabbitMq.csproj b/src/src/RabbitMq/RabbitMq.csproj index 1e9ba0f..1e96424 100644 --- a/src/src/RabbitMq/RabbitMq.csproj +++ b/src/src/RabbitMq/RabbitMq.csproj @@ -1,8 +1,8 @@ true - RabbitMQ module for Purview.Containers: throwaway RabbitMQ brokers on WSL Containers. - wsl;containers;rabbitmq;amqp;testing;integration + RabbitMQ module for Purview.Containers: throwaway RabbitMQ brokers on WSL Containers or Docker. + wsl;containers;docker;rabbitmq;amqp;testing;integration MIT Purview diff --git a/src/src/RabbitMq/Sdk/README.md b/src/src/RabbitMq/Sdk/README.md index 2e88061..7b5bb9d 100644 --- a/src/src/RabbitMq/Sdk/README.md +++ b/src/src/RabbitMq/Sdk/README.md @@ -18,7 +18,7 @@ bring your own client. `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -62,10 +62,10 @@ Call the endpoint accessors after `StartAsync()`. The default wait strategy matches the canonical `Server startup complete` log line rather than running `rabbitmq-diagnostics ping`: under WSLC an exec-based probe races the Erlang cookie setup and can trigger a startup failure (`eacces` reading `.erlang.cookie`). See -[Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) for the full note. +[Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) for the full note. ## Documentation -- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. -- [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. -- [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Redis/Redis.csproj b/src/src/Redis/Redis.csproj index b953118..0ed8738 100644 --- a/src/src/Redis/Redis.csproj +++ b/src/src/Redis/Redis.csproj @@ -1,8 +1,8 @@ true - Redis module for Purview.Containers: throwaway Redis instances on WSL Containers. - wsl;containers;redis;testing;integration + Redis module for Purview.Containers: throwaway Redis instances on WSL Containers or Docker. + wsl;containers;docker;redis;testing;integration MIT Purview diff --git a/src/src/Redis/Sdk/README.md b/src/src/Redis/Sdk/README.md index c2f68f0..4c7c914 100644 --- a/src/src/Redis/Sdk/README.md +++ b/src/src/Redis/Sdk/README.md @@ -18,7 +18,7 @@ package — bring your own client. `Purview.Containers.Wsl` package supplies `buildTransitive` defaults for `WindowsSdkPackageVersion`/`PlatformTarget` and (for a platform-neutral consumer on a Windows build host) the implementation payload, rejecting an unsupported consumer with `PCC0001`/`PCC0002` — see the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - **Docker backend:** any reachable Docker daemon (`docker info`), with a `net10.0` or later project on any platform. No Windows target framework and no `PCC` guards apply. - **Experimental:** the API, defaults and packaging can change between prereleases; there is no @@ -61,6 +61,6 @@ await using var garnet = new RedisBuilder("ghcr.io/microsoft/garnet:latest").Bui ## Documentation -- [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. -- [Modules](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. -- [Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) — choosing and configuring the runtime. +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/containers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Wsl/Sdk/README.md b/src/src/Wsl/Sdk/README.md index 176cd5b..e90237a 100644 --- a/src/src/Wsl/Sdk/README.md +++ b/src/src/Wsl/Sdk/README.md @@ -20,7 +20,7 @@ with no configuration change. It registers itself as the `wsl` backend in the co > **Running in CI, or on a machine without WSL Containers?** With `auto` (the default) the backend is > reported unavailable and selection falls through to Docker. Pin instead with > `PURVIEW_CONTAINERS_BACKEND=wsl|docker` or `ContainerBackends.Use(...)` — see -> [Backends: WSLC or Docker](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Backends.md). +> [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md). ## Requirements @@ -35,7 +35,7 @@ with no configuration change. It registers itself as the `wsl` backend in the co `PlatformTarget` that every module package inherits, and (for a platform-neutral consumer on a Windows build host) copies the Windows implementation payload next to the output. The full contract, the error reference and the `EnableWindowsTargeting` workaround for non-Windows CI agents are in the - [consumer requirements](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Consumer-Requirements.md). + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). - Verify the host with `wsl --version` and `wslc version`. The library never installs or updates WSL itself; `WslContainerRuntime.GetInfoAsync()` reports what is missing. - **Experimental:** this is an experiment in driving WSL Containers. The public API, defaults and @@ -120,11 +120,11 @@ and diagnostics. ## Documentation -See the [project wiki](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Home.md): -[Getting Started](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Getting-Started.md), -[Architecture](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Architecture.md), -[Lifecycle](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Lifecycle.md), -[Networking](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Networking.md) and -[Wait Strategies](https://github.com/purview-dev/wsl-containers/blob/main/docs/wiki/Wait-Strategies.md). +See the [project wiki](https://github.com/purview-dev/containers/blob/main/docs/wiki/Home.md): +[Getting Started](https://github.com/purview-dev/containers/blob/main/docs/wiki/Getting-Started.md), +[Architecture](https://github.com/purview-dev/containers/blob/main/docs/wiki/Architecture.md), +[Lifecycle](https://github.com/purview-dev/containers/blob/main/docs/wiki/Lifecycle.md), +[Networking](https://github.com/purview-dev/containers/blob/main/docs/wiki/Networking.md) and +[Wait Strategies](https://github.com/purview-dev/containers/blob/main/docs/wiki/Wait-Strategies.md). Ready-made service modules ship as `Purview.Containers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, `Azurite`, `Nats` and `MySql`. diff --git a/src/src/Wsl/Wsl.csproj b/src/src/Wsl/Wsl.csproj index 43185d4..f3dc77a 100644 --- a/src/src/Wsl/Wsl.csproj +++ b/src/src/Wsl/Wsl.csproj @@ -19,8 +19,8 @@ --> net10.0;net10.0-windows10.0.19041.0 - WSL Containers backend for Purview.Containers: WSLC-native throwaway Linux containers for .NET integration testing. - wsl;containers;linux;windows;testing;integration + WSL Containers (WSLC) backend for Purview.Containers: run WSLC-native throwaway Linux containers from any .NET 10 project, with automatic Docker fallback. + wsl;wslc;containers;linux;windows;testing;integration;testcontainers MIT Purview + net10.0 + + + + + + + diff --git a/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj b/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj index 1186d0c..8cbd4d0 100644 --- a/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj +++ b/src/tests/Modules.DockerIntegrationTests/Modules.DockerIntegrationTests.csproj @@ -1,4 +1,14 @@ + + + net10.0 + + @@ -9,4 +19,14 @@ + + + + + From 30987568ca2ec8aeea25f7116fbd191bbaa6c530 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 22:02:32 +0100 Subject: [PATCH 08/11] ci: add a manual WSLC integration workflow for a self-hosted runner The WSLC integration suites need a Windows host with WSL Containers, which no hosted runner provides, so they cannot run on pull_request. Add integration-wsl.yml, a workflow_dispatch-only workflow that runs Wsl.IntegrationTests and the seven service-module suites one project at a time on a self-hosted Windows runner carrying the custom wslc label, after checking 'wsl --version' and 'wslc version' so a mis-provisioned runner fails fast. Declare the custom label in .github/actionlint.yaml so actionlint validates the workflow, and document the runner prerequisites in docs/wiki/Testing.md. --- .github/actionlint.yaml | 6 ++ .github/workflows/integration-wsl.yml | 89 +++++++++++++++++++++++++++ AGENTS.md | 6 +- docs/wiki/Testing.md | 33 ++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/integration-wsl.yml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..5649e93 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,6 @@ +# actionlint configuration. The WSLC integration workflow targets a self-hosted runner with a custom +# `wslc` label (a Windows x64 host with WSL Containers installed); declaring it here lets actionlint +# validate .github/workflows/integration-wsl.yml. +self-hosted-runner: + labels: + - wslc \ No newline at end of file diff --git a/.github/workflows/integration-wsl.yml b/.github/workflows/integration-wsl.yml new file mode 100644 index 0000000..67a12ef --- /dev/null +++ b/.github/workflows/integration-wsl.yml @@ -0,0 +1,89 @@ +name: Integration (WSL Containers) + +# Manual-only. The WSLC integration suites need a Windows host with WSL Containers installed, which no +# GitHub-hosted runner provides, so this job runs on a self-hosted Windows runner carrying the custom +# `wslc` label. It is deliberately not attached to pull_request or push: it is the deliberate, manual +# proof that the WSLC backend and every service module run on WSL Containers. The Docker half of the +# matrix runs on every pull request instead (see pr.yml, the `integration-docker` job) because it needs +# only a daemon and therefore works on a hosted Linux runner. +# +# Runner prerequisites are documented in docs/wiki/Testing.md. +on: + workflow_dispatch: + inputs: + ref: + description: Git ref (branch, tag or SHA) to test. Defaults to the ref this workflow is dispatched from. + required: false + default: "" + filter: + description: TUnit treenode filter. Defaults to every test. + required: false + default: "/*/*/*/*" + +concurrency: + # Never cancel a WSLC run in flight: it owns a session and its image store. + group: ${{ github.workflow }}-${{ inputs.ref || github.ref }} + cancel-in-progress: false + +jobs: + integration-wsl: + name: Integration (WSL Containers) + # A self-hosted Windows x64 runner with WSL Containers installed and the custom `wslc` label. + runs-on: [self-hosted, wslc] + timeout-minutes: 60 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + # Keep in sync with `sdk.version` in global.json (and the other workflows). + dotnet-version: "11.0.100-rc.1.26425.128" + + - name: Verify the WSL Containers host + shell: pwsh + run: | + wsl --version + wslc version + + - name: Restore + run: dotnet restore src/WSLTestContainers.slnx + + - name: Build + run: dotnet build src/WSLTestContainers.slnx --configuration Release --no-restore + + - name: WSLC backend contract + # Real containers on a shared WSLC session; ~27 tests and ~4 minutes. + run: dotnet test src/tests/Wsl.IntegrationTests/Wsl.IntegrationTests.csproj --configuration Release --no-build --treenode-filter '${{ inputs.filter }}' + + - name: Service modules on WSLC + # Each module pulls its image and starts a real service. One project at a time keeps the shared + # WSLC image store uncontended (parallel sessions fail with 0x80070020). Every project runs so a + # single failure does not hide the rest, and the step fails at the end if any of them did. + shell: pwsh + run: | + $projects = @( + 'src/tests/PostgreSql.IntegrationTests/PostgreSql.IntegrationTests.csproj', + 'src/tests/Redis.IntegrationTests/Redis.IntegrationTests.csproj', + 'src/tests/MsSql.IntegrationTests/MsSql.IntegrationTests.csproj', + 'src/tests/MySql.IntegrationTests/MySql.IntegrationTests.csproj', + 'src/tests/RabbitMq.IntegrationTests/RabbitMq.IntegrationTests.csproj', + 'src/tests/Azurite.IntegrationTests/Azurite.IntegrationTests.csproj', + 'src/tests/Nats.IntegrationTests/Nats.IntegrationTests.csproj' + ) + + $failed = @() + foreach ($project in $projects) { + Write-Host "::group::$project" + dotnet test $project --configuration Release --no-build --treenode-filter '${{ inputs.filter }}' + if ($LASTEXITCODE -ne 0) { + $failed += $project + } + Write-Host "::endgroup::" + } + + if ($failed.Count -gt 0) { + throw "WSLC integration suites failed: $($failed -join ', ')" + } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 5bff7a4..681a1c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,7 +180,11 @@ Commit messages follow Conventional Commits enforced by the `commit-msg` lefthoo `purview-dev/build/.github/workflows/purview-build.yml`, with pack and validation enabled. - `.github/workflows/release.yml` runs the shared release pipeline with `release-mode: NuGet` on a push to `main`. -- Both workflows pin `dotnet-version` to `global.json`'s `sdk.version`; keep them in sync, and keep +- `.github/workflows/integration-wsl.yml` is a **manual** (`workflow_dispatch`) workflow that runs the + WSLC integration suites on a self-hosted Windows runner with the custom `wslc` label (documented in + [Testing](docs/wiki/Testing.md)); `.github/actionlint.yaml` declares that label. It is never attached to + `pull_request` or `push`. +- The workflows pin `dotnet-version` to `global.json`'s `sdk.version`; keep them in sync, and keep `purview-build.json` pointing at `src/WSLTestContainers.slnx`. The shared workflow runs on `ubuntu-latest`, which is why `EnableWindowsTargeting=true` must stay in `src/Directory.Build.props`. diff --git a/docs/wiki/Testing.md b/docs/wiki/Testing.md index de16eb7..6c32b2d 100644 --- a/docs/wiki/Testing.md +++ b/docs/wiki/Testing.md @@ -61,6 +61,39 @@ image cache (no per-process re-pull) and is the fastest option. In Visual Studio, untick **Run Tests in Parallel** (or set *Maximum Parallel Test Projects* to 1) before running the WSLC integration suites. +## Running the WSLC suites in CI (manual) + +The Docker half of the matrix runs on every pull request. The WSLC half cannot: it needs a Windows host +with WSL Containers, which no GitHub-hosted runner provides, so it is a **manual** workflow — +`.github/workflows/integration-wsl.yml` — that runs on a self-hosted runner. + +Trigger it from **Actions → Integration (WSL Containers) → Run workflow**, or: + +```bash +gh workflow run "Integration (WSL Containers)" --ref main +gh run watch +``` + +It runs `Wsl.IntegrationTests` and then the seven service-module suites (`PostgreSql`, `Redis`, `MsSql`, +`MySql`, `RabbitMq`, `Azurite`, `Nats`) one project at a time, so the shared WSLC image store is never +contended. Two optional inputs: `ref` (a branch, tag or SHA other than the selected one) and `filter` +(a TUnit treenode filter, defaulting to every test). + +### Self-hosted runner prerequisites + +Register a **Windows x64** runner for this repository (or organisation) with the labels +`self-hosted` and the custom **`wslc`** label, and install on that machine: + +- Windows 10/11 with **WSL Containers**: `wsl --install --no-distribution`, verified with + `wsl --version` and `wslc version`. +- The **.NET 11 SDK** the repository pins (`11.0.100-rc.1.26425.128`). + +The job's first step prints `wsl --version` and `wslc version`, so a mis-provisioned runner fails +immediately instead of surfacing as container timeouts. The WSLC suites skip themselves when the host +lacks the WSL Containers components, so a runner without it would report successes-with-skips rather than +real coverage — the host check is what makes that visible. `actionlint` is told about the custom label in +`.github/actionlint.yaml`. + ## Expectations - The `Wsl.IntegrationTests` module runs many real containers in one session and can take several From 9fa1d0a8205e5db7c91497077f5b5ffff4307c75 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 22:15:43 +0100 Subject: [PATCH 09/11] fix(docker): stop the backend-selection test racing a fast exit GenericBuilder_WithTheDockerBackendSelected_StartsAContainer ran /bin/echo selected and then asserted ContainerState.Running. On a fast runner the echo container exits before the state is read, so the assertion flaked (observed on a CI run, 6/7). Use a long-lived command so the container is reliably running. --- .../Docker.IntegrationTests/DockerBackendSelectionTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs b/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs index fba5272..232c9d3 100644 --- a/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs +++ b/src/tests/Docker.IntegrationTests/DockerBackendSelectionTests.cs @@ -20,9 +20,11 @@ public async Task GenericBuilder_WithTheDockerBackendSelected_StartsAContainer() ContainerBackends.Register(DockerContainerBackend.Create()); ContainerBackends.Use(ContainerBackendSelection.Named("docker")); + // A long-lived command: with a bare `/bin/echo` the container can exit before the state is + // read, making the Running assertion below a race. await using var container = new ContainerBuilder() .WithImage("alpine:3.19") - .WithCommand("/bin/echo", "selected") + .WithCommand("/bin/sh", "-c", "echo selected && sleep 60") .Build(); await container.StartAsync(); From 991561108e96c65e5aff301696977d02d71a7edf Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 22:39:16 +0100 Subject: [PATCH 10/11] docs(samples): document the zero-config auto story and add AutoSample The same test project should run on WSLC on a Windows machine and Docker everywhere else without a backend choice in code or environment. That was only described in pieces across Getting-Started, Backends and Modules. Add docs/wiki/Using-in-Your-Tests.md with the copy-paste project file (net10.0, both backends, Redis and PostgreSQL), the test, and a per-host table; link it from the sidebar, index and Getting Started. Add samples/getting-started/AutoSample: a net10.0 project referencing both backends and two modules that lets automatic selection choose (it assembles the wslc/ payload by hand, as a package consumer gets it from Purview.Containers.Wsl buildTransitive). Wire it into the solution, add `just sample-auto`, and update the samples README. Guard the documented shape with verify-consumers case 20 (net10.0 + two modules + both backends); the RequireText check now accepts multiple patterns so it asserts both registrations are generated. Update the Consumer-Requirements count to twenty. --- Justfile | 6 + docs/wiki/Consumer-Requirements.md | 3 +- docs/wiki/Getting-Started.md | 4 + docs/wiki/Using-in-Your-Tests.md | 123 ++++++++++++++++++ docs/wiki/_Sidebar.md | 1 + docs/wiki/index.md | 1 + .../AutoSample/AutoSample.csproj | 54 ++++++++ samples/getting-started/AutoSample/Program.cs | 50 +++++++ samples/getting-started/README.md | 13 +- scripts/verify-consumers.ps1 | 69 +++++++--- src/WSLTestContainers.slnx | 1 + 11 files changed, 305 insertions(+), 20 deletions(-) create mode 100644 docs/wiki/Using-in-Your-Tests.md create mode 100644 samples/getting-started/AutoSample/AutoSample.csproj create mode 100644 samples/getting-started/AutoSample/Program.cs diff --git a/Justfile b/Justfile index 1008e65..d314846 100644 --- a/Justfile +++ b/Justfile @@ -131,6 +131,12 @@ sample-wsl: echo "Running {{ BLUE }}samples/getting-started/WslSample{{ NORMAL }} (needs WSL Containers)..." dotnet run --project samples/getting-started/WslSample/WslSample.csproj +# Runs the auto (zero-config) getting-started sample: picks WSL Containers on Windows or Docker elsewhere +[group('Samples')] +sample-auto: + echo "Running {{ BLUE }}samples/getting-started/AutoSample{{ NORMAL }} (WSL Containers on Windows, Docker elsewhere)..." + dotnet run --project samples/getting-started/AutoSample/AutoSample.csproj + # Runs the Docker getting-started sample: needs a reachable Docker daemon, starts a real container [group('Samples')] sample-docker: diff --git a/docs/wiki/Consumer-Requirements.md b/docs/wiki/Consumer-Requirements.md index 187f6a0..b12833a 100644 --- a/docs/wiki/Consumer-Requirements.md +++ b/docs/wiki/Consumer-Requirements.md @@ -238,7 +238,7 @@ because the package keeps a `net10.0-windows10.0.19041` build, but the condition ## Verifying these requirements -`just verify-consumers` packs the solution and builds nineteen throwaway consumer projects against the +`just verify-consumers` packs the solution and builds twenty throwaway consumer projects against the produced packages, asserting every claim on this page: | Case | Consumer | Expected outcome | @@ -262,6 +262,7 @@ produced packages, asserting every claim on this page: | 17 | the documented backend example on WSL Containers | builds | | 18 | the documented backend example on Docker (`net10.0`) | builds | | 19 | a plain `net10.0` consumer of the WSL Containers backend | builds; the `wsl` registration is generated (the portable facade) | +| 20 | a `net10.0` consumer of two modules with **both** backends (the auto shape) | builds; both registrations are generated | The script is `scripts/verify-consumers.ps1` and it only writes to the temp folder. It needs network access (it restores transitive dependencies from nuget.org), so it is a local/CI-explicit step diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 0368da1..7d8ef51 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -42,6 +42,10 @@ dotnet add package Purview.Containers.PostgreSql dotnet add package Purview.Containers.Wsl # ...or Purview.Containers.Docker ``` +> **Want it to just work without choosing a backend?** Reference **both** backends from a platform-neutral +> `net10.0` project and let automatic selection decide — WSLC on a Windows machine, Docker everywhere else, +> with no code or configuration change. See [Using it in your tests](Using-in-Your-Tests.md). + ## 2. Run a generic container The same code works on either backend — only the package reference from step 1 decides where it runs: diff --git a/docs/wiki/Using-in-Your-Tests.md b/docs/wiki/Using-in-Your-Tests.md new file mode 100644 index 0000000..8f80a0e --- /dev/null +++ b/docs/wiki/Using-in-Your-Tests.md @@ -0,0 +1,123 @@ +# Using it in your tests (auto) + +The zero-configuration shape: **one project, no backend choice** in your code or environment. The same +tests run on **WSL Containers** on a Windows developer machine and on **Docker** on a Linux CI runner (or +a Mac) — nothing changes between them. + +## The project + +```xml + + + net10.0 + + + + + + + + + + +``` + +Three things make this work, and nothing else is required: + +- **A platform-neutral target framework** (`net10.0`). `Purview.Containers.Wsl` is multi-target; a + `net10.0` project binds its portable facade, which runs the WSLC implementation on Windows and reports + `wsl` unavailable everywhere else. (A Windows target framework still works — it binds the + implementation directly — but it cannot run on a Linux CI runner.) +- **Both backend packages.** WSLC is preferred where it is usable and Docker is the fallback, so the one + project runs everywhere. Referencing only the WSL backend leaves a Docker-only machine with nothing to + fall back to. +- **No registration line, no environment variable.** Each backend package ships `buildTransitive` assets + that generate a module initializer in your assembly, so the process discovers `wsl` and `docker` by + itself. + +## The test + +```csharp +using Npgsql; +using Purview.Containers.PostgreSql; +using Purview.Containers.Redis; +using StackExchange.Redis; + +public class CacheAndDatabaseTests +{ + [Test] + public async Task PostgreSql_is_usable() + { + await using var postgres = new PostgreSqlBuilder().WithDatabase("app").Build(); + await postgres.StartAsync(); // returns once pg_isready succeeds + + await using var connection = new NpgsqlConnection(postgres.GetConnectionString()); + await connection.OpenAsync(); + // ...your assertions against a real database... + } + + [Test] + public async Task Redis_is_usable() + { + await using var redis = new RedisBuilder().Build(); + await redis.StartAsync(); // returns once redis-cli ping succeeds + + await using var cache = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString()); + await cache.GetDatabase().PingAsync(); + // ...your assertions against a real cache... + } +} +``` + +That is the whole story: no image names, no ports, no `Testcontainers`, no `docker`, no `wslc`. The +builder default image, the port mapping (a random host port), and the readiness check all come from the +module; `GetConnectionString()` points at the mapped host port. + +## What happens on each host + +| Host | Selected backend | Why | +| --- | --- | --- | +| Windows with WSL Containers | `wsl` | The facade loads the WSLC implementation and WSLC is preferred. | +| Windows without WSL Containers, with Docker | `docker` | The facade reports `wsl` unavailable, so selection falls through. | +| Linux / macOS (any CI runner) | `docker` | The facade only ever activates on Windows. | + +Automatic selection probes the registered backends in priority order (`wsl` is `0`, `docker` is `100`) and +returns the first one that is both **available** and **compatible**. When none is usable, the exception +lists each backend's availability, version and missing components — see +[Backends: WSLC or Docker](Backends.md). + +## Pinning, and seeing what it picked + +Auto is the default and needs no configuration. Two things help while debugging: + +```csharp +// Which backends can this machine run, and what version? +foreach (var backend in await ContainerBackends.ProbeAllAsync()) +{ + Console.WriteLine($"{backend.Name}: usable={backend.IsUsable} version={backend.Version}"); +} +``` + +```bash +# Fail loudly instead of falling back (the usual choice in CI). +PURVIEW_CONTAINERS_BACKEND=docker # or: wsl +``` + +A named backend never silently falls back: if it cannot run, the test fails with that backend's own +diagnostics. + +## Requirements + +- The abstractions and every service module are portable `net10.0`; see + [Consumer Requirements](Consumer-Requirements.md) for the exact target-framework contract, the + `PCC0001`/`PCC0002` guards and the `EnableWindowsTargeting` workaround for non-Windows build agents. +- The per-module connection-string shapes (Redis, PostgreSQL, SQL Server, MySQL, RabbitMQ, Azurite, NATS) + are in [Modules](Modules.md#connection-strings). +- A live sample of this shape is `samples/getting-started/AutoSample` (`just sample-auto`). + +## Related + +- [Getting Started](Getting-Started.md) — install and the first container. +- [Backends: WSLC or Docker](Backends.md) — the comparison, side-by-side setup and CI examples. +- [Modules](Modules.md) — the service modules and their connection strings. +- [Consumer Requirements](Consumer-Requirements.md) — the target-framework contract and the guards. \ No newline at end of file diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 8ef1e07..8b7bdd1 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -1,5 +1,6 @@ - [Home](Home.md) - [Getting Started](Getting-Started.md) +- [Using in Your Tests (auto)](Using-in-Your-Tests.md) - [Backends: WSLC or Docker](Backends.md) - [Consumer Requirements](Consumer-Requirements.md) - [Architecture](Architecture.md) diff --git a/docs/wiki/index.md b/docs/wiki/index.md index 2a7eb2a..434f717 100644 --- a/docs/wiki/index.md +++ b/docs/wiki/index.md @@ -10,6 +10,7 @@ code runs on either runtime; see [Backends: WSLC or Docker](Backends.md). ## Guides - [Getting Started](Getting-Started.md) +- [Using in your tests (auto)](Using-in-Your-Tests.md) - [Backends: WSLC or Docker](Backends.md) - [Consumer requirements](Consumer-Requirements.md) - [Architecture](Architecture.md) diff --git a/samples/getting-started/AutoSample/AutoSample.csproj b/samples/getting-started/AutoSample/AutoSample.csproj new file mode 100644 index 0000000..10dc53d --- /dev/null +++ b/samples/getting-started/AutoSample/AutoSample.csproj @@ -0,0 +1,54 @@ + + + + Exe + net10.0 + enable + enable + false + + + + + + + + + + + + $(MSBuildThisFileDirectory)../../../src/src/Wsl/bin/$(Configuration)/net10.0-windows10.0.19041.0/ + $(NuGetPackageRoot)microsoft.wsl.containers/3.0.1 + $(NuGetPackageRoot)microsoft.windows.sdk.net.ref/10.0.26100.80 + + + + + + + + + + + + + + + diff --git a/samples/getting-started/AutoSample/Program.cs b/samples/getting-started/AutoSample/Program.cs new file mode 100644 index 0000000..280576a --- /dev/null +++ b/samples/getting-started/AutoSample/Program.cs @@ -0,0 +1,50 @@ +// The zero-configuration sample: the same test-style code a consumer writes, with no backend choice in +// code or environment. On a Windows machine with WSL Containers this runs on WSLC; on a machine with only +// Docker it runs on Docker. Prerequisite: either backend reachable (see docs/wiki/Using-in-Your-Tests.md). +using Purview.Containers; +using Purview.Containers.Docker; +using Purview.Containers.PostgreSql; +using Purview.Containers.Redis; +using Purview.Containers.Runtime; +using Purview.Containers.Wsl; + +// A package consumer gets backend registration generated from the packages' buildTransitive assets, so +// this line does not appear in consumer code. The sample references the backends as projects, so it +// registers both explicitly - exactly what the generated initializer does - and lets automatic selection +// choose between them. +ContainerBackends.Register(WslContainerBackend.Create()); +ContainerBackends.Register(DockerContainerBackend.Create()); + +Console.WriteLine("Registered backends (auto picks the first usable one):"); +foreach (var backend in await ContainerBackends.ProbeAllAsync()) +{ + Console.WriteLine($" {backend.Name, -7} usable={backend.IsUsable} version={backend.Version}"); +} + +IContainerBackend selected; +try +{ + selected = await ContainerBackends.ResolveAsync(); +} +catch (ContainerBackendUnavailableException exception) +{ + Console.WriteLine(); + Console.WriteLine("No usable backend on this machine - start Docker or install WSL Containers."); + Console.WriteLine(exception.Message); + return 0; +} + +Console.WriteLine($"Selected backend: {selected.Name}"); +Console.WriteLine(); + +await using var redis = new RedisBuilder().Build(); +await redis.StartAsync(); +var ping = await redis.ExecAsync(["redis-cli", "ping"]); +Console.WriteLine($"redis : {redis.GetConnectionString()} -> {ping.Stdout.Trim()}"); + +await using var postgres = new PostgreSqlBuilder().WithDatabase("app").Build(); +await postgres.StartAsync(); +var ready = await postgres.ExecAsync(["pg_isready"]); +Console.WriteLine($"postgresql : {postgres.GetConnectionString()} -> {ready.Stdout.Trim()}"); + +return 0; diff --git a/samples/getting-started/README.md b/samples/getting-started/README.md index 1cd0b46..6ba8130 100644 --- a/samples/getting-started/README.md +++ b/samples/getting-started/README.md @@ -1,22 +1,27 @@ # Getting-started samples -Two runnable samples that differ in exactly one thing: which backend they use. The container code is the -same, which is the point — see [Backends: WSLC or Docker](../../docs/wiki/Backends.md). +Three runnable samples. `WslSample` and `DockerSample` differ in exactly one thing — which backend they +use — and the container code is identical, which is the point. `AutoSample` removes even that choice: it +references both backends and lets automatic selection decide, so the same project runs on WSL Containers +on Windows and Docker everywhere else. See [Backends: WSLC or Docker](../../docs/wiki/Backends.md) and +[Using it in your tests](../../docs/wiki/Using-in-Your-Tests.md). | Sample | Backend | Target framework | Prerequisite | | --- | --- | --- | --- | | `WslSample` | `Purview.Containers.Wsl` | `net11.0-windows10.0.19041.0` | Windows with WSL Containers (`wsl --install --no-distribution`) | | `DockerSample` | `Purview.Containers.Docker` | `net10.0` | a reachable Docker daemon (`docker info`) | +| `AutoSample` | both (auto) | `net10.0` | either — WSLC on Windows, Docker elsewhere | Run them from the repository root: ```powershell +just sample-auto # or: dotnet run --project samples/getting-started/AutoSample just sample-wsl # or: dotnet run --project samples/getting-started/WslSample just sample-docker # or: dotnet run --project samples/getting-started/DockerSample ``` -Each sample starts a real container, waits for it to log `ready`, prints the mapped host port and the -container's output, and disposes it again. +Each sample starts real containers, waits for them to be ready, prints the mapped host ports and the +services' output, and disposes them again. ## These are repository samples diff --git a/scripts/verify-consumers.ps1 b/scripts/verify-consumers.ps1 index c640009..e293d5c 100644 --- a/scripts/verify-consumers.ps1 +++ b/scripts/verify-consumers.ps1 @@ -30,7 +30,8 @@ 16 net11.0 windows + both backends (auto detection) -> builds, both registrations generated 17 the documented backend example, on WSL Containers -> builds 18 the documented backend example, on Docker (net10.0) -> builds - 19 plain net10.0 + WSL backend (portable facade, auto WSLC/Docker) -> builds, wsl registration generated + 19 plain net10.0 + WSL backend (portable facade, auto) -> builds, wsl registration generated + 20 net10.0 + Redis/PostgreSql + both backends (auto) -> builds, both registrations generated .PARAMETER FeedPath Folder holding the packed .nupkg files. Defaults to /artifacts. @@ -236,6 +237,35 @@ $wslBackendItems = @' '@ +# The "auto" shape documented in docs/wiki/Using-in-Your-Tests.md: a platform-neutral consumer with two +# service modules and BOTH backends, so the one project runs on WSLC (Windows) and Docker (everywhere else). +$autoItems = @' + + + + + +'@ + +$autoSource = @' +using Purview.Containers; +using Purview.Containers.PostgreSql; +using Purview.Containers.Redis; + +namespace Consumer; + +public static class Program +{ + public static async Task SelectedBackendAsync() => (await ContainerBackends.ResolveAsync()).Name; + + public static RedisBuilder Cache() => new RedisBuilder(); + + public static PostgreSqlBuilder Database() => new PostgreSqlBuilder().WithDatabase("app"); + + public static void Main() { } +} +'@ + # Mirrors the container code in the "The same test, either backend" example of docs/wiki/Backends.md, so # the documented example is compiled against both backend packages on every run. The test assertion is # omitted because the generated consumer project has no test framework. @@ -276,7 +306,7 @@ function New-ConsumerCase { [hashtable] $Sources = @{ 'Smoke.cs' = $apiSource }, [string] $Expect = 'Builds', [string] $RequireFile = '', - [string] $RequireText = '' + [string[]] $RequireText = @() ) [pscustomobject]@{ @@ -376,10 +406,17 @@ $cases = @( -Package 'Purview.Containers.Docker' ` -Sources @{ 'Smoke.cs' = $documentedBackendSource } - New-ConsumerCase -Id '19' -Name 'plain net10.0 + WSL backend (portable facade, auto WSLC/Docker)' ` + New-ConsumerCase -Id '19' -Name 'plain net10.0 + WSL backend (portable facade, auto)' ` -Framework 'net10.0' ` -Sources @{ 'Smoke.Portable.cs' = $portableSource } ` -RequireText 'WslContainerBackend.Create()' + + New-ConsumerCase -Id '20' -Name 'net10.0 + Redis/PostgreSql + both backends (auto, zero-config)' ` + -Framework 'net10.0' ` + -Package $modulePackage ` + -Items $autoItems ` + -Sources @{ 'Smoke.Auto.cs' = $autoSource } ` + -RequireText @('WslContainerBackend.Create()', 'DockerContainerBackend.Create()') ) @@ -454,18 +491,20 @@ foreach ($case in $cases) { } } - if ($passed -and $case.RequireText) { - $found = @( - Get-ChildItem -Path $directory -Recurse -File -Include *.cs, *.csproj, *.json -ErrorAction SilentlyContinue | - Select-String -Pattern $case.RequireText -SimpleMatch -ErrorAction SilentlyContinue - ) - - if ($found.Count -eq 0) { - $passed = $false - $signal = "$signal; '$($case.RequireText)' was not generated into the consumer" - } - else { - $signal = "$signal; generated '$($case.RequireText)'" + if ($passed -and $case.RequireText.Count -gt 0) { + foreach ($pattern in $case.RequireText) { + $found = @( + Get-ChildItem -Path $directory -Recurse -File -Include *.cs, *.csproj, *.json -ErrorAction SilentlyContinue | + Select-String -Pattern $pattern -SimpleMatch -ErrorAction SilentlyContinue + ) + + if ($found.Count -eq 0) { + $passed = $false + $signal = "$signal; '$pattern' was not generated into the consumer" + } + else { + $signal = "$signal; generated '$pattern'" + } } } diff --git a/src/WSLTestContainers.slnx b/src/WSLTestContainers.slnx index 3c4f6b7..a7df6cb 100644 --- a/src/WSLTestContainers.slnx +++ b/src/WSLTestContainers.slnx @@ -23,6 +23,7 @@ + From d70b46743af96dc1f5a95904c5c046d199494c4d Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 1 Oct 2026 23:10:00 +0100 Subject: [PATCH 11/11] refactor(core): split abstractions into Purview.Containers.Core, add umbrella BREAKING CHANGE: the abstractions package is renamed to Purview.Containers.Core. A package reference to Purview.Containers now means the new umbrella bundle. Consumer source is unchanged (the namespace stays Purview.Containers). Purview.Containers is the dependency root (IContainerBackend lives there) so it cannot reference the backends; a TUnit-style split keeps that direction and puts the bundle at the name consumers reach for: - src/src/Containers -> src/src/Core (Core.csproj; SDK derives PackageId and AssemblyName Purview.Containers.Core and RootNamespace Purview.Containers, so no consumer using changes) - buildTransitive assets move to Purview.Containers.Core.{props,targets} so NuGet still auto-imports them - new src/src/Containers/Containers.csproj: umbrella referencing Core, Wsl and Docker; no code and no buildTransitive of its own (the dependencies flow transitively) One reference now gives the whole auto setup: a net10.0 project with Purview.Containers and the modules runs on WSLC on Windows and Docker elsewhere, no registration and no environment variable. Docs (Architecture, Consumer-Requirements, Home, Packaging, Modules, Getting-Started, Using-in-Your-Tests, README, AGENTS, package READMEs) describe the split and present the umbrella as the zero-config default, with Core plus a single backend as the lean route for Linux-only CI. purview-build.json gains purview.containers.core and trims purview.containers to the umbrella. verify-consumers case 13 targets Core and a new case 21 references only the umbrella. AutoSample now references the umbrella project. --- AGENTS.md | 15 +-- README.md | 18 ++-- docs/wiki/Architecture.md | 23 +++-- docs/wiki/Consumer-Requirements.md | 6 +- docs/wiki/Getting-Started.md | 7 +- docs/wiki/Home.md | 6 +- docs/wiki/Modules.md | 2 +- docs/wiki/Packaging.md | 20 ++-- docs/wiki/Using-in-Your-Tests.md | 17 ++-- purview-build.json | 13 ++- .../AutoSample/AutoSample.csproj | 12 +-- samples/getting-started/README.md | 7 +- scripts/verify-consumers.ps1 | 27 +++++- src/WSLTestContainers.slnx | 1 + src/src/Azurite/Azurite.csproj | 2 +- src/src/Azurite/Sdk/README.md | 2 +- src/src/Containers/Containers.csproj | 17 +++- src/src/Containers/Sdk/README.md | 92 ++++++++++--------- src/src/{Containers => Core}/Container.cs | 0 .../ContainerBackendInfo.cs | 0 .../ContainerBackendSelection.cs | 0 .../{Containers => Core}/ContainerBackends.cs | 0 src/src/{Containers => Core}/ContainerBase.cs | 0 .../{Containers => Core}/ContainerBuilder.cs | 0 .../ContainerConfiguration.cs | 0 .../{Containers => Core}/ContainerLogEntry.cs | 0 src/src/{Containers => Core}/ContainerName.cs | 0 .../{Containers => Core}/ContainerState.cs | 0 src/src/Core/Core.csproj | 9 ++ .../Diagnostics/ContainerActivity.cs | 0 .../Diagnostics/Secret.cs | 0 .../Diagnostics/SecretRedactor.cs | 0 src/src/{Containers => Core}/ExecOptions.cs | 0 src/src/{Containers => Core}/ExecResult.cs | 0 src/src/{Containers => Core}/IContainer.cs | 0 .../{Containers => Core}/IContainerBackend.cs | 0 .../IContainerBackendPreference.cs | 0 .../{Containers => Core}/IContainerBuilder.cs | 0 .../IContainerConfiguration.cs | 0 src/src/{Containers => Core}/Images/Image.cs | 0 .../Images/ImagePullProgress.cs | 0 .../Images/ImageSummary.cs | 0 .../{Containers => Core}/Images/PullPolicy.cs | 0 src/src/{Containers => Core}/LogStream.cs | 0 .../{Containers => Core}/Mounts/BindMount.cs | 0 .../Mounts/NamedVolume.cs | 0 .../Networking/ContainerNetworkingMode.cs | 0 .../Networking/PortBinding.cs | 0 .../Networking/PortProtocol.cs | 0 .../RegistryCredentials.cs | 0 .../Runtime/ContainerException.cs | 0 src/src/Core/Sdk/README.md | 71 ++++++++++++++ .../Purview.Containers.Core.props} | 4 +- .../Purview.Containers.Core.targets} | 4 +- .../Waiting/AllWaitStrategy.cs | 0 .../Waiting/AnyWaitStrategy.cs | 0 .../Waiting/CommandWaitStrategy.cs | 0 .../Waiting/ContainerRunningWaitStrategy.cs | 0 .../Waiting/CustomWaitStrategy.cs | 0 .../Waiting/HttpWaitStrategy.cs | 0 .../Waiting/IWaitStrategy.cs | 0 .../Waiting/LogMessageWaitStrategy.cs | 0 .../Waiting/TcpPortWaitStrategy.cs | 0 src/src/{Containers => Core}/Waiting/Wait.cs | 0 .../Waiting/WaitContext.cs | 0 .../Waiting/WaitStrategy.cs | 0 .../Waiting/WaitStrategyRunner.cs | 0 src/src/Docker/Docker.csproj | 2 +- src/src/Docker/Sdk/README.md | 2 +- .../Purview.Containers.Docker.props | 2 +- src/src/MsSql/MsSql.csproj | 2 +- src/src/MsSql/Sdk/README.md | 2 +- src/src/MySql/MySql.csproj | 2 +- src/src/MySql/Sdk/README.md | 2 +- src/src/Nats/Nats.csproj | 2 +- src/src/Nats/Sdk/README.md | 2 +- src/src/PostgreSql/PostgreSql.csproj | 2 +- src/src/PostgreSql/Sdk/README.md | 2 +- src/src/RabbitMq/RabbitMq.csproj | 2 +- src/src/RabbitMq/Sdk/README.md | 2 +- src/src/Redis/Redis.csproj | 2 +- src/src/Redis/Sdk/README.md | 2 +- src/src/Wsl/Sdk/README.md | 2 +- .../Purview.Containers.Wsl.props | 4 +- src/src/Wsl/Wsl.csproj | 2 +- 85 files changed, 277 insertions(+), 136 deletions(-) rename src/src/{Containers => Core}/Container.cs (100%) rename src/src/{Containers => Core}/ContainerBackendInfo.cs (100%) rename src/src/{Containers => Core}/ContainerBackendSelection.cs (100%) rename src/src/{Containers => Core}/ContainerBackends.cs (100%) rename src/src/{Containers => Core}/ContainerBase.cs (100%) rename src/src/{Containers => Core}/ContainerBuilder.cs (100%) rename src/src/{Containers => Core}/ContainerConfiguration.cs (100%) rename src/src/{Containers => Core}/ContainerLogEntry.cs (100%) rename src/src/{Containers => Core}/ContainerName.cs (100%) rename src/src/{Containers => Core}/ContainerState.cs (100%) create mode 100644 src/src/Core/Core.csproj rename src/src/{Containers => Core}/Diagnostics/ContainerActivity.cs (100%) rename src/src/{Containers => Core}/Diagnostics/Secret.cs (100%) rename src/src/{Containers => Core}/Diagnostics/SecretRedactor.cs (100%) rename src/src/{Containers => Core}/ExecOptions.cs (100%) rename src/src/{Containers => Core}/ExecResult.cs (100%) rename src/src/{Containers => Core}/IContainer.cs (100%) rename src/src/{Containers => Core}/IContainerBackend.cs (100%) rename src/src/{Containers => Core}/IContainerBackendPreference.cs (100%) rename src/src/{Containers => Core}/IContainerBuilder.cs (100%) rename src/src/{Containers => Core}/IContainerConfiguration.cs (100%) rename src/src/{Containers => Core}/Images/Image.cs (100%) rename src/src/{Containers => Core}/Images/ImagePullProgress.cs (100%) rename src/src/{Containers => Core}/Images/ImageSummary.cs (100%) rename src/src/{Containers => Core}/Images/PullPolicy.cs (100%) rename src/src/{Containers => Core}/LogStream.cs (100%) rename src/src/{Containers => Core}/Mounts/BindMount.cs (100%) rename src/src/{Containers => Core}/Mounts/NamedVolume.cs (100%) rename src/src/{Containers => Core}/Networking/ContainerNetworkingMode.cs (100%) rename src/src/{Containers => Core}/Networking/PortBinding.cs (100%) rename src/src/{Containers => Core}/Networking/PortProtocol.cs (100%) rename src/src/{Containers => Core}/RegistryCredentials.cs (100%) rename src/src/{Containers => Core}/Runtime/ContainerException.cs (100%) create mode 100644 src/src/Core/Sdk/README.md rename src/src/{Containers/Sdk/buildTransitive/Purview.Containers.props => Core/Sdk/buildTransitive/Purview.Containers.Core.props} (89%) rename src/src/{Containers/Sdk/buildTransitive/Purview.Containers.targets => Core/Sdk/buildTransitive/Purview.Containers.Core.targets} (93%) rename src/src/{Containers => Core}/Waiting/AllWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/AnyWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/CommandWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/ContainerRunningWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/CustomWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/HttpWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/IWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/LogMessageWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/TcpPortWaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/Wait.cs (100%) rename src/src/{Containers => Core}/Waiting/WaitContext.cs (100%) rename src/src/{Containers => Core}/Waiting/WaitStrategy.cs (100%) rename src/src/{Containers => Core}/Waiting/WaitStrategyRunner.cs (100%) diff --git a/AGENTS.md b/AGENTS.md index 681a1c2..7b95410 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,8 @@ whenever a target framework, a backend package or the `buildTransitive` assets c | Path | Purpose | | --- | --- | | `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack | -| `src/src/Containers` | Backend-neutral abstractions (`Purview.Containers`, `net10.0`): `IContainer`/`ContainerConfiguration`, `ContainerBuilder`, `ContainerBase`, `IContainerBackend`/`ContainerBackends`, wait strategies, images, networking, mounts, diagnostics | +| `src/src/Containers` | Umbrella package (`Purview.Containers`, `net10.0`): references `Core` and both backends; no code of its own | +| `src/src/Core` | Backend-neutral abstractions (`Purview.Containers.Core`, `net10.0`, namespace `Purview.Containers`): `IContainer`/`ContainerConfiguration`, `ContainerBuilder`, `ContainerBase`, `IContainerBackend`/`ContainerBackends`, wait strategies, images, networking, mounts, diagnostics | | `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`, multi-target `net10.0` facade + `net10.0-windows10.0.19041.0` implementation): `WslContainerBackend`, the shared session runtime, `WslContainer`, `WslContainerSession`, and the portable facade (`WslPayload`, `*.Facade.cs`) that loads the implementation at run time | | `src/src/Docker` | Docker backend (`Purview.Containers.Docker`, `net10.0`): `DockerContainerBackend` + `DockerContainer` over Testcontainers | | `src/src/` | Service modules (`Purview.Containers.`, `net10.0`); each is a thin layer over the abstractions and carries a bespoke `Sdk/README.md` | @@ -78,7 +79,7 @@ whenever a target framework, a backend package or the `buildTransitive` assets c ## Consumer requirements and compatibility -- The packages split by framework: `Purview.Containers` (abstractions) and the service modules target +- The packages split by framework: `Purview.Containers.Core` (abstractions) and the service modules target **`net10.0`** on any platform. `Purview.Containers.Wsl` is **multi-target**: `net10.0` (a portable facade) and `net10.0-windows10.0.19041.0` (the WSLC implementation). `src/src/Directory.Build.props` sets the `net10.0` subtree default and `Wsl.csproj` adds the Windows build; the WSLC tests keep the @@ -94,7 +95,7 @@ whenever a target framework, a backend package or the `buildTransitive` assets c loads at run time. Those assets are framework-agnostic, so NuGet never raises `NU1202` at restore time; the guards are the fail-fast path. Keep them, and keep the `RequiredContent` entries (including `payload/**`) that declare them. They belong to the WSL backend package only. -- `Purview.Containers` ships `Sdk/buildTransitive/Purview.Containers.{props,targets}`, which turn +- `Purview.Containers.Core` ships `Sdk/buildTransitive/Purview.Containers.Core.{props,targets}`, which turn the `PurviewContainersBackends` list (each backend package appends its own entry) into a generated module initializer in the consuming assembly. That is how a package consumer's process discovers its backend without reflection or assembly scanning. A project-reference consumer (this repository's own @@ -126,11 +127,13 @@ whenever a target framework, a backend package or the `buildTransitive` assets c `src/Directory.Build.props` depend on it. - Each package ships `lib/$(TFM)/.{dll,xml}`, `README.md` (from `Sdk/README.md`) and `purview-logo-light.png`. PDBs ship only in the `.snupkg`. MSBuild assets are per package and must be - named after their own package id for NuGet to import them: `Purview.Containers` ships - `buildTransitive/Purview.Containers.{props,targets}` (backend registration), + named after their own package id for NuGet to import them: `Purview.Containers.Core` ships + `buildTransitive/Purview.Containers.Core.{props,targets}` (backend registration), `Purview.Containers.Wsl` ships `buildTransitive/Purview.Containers.Wsl.{props,targets}` (consumer defaults and guards) and `Purview.Containers.Docker` ships - `buildTransitive/Purview.Containers.Docker.props` (its registration entry). See + `buildTransitive/Purview.Containers.Docker.props` (its registration entry). The umbrella + `Purview.Containers` ships none of its own; `Core`'s and the backends' assets reach its consumers + transitively. See [Consumer requirements and compatibility](#consumer-requirements-and-compatibility). - `PackValidation.RequireExplicitContent` defaults to `true`, so `purview-build.json`'s `RequiredContent` is the **exhaustive** manifest: a produced package with no rule, or a packed entry matched by no glob, fails diff --git a/README.md b/README.md index c1575eb..967634f 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,16 @@ A Testcontainers-style library for .NET that runs throwaway Linux containers for integration testing — on **Microsoft WSL Containers (WSLC)** with no Docker installation, or on **Docker** through Testcontainers. -> **Two backends, one API.** Containers are created through the backend-neutral `Purview.Containers` -> abstractions, so the same test suite runs on **WSL Containers** (`Purview.Containers.Wsl`) or -> **Docker** (`Purview.Containers.Docker`, driven by Testcontainers). Selection is automatic by default -> and can be pinned with `PURVIEW_CONTAINERS_BACKEND`. A plain `net10.0` project — no Windows target -> framework needed — references `Purview.Containers.Wsl` and gets WSLC on a Windows developer machine -> and Docker on a Linux CI runner **without changing a line of test code or configuration**: the package -> is multi-target and its `net10.0` facade loads the WSLC implementation at run time on Windows and -> reports `wsl` as unavailable everywhere else. +> **Two backends, one API — one reference.** Containers are created through the backend-neutral +> `Purview.Containers.Core` abstractions, so the same test suite runs on **WSL Containers** +> (`Purview.Containers.Wsl`) or **Docker** (`Purview.Containers.Docker`, driven by Testcontainers). Add the +> umbrella package `Purview.Containers` to a plain `net10.0` project — no Windows target framework needed — +> and it brings the abstractions plus both backends, gets WSLC on a Windows developer machine and Docker on +> a Linux CI runner **without changing a line of test code or configuration**, and can be pinned with +> `PURVIEW_CONTAINERS_BACKEND`. The `Purview.Containers.Wsl` package is multi-target: its `net10.0` facade +> loads the WSLC implementation at run time on Windows and reports `wsl` as unavailable everywhere else. +> +> **[Using it in your tests (auto)](docs/wiki/Using-in-Your-Tests.md)** — the copy-paste zero-config shape. > > **[Backends: WSLC or Docker](docs/wiki/Backends.md)** — comparison, side-by-side project setup, CI > example and troubleshooting. diff --git a/docs/wiki/Architecture.md b/docs/wiki/Architecture.md index 7360994..07216b0 100644 --- a/docs/wiki/Architecture.md +++ b/docs/wiki/Architecture.md @@ -7,13 +7,14 @@ Design for a Testcontainers-style .NET library with pluggable container backends The library is split into a backend-neutral abstraction assembly and one assembly per backend: ``` -Purview.Containers (net10.0, portable) - ├─ IContainer / IContainerConfiguration / ContainerConfiguration the container contract - ├─ ContainerBuilder fluent configuration + validation - ├─ ContainerBase typed module container (delegates to the backend) - ├─ ContainerBackends + IContainerBackend backend registry and selection - ├─ Waiting / Images / Mounts / Networking / Diagnostics readiness, model, secrets - └─ Runtime/ContainerException neutral error taxonomy +Purview.Containers (net10.0) umbrella: references Core, Wsl and Docker + └─ Purview.Containers.Core (net10.0, portable) the backend-neutral abstractions + ├─ IContainer / IContainerConfiguration / ContainerConfiguration the container contract + ├─ ContainerBuilder fluent configuration + validation + ├─ ContainerBase typed module container (delegates to the backend) + ├─ ContainerBackends + IContainerBackend backend registry and selection + ├─ Waiting / Images / Mounts / Networking / Diagnostics readiness, model, secrets + └─ Runtime/ContainerException neutral error taxonomy Purview.Containers.Wsl (net10.0 facade + net10.0-windows10.0.19041.0 implementation) ├─ WslContainerBackend : IContainerBackend registers itself as "wsl" @@ -32,8 +33,10 @@ IContainer : IAsyncDisposable ``` A module (`Purview.Containers.PostgreSql`, `Purview.Containers.Redis`, …) derives its container from -`ContainerBase` and references `Purview.Containers` only. The backend is resolved at run time through -`ContainerBackends`, so the same module package works on WSLC or Docker. +`ContainerBase` and references `Purview.Containers.Core` only. The backend is resolved at run time through +`ContainerBackends`, so the same module package works on WSLC or Docker. `Purview.Containers` is the +umbrella package: it carries no code of its own and simply references `Core` plus both backends, so one +reference gives a consumer the whole "auto" setup. Microsoft types (`Session`, `Container`, `Process`, `ContainerSettings`, …) are **runtime implementation details** kept behind the public interfaces. They are not exposed through the public API surface (an opt-in accessor is the only escape hatch). @@ -56,7 +59,7 @@ precedence: Resolution is cached per process, so probes run once; `Reset()` (tests) and `Register`/`Use` invalidate the cache. Package consumers get registration from the generated module initializer in -`Purview.Containers.targets`; project-reference consumers register explicitly with +`Purview.Containers.Core.targets`; project-reference consumers register explicitly with `Register(...)`. Consumer-facing guidance (including the CI example) is in [Backends: WSLC or Docker](Backends.md). diff --git a/docs/wiki/Consumer-Requirements.md b/docs/wiki/Consumer-Requirements.md index b12833a..f6010eb 100644 --- a/docs/wiki/Consumer-Requirements.md +++ b/docs/wiki/Consumer-Requirements.md @@ -18,7 +18,8 @@ that exist for it, and of how each of them is verified. | Package | Target framework | Notes | | --- | --- | --- | -| `Purview.Containers` | `net10.0`, any platform | Backend-neutral abstractions. | +| `Purview.Containers` | `net10.0`, any platform | The umbrella: brings `Purview.Containers.Core` plus both backends. One reference; no target-framework requirement beyond .NET 10. | +| `Purview.Containers.Core` | `net10.0`, any platform | Backend-neutral abstractions (namespace `Purview.Containers`). | | `Purview.Containers.` | `net10.0`, any platform | Service modules. Restore anywhere; needs a backend package to actually run. | | `Purview.Containers.Wsl` | `net10.0` and `net10.0-windows10.0.19041.0` | The WSL Containers backend: a portable facade plus the Windows implementation. **The requirements on this page are its contract.** | | `Purview.Containers.Docker` | `net10.0`, any platform | The Docker backend (Testcontainers). Needs a reachable Docker daemon, not a Windows target framework. | @@ -238,7 +239,7 @@ because the package keeps a `net10.0-windows10.0.19041` build, but the condition ## Verifying these requirements -`just verify-consumers` packs the solution and builds twenty throwaway consumer projects against the +`just verify-consumers` packs the solution and builds twenty-one throwaway consumer projects against the produced packages, asserting every claim on this page: | Case | Consumer | Expected outcome | @@ -263,6 +264,7 @@ produced packages, asserting every claim on this page: | 18 | the documented backend example on Docker (`net10.0`) | builds | | 19 | a plain `net10.0` consumer of the WSL Containers backend | builds; the `wsl` registration is generated (the portable facade) | | 20 | a `net10.0` consumer of two modules with **both** backends (the auto shape) | builds; both registrations are generated | +| 21 | a `net10.0` consumer of modules with only the umbrella `Purview.Containers` | builds; both registrations are generated (one reference) | The script is `scripts/verify-consumers.ps1` and it only writes to the temp folder. It needs network access (it restores transitive dependencies from nuget.org), so it is a local/CI-explicit step diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 7d8ef51..b8e183e 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -42,9 +42,10 @@ dotnet add package Purview.Containers.PostgreSql dotnet add package Purview.Containers.Wsl # ...or Purview.Containers.Docker ``` -> **Want it to just work without choosing a backend?** Reference **both** backends from a platform-neutral -> `net10.0` project and let automatic selection decide — WSLC on a Windows machine, Docker everywhere else, -> with no code or configuration change. See [Using it in your tests](Using-in-Your-Tests.md). +> **Want it to just work without choosing a backend?** Add `Purview.Containers` (the umbrella — it brings +> the abstractions and both backends) to a platform-neutral `net10.0` project and let automatic selection +> decide — WSLC on a Windows machine, Docker everywhere else, with no code or configuration change. See +> [Using it in your tests](Using-in-Your-Tests.md). ## 2. Run a generic container diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 3c70d2d..82fe737 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -28,7 +28,8 @@ This wiki is the project documentation hub. The packages are published under the | Package | Purpose | | --- | --- | -| `Purview.Containers` | Backend-neutral abstractions: the container contract, builders, wait strategies, images, networking, mounts, diagnostics, backend selection. | +| `Purview.Containers` | The umbrella: references `Purview.Containers.Core` and both backends, so one reference runs the same tests on WSLC on Windows and Docker elsewhere. No code of its own. | +| `Purview.Containers.Core` | Backend-neutral abstractions: the container contract, builders, wait strategies, images, networking, mounts, diagnostics, backend selection (namespace `Purview.Containers`). | | `Purview.Containers.Wsl` | The WSL Containers backend: sessions, images, ports, mounts, wait strategies, logs/exec, diagnostics for WSLC. | | `Purview.Containers.Docker` | The Docker backend: the same containers on any reachable Docker daemon, driven by Testcontainers. | | `Purview.Containers.PostgreSql` | PostgreSQL container (`postgres:17`), `pg_isready` readiness, Npgsql connection string. | @@ -107,7 +108,8 @@ example and the troubleshooting reference. | Path | Purpose | | --- | --- | | `src/WSLTestContainers.slnx` | Canonical solution for restore, build, test and pack. | -| `src/src/Containers` | Backend-neutral abstractions (`Purview.Containers`): the container contract, builders and backend selection. | +| `src/src/Containers` | Umbrella package (`Purview.Containers`): references Core and both backends; no code of its own. | +| `src/src/Core` | Backend-neutral abstractions (`Purview.Containers.Core`, namespace `Purview.Containers`): the container contract, builders and backend selection. | | `src/src/Wsl` | WSL Containers backend (`Purview.Containers.Wsl`): containers, images, runtime, networking, mounts, diagnostics. | | `src/src/Docker` | Docker backend (`Purview.Containers.Docker`): the same containers on a Docker daemon, via Testcontainers. | | `src/src/` | Service modules; each carries a bespoke `Sdk/README.md` that ships as the package README. | diff --git a/docs/wiki/Modules.md b/docs/wiki/Modules.md index a90ee2b..e4af0e7 100644 --- a/docs/wiki/Modules.md +++ b/docs/wiki/Modules.md @@ -5,7 +5,7 @@ Module architecture for `Purview.Containers`. ## Principle Modules are thin packages layered on the backend-neutral abstractions -([`Purview.Containers`](Architecture.md)). A module supplies only: +([`Purview.Containers.Core`](Architecture.md)). A module supplies only: - default image - default ports diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index 7517325..89c61c9 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -9,22 +9,24 @@ Each package ships exactly: | Entry | Source | | --- | --- | -| `lib/$(TFM)/Purview.Containers.*.dll` | The built assembly: `net10.0` for `Purview.Containers` and the service modules; `Purview.Containers.Wsl` ships `net10.0` (the portable facade) and `net10.0-windows10.0.19041` (the implementation). | -| `lib/$(TFM)/Purview.Containers.Wsl.xml` / `lib/$(TFM)/Purview.Containers..xml` | XML documentation, generated because `GenerateDocumentationFile` is on for packable projects. | +| `lib/$(TFM)/Purview.Containers.*.dll` | The built assembly: `net10.0` for `Purview.Containers` (umbrella), `Purview.Containers.Core` and the service modules; `Purview.Containers.Wsl` ships `net10.0` (the portable facade) and `net10.0-windows10.0.19041` (the implementation). | +| `lib/$(TFM)/.xml` | XML documentation, generated because `GenerateDocumentationFile` is on for packable projects. | | `README.md` | The package's bespoke `Sdk/README.md` (see below). | | `purview-logo-light.png` | The shared Purview package icon (`assets/images/purview-logo-light.png`). | -| `buildTransitive/Purview.Containers.props` / `.targets` | **Abstractions package.** Declares the `PurviewContainersBackends` list and turns it into a generated backend initializer in the consuming assembly. | +| `buildTransitive/Purview.Containers.Core.props` / `.targets` | **Core (abstractions) package.** Declares the `PurviewContainersBackends` list and turns it into a generated backend initializer in the consuming assembly. | | `buildTransitive/Purview.Containers.Wsl.props` / `.targets` | **WSL Containers backend only.** Defaults `WindowsSdkPackageVersion`/`PlatformTarget` and raises `PCC0001`/`PCC0002` for unsupported consumers. | | `buildTransitive/Purview.Containers.Docker.props` | **Docker backend only.** Adds the Docker backend to the `PurviewContainersBackends` list. | | `payload/win-x64/*`, `payload/win-arm64/*` | **WSL Containers backend only.** The Windows implementation, the WSLC projection, its Windows SDK dependencies and the native SDK. The `net10.0` facade loads them at run time on a Windows host; the `buildTransitive` targets copy the matching folder to the consumer output. | -Portable PDBs are delivered through the `.snupkg`, never inside the `.nupkg`. +Portable PDBs are delivered through the `.snupkg`, never inside the `.nupkg`. The umbrella +`Purview.Containers` package has no `buildTransitive` of its own: its dependencies' assets flow +transitively. -`$(TFM)` is expanded per shipped framework: `Purview.Containers`, `Purview.Containers.Docker` and the -service modules ship `lib/net10.0/`; `Purview.Containers.Wsl` ships both `lib/net10.0/` (the facade) -and `lib/net10.0-windows10.0.19041/` (the implementation), with the `payload/` folders alongside. -Every package therefore restores on a portable `net10.0` project. Consumers must target .NET 10+; see -[Consumer Requirements](Consumer-Requirements.md). +`$(TFM)` is expanded per shipped framework: `Purview.Containers`, `Purview.Containers.Core`, +`Purview.Containers.Docker` and the service modules ship `lib/net10.0/`; `Purview.Containers.Wsl` ships +both `lib/net10.0/` (the facade) and `lib/net10.0-windows10.0.19041/` (the implementation), with the +`payload/` folders alongside. Every package therefore restores on a portable `net10.0` project. Consumers +must target .NET 10+; see [Consumer Requirements](Consumer-Requirements.md). ## Package metadata diff --git a/docs/wiki/Using-in-Your-Tests.md b/docs/wiki/Using-in-Your-Tests.md index 8f80a0e..6beddd1 100644 --- a/docs/wiki/Using-in-Your-Tests.md +++ b/docs/wiki/Using-in-Your-Tests.md @@ -15,9 +15,8 @@ a Mac) — nothing changes between them. - - - + + ``` @@ -28,12 +27,14 @@ Three things make this work, and nothing else is required: `net10.0` project binds its portable facade, which runs the WSLC implementation on Windows and reports `wsl` unavailable everywhere else. (A Windows target framework still works — it binds the implementation directly — but it cannot run on a Linux CI runner.) -- **Both backend packages.** WSLC is preferred where it is usable and Docker is the fallback, so the one - project runs everywhere. Referencing only the WSL backend leaves a Docker-only machine with nothing to - fall back to. +- **The umbrella package** (`Purview.Containers`), which brings `Purview.Containers.Core` (the + abstractions) and both backends. WSLC is preferred where it is usable and Docker is the fallback, so the + one project runs everywhere. Prefer to be explicit? Reference `Purview.Containers.Core` plus + `Purview.Containers.Wsl` and/or `Purview.Containers.Docker` individually — a Linux-only CI job can skip + the WSL backend and its ~19 MB payload with `Core` + `.Docker` only. - **No registration line, no environment variable.** Each backend package ships `buildTransitive` assets - that generate a module initializer in your assembly, so the process discovers `wsl` and `docker` by - itself. + that generate a module initializer in your assembly, so the process discovers the registered backends by + itself. Those assets flow transitively through the umbrella. ## The test diff --git a/purview-build.json b/purview-build.json index a5076d7..07f44fd 100644 --- a/purview-build.json +++ b/purview-build.json @@ -15,13 +15,22 @@ // (assets/images/purview-logo-light.png, linked as Sdk/purview-logo-light.png). A package with // no rule here, or an entry matching none of its globs, fails validation. "RequiredContent": { + // Purview.Containers is the umbrella: it has no buildTransitive of its own, it just depends on + // Core + Wsl + Docker, whose assets flow transitively. "purview.containers": [ "lib/$(TFM)/Purview.Containers.dll", "lib/$(TFM)/Purview.Containers.xml", "README.md", + "purview-logo-light.png" + ], + // Purview.Containers.Core owns the backend registration assets (buildTransitive). + "purview.containers.core": [ + "lib/$(TFM)/Purview.Containers.Core.dll", + "lib/$(TFM)/Purview.Containers.Core.xml", + "README.md", "purview-logo-light.png", - "buildTransitive/Purview.Containers.props", - "buildTransitive/Purview.Containers.targets" + "buildTransitive/Purview.Containers.Core.props", + "buildTransitive/Purview.Containers.Core.targets" ], "purview.containers.docker": [ "lib/$(TFM)/Purview.Containers.Docker.dll", diff --git a/samples/getting-started/AutoSample/AutoSample.csproj b/samples/getting-started/AutoSample/AutoSample.csproj index 10dc53d..f9ed7fa 100644 --- a/samples/getting-started/AutoSample/AutoSample.csproj +++ b/samples/getting-started/AutoSample/AutoSample.csproj @@ -19,16 +19,16 @@ - - + $(MSBuildThisFileDirectory)../../../src/src/Wsl/bin/$(Configuration)/net10.0-windows10.0.19041.0/ diff --git a/samples/getting-started/README.md b/samples/getting-started/README.md index 6ba8130..2290964 100644 --- a/samples/getting-started/README.md +++ b/samples/getting-started/README.md @@ -2,15 +2,16 @@ Three runnable samples. `WslSample` and `DockerSample` differ in exactly one thing — which backend they use — and the container code is identical, which is the point. `AutoSample` removes even that choice: it -references both backends and lets automatic selection decide, so the same project runs on WSL Containers -on Windows and Docker everywhere else. See [Backends: WSLC or Docker](../../docs/wiki/Backends.md) and +references the umbrella (`Purview.Containers`) and lets automatic selection decide, so the same project +runs on WSL Containers on Windows and Docker everywhere else. See +[Backends: WSLC or Docker](../../docs/wiki/Backends.md) and [Using it in your tests](../../docs/wiki/Using-in-Your-Tests.md). | Sample | Backend | Target framework | Prerequisite | | --- | --- | --- | --- | | `WslSample` | `Purview.Containers.Wsl` | `net11.0-windows10.0.19041.0` | Windows with WSL Containers (`wsl --install --no-distribution`) | | `DockerSample` | `Purview.Containers.Docker` | `net10.0` | a reachable Docker daemon (`docker info`) | -| `AutoSample` | both (auto) | `net10.0` | either — WSLC on Windows, Docker elsewhere | +| `AutoSample` | umbrella (auto) | `net10.0` | either — WSLC on Windows, Docker elsewhere | Run them from the repository root: diff --git a/scripts/verify-consumers.ps1 b/scripts/verify-consumers.ps1 index e293d5c..545f98a 100644 --- a/scripts/verify-consumers.ps1 +++ b/scripts/verify-consumers.ps1 @@ -24,14 +24,15 @@ 10 the net11.0-windows shorthand TFM (no OS version) -> PCC0001 11 multi-targeting with a conditional PackageReference -> builds 12 multi-targeting with an unconditional PackageReference -> builds (both inner builds supported) - 13 net10.0 + portable abstractions -> builds + 13 net10.0 + core abstractions -> builds 14 net10.0 + Docker backend -> builds, backend registration generated 15 net10.0 + service module (no backend package) -> builds 16 net11.0 windows + both backends (auto detection) -> builds, both registrations generated 17 the documented backend example, on WSL Containers -> builds 18 the documented backend example, on Docker (net10.0) -> builds - 19 plain net10.0 + WSL backend (portable facade, auto) -> builds, wsl registration generated - 20 net10.0 + Redis/PostgreSql + both backends (auto) -> builds, both registrations generated + 19 plain net10.0 + WSL backend (portable facade, auto) -> builds, wsl registration generated + 20 net10.0 + Redis/PostgreSql + both backends (auto) -> builds, both registrations generated + 21 net10.0 + umbrella Purview.Containers + modules (one ref) -> builds, both registrations generated .PARAMETER FeedPath Folder holding the packed .nupkg files. Defaults to /artifacts. @@ -229,6 +230,15 @@ $bothBackendsItems = @' '@ +# The umbrella shape: one backend reference (Purview.Containers, which brings Core + WSL + Docker) plus the +# service modules. This is the "one reference" story documented in docs/wiki/Using-in-Your-Tests.md. +$umbrellaItems = @' + + + + +'@ + # The realistic Windows consumer shape: a service module plus the WSL Containers backend. The module is # backend-neutral, so the backend package is what supplies the WSL build defaults and the guards. $wslBackendItems = @' @@ -375,9 +385,9 @@ $cases = @( -Items $multiTargetItems ` -Sources @{ 'Smoke.Windows.cs' = $apiSource; 'Smoke.Portable.cs' = $portableSource } - New-ConsumerCase -Id '13' -Name 'net10 + portable abstractions' ` + New-ConsumerCase -Id '13' -Name 'net10 + core abstractions' ` -Framework 'net10.0' ` - -Package 'Purview.Containers' ` + -Package 'Purview.Containers.Core' ` -Sources @{ 'Smoke.Portable.cs' = $portableSource } New-ConsumerCase -Id '14' -Name 'net10 + docker backend (generated registration)' ` @@ -417,6 +427,13 @@ $cases = @( -Items $autoItems ` -Sources @{ 'Smoke.Auto.cs' = $autoSource } ` -RequireText @('WslContainerBackend.Create()', 'DockerContainerBackend.Create()') + + New-ConsumerCase -Id '21' -Name 'net10.0 + umbrella Purview.Containers + modules (one reference, auto)' ` + -Framework 'net10.0' ` + -Package 'Purview.Containers' ` + -Items $umbrellaItems ` + -Sources @{ 'Smoke.Auto.cs' = $autoSource } ` + -RequireText @('WslContainerBackend.Create()', 'DockerContainerBackend.Create()') ) diff --git a/src/WSLTestContainers.slnx b/src/WSLTestContainers.slnx index a7df6cb..6b9ec5c 100644 --- a/src/WSLTestContainers.slnx +++ b/src/WSLTestContainers.slnx @@ -12,6 +12,7 @@ + diff --git a/src/src/Azurite/Azurite.csproj b/src/src/Azurite/Azurite.csproj index 739cdc4..21d1e7c 100644 --- a/src/src/Azurite/Azurite.csproj +++ b/src/src/Azurite/Azurite.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/src/Azurite/Sdk/README.md b/src/src/Azurite/Sdk/README.md index a00a6c2..df24de4 100644 --- a/src/src/Azurite/Sdk/README.md +++ b/src/src/Azurite/Sdk/README.md @@ -7,7 +7,7 @@ integration testing on **WSL Containers (WSLC)** or **Docker**. dotnet add package Purview.Containers.Azurite ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). See the [Getting Started guide](https://github.com/purview-dev/containers/blob/main/docs/wiki/Getting-Started.md). ## Requirements diff --git a/src/src/Containers/Containers.csproj b/src/src/Containers/Containers.csproj index e919dd5..aa5b8c6 100644 --- a/src/src/Containers/Containers.csproj +++ b/src/src/Containers/Containers.csproj @@ -1,9 +1,22 @@ true - Backend-neutral container testing abstractions for .NET: a Testcontainers-style API that runs the same test code on WSL Containers or Docker. - containers;testing;integration;testcontainers;abstractions;docker;wsl + + Purview.Containers: the umbrella package. Brings the abstractions (Purview.Containers.Core) and both backends (WSL Containers and Docker), so one reference runs the same tests on WSLC on Windows and Docker everywhere else. + containers;testing;integration;testcontainers;docker;wsl;wslc;bundle MIT Purview + + + + + + diff --git a/src/src/Containers/Sdk/README.md b/src/src/Containers/Sdk/README.md index ae38fd8..8c2795d 100644 --- a/src/src/Containers/Sdk/README.md +++ b/src/src/Containers/Sdk/README.md @@ -1,68 +1,72 @@ # Purview.Containers -Backend-neutral container testing abstractions for .NET — the shared surface behind -[`Purview.Containers.Wsl`](https://www.nuget.org/packages/Purview.Containers.Wsl) (WSL Containers) and -`Purview.Containers.Docker` (Docker Engine via Testcontainers). +The **umbrella package**: one reference that brings the abstractions and both backends, so the same tests +run on **WSL Containers (WSLC)** on a Windows machine and on **Docker** everywhere else — no backend +choice in your code or environment. ```bash dotnet add package Purview.Containers ``` -Most consumers reference a backend package or a service module instead; this package arrives -transitively and supplies the model plus the backend registration hook. +It depends on: -## What is in the package +- [`Purview.Containers.Core`](https://www.nuget.org/packages/Purview.Containers.Core) — the backend-neutral + abstractions (`IContainer`, `ContainerBuilder`, `ContainerBackends`, wait strategies, …; public namespace + `Purview.Containers`). +- [`Purview.Containers.Wsl`](https://www.nuget.org/packages/Purview.Containers.Wsl) — the WSL Containers + backend. +- [`Purview.Containers.Docker`](https://www.nuget.org/packages/Purview.Containers.Docker) — the Docker + backend (Testcontainers). -| Area | Types | -| --- | --- | -| Container contract | `IContainer`, `IContainerConfiguration`, `ContainerConfiguration`, `ContainerBuilder`, `ContainerBase` | -| Backends | `IContainerBackend`, `ContainerBackendInfo`, `ContainerBackends`, `ContainerBackendUnavailableException` | -| Readiness | `Purview.Containers.Waiting`: `Wait`, `IWaitStrategy`, TCP/HTTP/command/log strategies | -| Configuration model | `PortBinding`, `BindMount`, `NamedVolume`, `RegistryCredentials`, `Image`, `PullPolicy` | -| Diagnostics | `Secret`, `SecretRedactor`, `ContainerState`, `ExecResult`, `ContainerLogEntry` | -| Errors | `Purview.Containers.Runtime`: `ContainerException` and its specialisations | +## Quick start -## Backend selection +A service module plus this package is all you need: -A container is produced by an `IContainerBackend`. Backend packages register themselves into the -consuming assembly through the `buildTransitive` assets shipped here: +```bash +dotnet add package Purview.Containers.Redis +dotnet add package Purview.Containers +``` ```csharp -// generated by Purview.Containers.targets -[ModuleInitializer] -internal static void Initialize() -{ - ContainerBackends.Register(WslContainerBackend.Create()); -} +using Purview.Containers.Redis; +using StackExchange.Redis; + +await using var redis = new RedisBuilder().Build(); +await redis.StartAsync(); // waits for redis-cli ping + +await using var cache = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString()); +await cache.GetDatabase().PingAsync(); ``` -`ContainerBackends.ResolveAsync()` picks the backend with this precedence: +There is no registration line and no `PURVIEW_CONTAINERS_BACKEND`: each backend package ships +`buildTransitive` assets that register themselves in your assembly, and automatic selection picks the +first usable backend (WSLC is preferred where it works, Docker otherwise). Use a **`net10.0`** (or later) +project — see [Using it in your tests](https://github.com/purview-dev/containers/blob/main/docs/wiki/Using-in-Your-Tests.md). -| # | Rule | How | -| --- | --- | --- | -| 1 | Pinned instance | `ContainerBackends.Use(new WslContainerBackend())`, or `WithBackend(...)` on one builder | -| 2 | Named backend | `ContainerBackends.Use(ContainerBackendSelection.Named("docker"))`, or `PURVIEW_CONTAINERS_BACKEND=docker` | -| 3 | Automatic detection | probe every registered backend; the first usable one wins (WSLC before Docker) | +## When to use this versus a single backend -`PURVIEW_CONTAINERS_BACKEND` accepts `auto` (the default) or a backend name, and is case-insensitive. -A named or pinned backend **never** silently falls back: if it is missing or unusable, the resolution -fails with that backend's own diagnostics. When nothing is usable, the error lists every backend's -availability, version and missing components, and names the fix. Results are cached per process, so the -probe cost is paid once. +| You want | Reference | +| --- | --- | +| WSLC locally **and** Docker in CI, one project, zero config | `Purview.Containers` (this package) | +| Docker only (e.g. a Linux-only CI job) — avoids the ~19 MB WSLC payload | `Purview.Containers.Core` + `Purview.Containers.Docker` | +| WSLC only | `Purview.Containers.Core` + `Purview.Containers.Wsl` | -`ContainerBackends.ProbeAllAsync()` returns the same probe information without selecting anything, for -callers that want to apply their own policy. +Adding a future backend (for example Podman) means adding its package to this bundle; your project does +not change. ## Requirements -- **.NET 10 or later**, any platform (this package is portable). -- A **backend package**: `Purview.Containers.Wsl` (WSLC on a Windows host, Docker elsewhere) or - `Purview.Containers.Docker` (Docker Engine reachable from the test host). +- **.NET 10 or later.** The abstractions and service modules are portable; the WSL Containers backend is + multi-target and stays dormant on non-Windows hosts. See the + [consumer requirements](https://github.com/purview-dev/containers/blob/main/docs/wiki/Consumer-Requirements.md). +- Either a Windows 10/11 host with **WSL Containers** (`wsl --install --no-distribution`) or a reachable + **Docker** daemon. -## Related +## Documentation -- [Purview.Containers.Wsl](https://www.nuget.org/packages/Purview.Containers.Wsl) — WSL Containers backend. -- Documentation wiki: . +- [Using it in your tests (auto)](https://github.com/purview-dev/containers/blob/main/docs/wiki/Using-in-Your-Tests.md) +- [Backends: WSLC or Docker](https://github.com/purview-dev/containers/blob/main/docs/wiki/Backends.md) +- [Modules](https://github.com/purview-dev/containers/blob/main/docs/wiki/Modules.md) -> **Experimental.** The public API, defaults and packaging rules can change between prereleases, and -> there is no production support guarantee. Pin the exact package version you build against. +> **Experimental.** The public API, defaults and packaging rules can change between prereleases; there is +> no production support guarantee. \ No newline at end of file diff --git a/src/src/Containers/Container.cs b/src/src/Core/Container.cs similarity index 100% rename from src/src/Containers/Container.cs rename to src/src/Core/Container.cs diff --git a/src/src/Containers/ContainerBackendInfo.cs b/src/src/Core/ContainerBackendInfo.cs similarity index 100% rename from src/src/Containers/ContainerBackendInfo.cs rename to src/src/Core/ContainerBackendInfo.cs diff --git a/src/src/Containers/ContainerBackendSelection.cs b/src/src/Core/ContainerBackendSelection.cs similarity index 100% rename from src/src/Containers/ContainerBackendSelection.cs rename to src/src/Core/ContainerBackendSelection.cs diff --git a/src/src/Containers/ContainerBackends.cs b/src/src/Core/ContainerBackends.cs similarity index 100% rename from src/src/Containers/ContainerBackends.cs rename to src/src/Core/ContainerBackends.cs diff --git a/src/src/Containers/ContainerBase.cs b/src/src/Core/ContainerBase.cs similarity index 100% rename from src/src/Containers/ContainerBase.cs rename to src/src/Core/ContainerBase.cs diff --git a/src/src/Containers/ContainerBuilder.cs b/src/src/Core/ContainerBuilder.cs similarity index 100% rename from src/src/Containers/ContainerBuilder.cs rename to src/src/Core/ContainerBuilder.cs diff --git a/src/src/Containers/ContainerConfiguration.cs b/src/src/Core/ContainerConfiguration.cs similarity index 100% rename from src/src/Containers/ContainerConfiguration.cs rename to src/src/Core/ContainerConfiguration.cs diff --git a/src/src/Containers/ContainerLogEntry.cs b/src/src/Core/ContainerLogEntry.cs similarity index 100% rename from src/src/Containers/ContainerLogEntry.cs rename to src/src/Core/ContainerLogEntry.cs diff --git a/src/src/Containers/ContainerName.cs b/src/src/Core/ContainerName.cs similarity index 100% rename from src/src/Containers/ContainerName.cs rename to src/src/Core/ContainerName.cs diff --git a/src/src/Containers/ContainerState.cs b/src/src/Core/ContainerState.cs similarity index 100% rename from src/src/Containers/ContainerState.cs rename to src/src/Core/ContainerState.cs diff --git a/src/src/Core/Core.csproj b/src/src/Core/Core.csproj new file mode 100644 index 0000000..38cc6ff --- /dev/null +++ b/src/src/Core/Core.csproj @@ -0,0 +1,9 @@ + + + true + Backend-neutral container testing abstractions for .NET: the shared Testcontainers-style model behind Purview.Containers.Wsl and Purview.Containers.Docker. + containers;testing;integration;testcontainers;abstractions;docker;wsl + MIT + Purview + + diff --git a/src/src/Containers/Diagnostics/ContainerActivity.cs b/src/src/Core/Diagnostics/ContainerActivity.cs similarity index 100% rename from src/src/Containers/Diagnostics/ContainerActivity.cs rename to src/src/Core/Diagnostics/ContainerActivity.cs diff --git a/src/src/Containers/Diagnostics/Secret.cs b/src/src/Core/Diagnostics/Secret.cs similarity index 100% rename from src/src/Containers/Diagnostics/Secret.cs rename to src/src/Core/Diagnostics/Secret.cs diff --git a/src/src/Containers/Diagnostics/SecretRedactor.cs b/src/src/Core/Diagnostics/SecretRedactor.cs similarity index 100% rename from src/src/Containers/Diagnostics/SecretRedactor.cs rename to src/src/Core/Diagnostics/SecretRedactor.cs diff --git a/src/src/Containers/ExecOptions.cs b/src/src/Core/ExecOptions.cs similarity index 100% rename from src/src/Containers/ExecOptions.cs rename to src/src/Core/ExecOptions.cs diff --git a/src/src/Containers/ExecResult.cs b/src/src/Core/ExecResult.cs similarity index 100% rename from src/src/Containers/ExecResult.cs rename to src/src/Core/ExecResult.cs diff --git a/src/src/Containers/IContainer.cs b/src/src/Core/IContainer.cs similarity index 100% rename from src/src/Containers/IContainer.cs rename to src/src/Core/IContainer.cs diff --git a/src/src/Containers/IContainerBackend.cs b/src/src/Core/IContainerBackend.cs similarity index 100% rename from src/src/Containers/IContainerBackend.cs rename to src/src/Core/IContainerBackend.cs diff --git a/src/src/Containers/IContainerBackendPreference.cs b/src/src/Core/IContainerBackendPreference.cs similarity index 100% rename from src/src/Containers/IContainerBackendPreference.cs rename to src/src/Core/IContainerBackendPreference.cs diff --git a/src/src/Containers/IContainerBuilder.cs b/src/src/Core/IContainerBuilder.cs similarity index 100% rename from src/src/Containers/IContainerBuilder.cs rename to src/src/Core/IContainerBuilder.cs diff --git a/src/src/Containers/IContainerConfiguration.cs b/src/src/Core/IContainerConfiguration.cs similarity index 100% rename from src/src/Containers/IContainerConfiguration.cs rename to src/src/Core/IContainerConfiguration.cs diff --git a/src/src/Containers/Images/Image.cs b/src/src/Core/Images/Image.cs similarity index 100% rename from src/src/Containers/Images/Image.cs rename to src/src/Core/Images/Image.cs diff --git a/src/src/Containers/Images/ImagePullProgress.cs b/src/src/Core/Images/ImagePullProgress.cs similarity index 100% rename from src/src/Containers/Images/ImagePullProgress.cs rename to src/src/Core/Images/ImagePullProgress.cs diff --git a/src/src/Containers/Images/ImageSummary.cs b/src/src/Core/Images/ImageSummary.cs similarity index 100% rename from src/src/Containers/Images/ImageSummary.cs rename to src/src/Core/Images/ImageSummary.cs diff --git a/src/src/Containers/Images/PullPolicy.cs b/src/src/Core/Images/PullPolicy.cs similarity index 100% rename from src/src/Containers/Images/PullPolicy.cs rename to src/src/Core/Images/PullPolicy.cs diff --git a/src/src/Containers/LogStream.cs b/src/src/Core/LogStream.cs similarity index 100% rename from src/src/Containers/LogStream.cs rename to src/src/Core/LogStream.cs diff --git a/src/src/Containers/Mounts/BindMount.cs b/src/src/Core/Mounts/BindMount.cs similarity index 100% rename from src/src/Containers/Mounts/BindMount.cs rename to src/src/Core/Mounts/BindMount.cs diff --git a/src/src/Containers/Mounts/NamedVolume.cs b/src/src/Core/Mounts/NamedVolume.cs similarity index 100% rename from src/src/Containers/Mounts/NamedVolume.cs rename to src/src/Core/Mounts/NamedVolume.cs diff --git a/src/src/Containers/Networking/ContainerNetworkingMode.cs b/src/src/Core/Networking/ContainerNetworkingMode.cs similarity index 100% rename from src/src/Containers/Networking/ContainerNetworkingMode.cs rename to src/src/Core/Networking/ContainerNetworkingMode.cs diff --git a/src/src/Containers/Networking/PortBinding.cs b/src/src/Core/Networking/PortBinding.cs similarity index 100% rename from src/src/Containers/Networking/PortBinding.cs rename to src/src/Core/Networking/PortBinding.cs diff --git a/src/src/Containers/Networking/PortProtocol.cs b/src/src/Core/Networking/PortProtocol.cs similarity index 100% rename from src/src/Containers/Networking/PortProtocol.cs rename to src/src/Core/Networking/PortProtocol.cs diff --git a/src/src/Containers/RegistryCredentials.cs b/src/src/Core/RegistryCredentials.cs similarity index 100% rename from src/src/Containers/RegistryCredentials.cs rename to src/src/Core/RegistryCredentials.cs diff --git a/src/src/Containers/Runtime/ContainerException.cs b/src/src/Core/Runtime/ContainerException.cs similarity index 100% rename from src/src/Containers/Runtime/ContainerException.cs rename to src/src/Core/Runtime/ContainerException.cs diff --git a/src/src/Core/Sdk/README.md b/src/src/Core/Sdk/README.md new file mode 100644 index 0000000..8540ea7 --- /dev/null +++ b/src/src/Core/Sdk/README.md @@ -0,0 +1,71 @@ +# Purview.Containers.Core + +Backend-neutral container testing abstractions for .NET — the shared surface behind +[`Purview.Containers.Wsl`](https://www.nuget.org/packages/Purview.Containers.Wsl) (WSL Containers) and +`Purview.Containers.Docker` (Docker Engine via Testcontainers). + +```bash +dotnet add package Purview.Containers.Core +``` + +Most consumers reference [`Purview.Containers`](https://www.nuget.org/packages/Purview.Containers) (the +bundle that brings this package plus both backends), a single backend package, or a service module +instead; this package arrives transitively and supplies the model plus the backend registration hook. The +public namespace is `Purview.Containers`, so consumer code that uses it never needs to know it is a +separate package. + +## What is in the package + +| Area | Types | +| --- | --- | +| Container contract | `IContainer`, `IContainerConfiguration`, `ContainerConfiguration`, `ContainerBuilder`, `ContainerBase` | +| Backends | `IContainerBackend`, `ContainerBackendInfo`, `ContainerBackends`, `ContainerBackendUnavailableException` | +| Readiness | `Purview.Containers.Waiting`: `Wait`, `IWaitStrategy`, TCP/HTTP/command/log strategies | +| Configuration model | `PortBinding`, `BindMount`, `NamedVolume`, `RegistryCredentials`, `Image`, `PullPolicy` | +| Diagnostics | `Secret`, `SecretRedactor`, `ContainerState`, `ExecResult`, `ContainerLogEntry` | +| Errors | `Purview.Containers.Runtime`: `ContainerException` and its specialisations | + +## Backend selection + +A container is produced by an `IContainerBackend`. Backend packages register themselves into the +consuming assembly through the `buildTransitive` assets shipped here: + +```csharp +// generated by Purview.Containers.Core.targets +[ModuleInitializer] +internal static void Initialize() +{ + ContainerBackends.Register(WslContainerBackend.Create()); +} +``` + +`ContainerBackends.ResolveAsync()` picks the backend with this precedence: + +| # | Rule | How | +| --- | --- | --- | +| 1 | Pinned instance | `ContainerBackends.Use(new WslContainerBackend())`, or `WithBackend(...)` on one builder | +| 2 | Named backend | `ContainerBackends.Use(ContainerBackendSelection.Named("docker"))`, or `PURVIEW_CONTAINERS_BACKEND=docker` | +| 3 | Automatic detection | probe every registered backend; the first usable one wins (WSLC before Docker) | + +`PURVIEW_CONTAINERS_BACKEND` accepts `auto` (the default) or a backend name, and is case-insensitive. +A named or pinned backend **never** silently falls back: if it is missing or unusable, the resolution +fails with that backend's own diagnostics. When nothing is usable, the error lists every backend's +availability, version and missing components, and names the fix. Results are cached per process, so the +probe cost is paid once. + +`ContainerBackends.ProbeAllAsync()` returns the same probe information without selecting anything, for +callers that want to apply their own policy. + +## Requirements + +- **.NET 10 or later**, any platform (this package is portable). +- A **backend package**: `Purview.Containers.Wsl` (WSLC on a Windows host, Docker elsewhere) or + `Purview.Containers.Docker` (Docker Engine reachable from the test host). + +## Related + +- [Purview.Containers.Wsl](https://www.nuget.org/packages/Purview.Containers.Wsl) — WSL Containers backend. +- Documentation wiki: . + +> **Experimental.** The public API, defaults and packaging rules can change between prereleases, and +> there is no production support guarantee. Pin the exact package version you build against. diff --git a/src/src/Containers/Sdk/buildTransitive/Purview.Containers.props b/src/src/Core/Sdk/buildTransitive/Purview.Containers.Core.props similarity index 89% rename from src/src/Containers/Sdk/buildTransitive/Purview.Containers.props rename to src/src/Core/Sdk/buildTransitive/Purview.Containers.Core.props index 87aaea5..d3c7734 100644 --- a/src/src/Containers/Sdk/buildTransitive/Purview.Containers.props +++ b/src/src/Core/Sdk/buildTransitive/Purview.Containers.Core.props @@ -1,8 +1,8 @@ diff --git a/src/src/Containers/Waiting/AllWaitStrategy.cs b/src/src/Core/Waiting/AllWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/AllWaitStrategy.cs rename to src/src/Core/Waiting/AllWaitStrategy.cs diff --git a/src/src/Containers/Waiting/AnyWaitStrategy.cs b/src/src/Core/Waiting/AnyWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/AnyWaitStrategy.cs rename to src/src/Core/Waiting/AnyWaitStrategy.cs diff --git a/src/src/Containers/Waiting/CommandWaitStrategy.cs b/src/src/Core/Waiting/CommandWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/CommandWaitStrategy.cs rename to src/src/Core/Waiting/CommandWaitStrategy.cs diff --git a/src/src/Containers/Waiting/ContainerRunningWaitStrategy.cs b/src/src/Core/Waiting/ContainerRunningWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/ContainerRunningWaitStrategy.cs rename to src/src/Core/Waiting/ContainerRunningWaitStrategy.cs diff --git a/src/src/Containers/Waiting/CustomWaitStrategy.cs b/src/src/Core/Waiting/CustomWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/CustomWaitStrategy.cs rename to src/src/Core/Waiting/CustomWaitStrategy.cs diff --git a/src/src/Containers/Waiting/HttpWaitStrategy.cs b/src/src/Core/Waiting/HttpWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/HttpWaitStrategy.cs rename to src/src/Core/Waiting/HttpWaitStrategy.cs diff --git a/src/src/Containers/Waiting/IWaitStrategy.cs b/src/src/Core/Waiting/IWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/IWaitStrategy.cs rename to src/src/Core/Waiting/IWaitStrategy.cs diff --git a/src/src/Containers/Waiting/LogMessageWaitStrategy.cs b/src/src/Core/Waiting/LogMessageWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/LogMessageWaitStrategy.cs rename to src/src/Core/Waiting/LogMessageWaitStrategy.cs diff --git a/src/src/Containers/Waiting/TcpPortWaitStrategy.cs b/src/src/Core/Waiting/TcpPortWaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/TcpPortWaitStrategy.cs rename to src/src/Core/Waiting/TcpPortWaitStrategy.cs diff --git a/src/src/Containers/Waiting/Wait.cs b/src/src/Core/Waiting/Wait.cs similarity index 100% rename from src/src/Containers/Waiting/Wait.cs rename to src/src/Core/Waiting/Wait.cs diff --git a/src/src/Containers/Waiting/WaitContext.cs b/src/src/Core/Waiting/WaitContext.cs similarity index 100% rename from src/src/Containers/Waiting/WaitContext.cs rename to src/src/Core/Waiting/WaitContext.cs diff --git a/src/src/Containers/Waiting/WaitStrategy.cs b/src/src/Core/Waiting/WaitStrategy.cs similarity index 100% rename from src/src/Containers/Waiting/WaitStrategy.cs rename to src/src/Core/Waiting/WaitStrategy.cs diff --git a/src/src/Containers/Waiting/WaitStrategyRunner.cs b/src/src/Core/Waiting/WaitStrategyRunner.cs similarity index 100% rename from src/src/Containers/Waiting/WaitStrategyRunner.cs rename to src/src/Core/Waiting/WaitStrategyRunner.cs diff --git a/src/src/Docker/Docker.csproj b/src/src/Docker/Docker.csproj index 7d53bce..e82ba9a 100644 --- a/src/src/Docker/Docker.csproj +++ b/src/src/Docker/Docker.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/src/Docker/Sdk/README.md b/src/src/Docker/Sdk/README.md index 56a7080..c6f9676 100644 --- a/src/src/Docker/Sdk/README.md +++ b/src/src/Docker/Sdk/README.md @@ -1,6 +1,6 @@ # Purview.Containers.Docker -The **Docker backend** for [`Purview.Containers`](https://www.nuget.org/packages/Purview.Containers): +The **Docker backend** for [`Purview.Containers.Core`](https://www.nuget.org/packages/Purview.Containers.Core): throwaway containers for integration testing on any Docker daemon reachable from the test host — Docker Desktop, Docker Engine inside WSL2, a remote daemon, Docker-in-Docker, or the daemon a CI runner provides. It drives the daemon through [Testcontainers for .NET](https://dotnet.testcontainers.org/). diff --git a/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props b/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props index c17c2f9..1c8854d 100644 --- a/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props +++ b/src/src/Docker/Sdk/buildTransitive/Purview.Containers.Docker.props @@ -3,7 +3,7 @@ Purview.Containers.Docker backend registration (props). Appends the Docker backend to the PurviewContainersBackends list, which - Purview.Containers.targets (shipped by the Purview.Containers package) turns into a generated + Purview.Containers.Core.targets (shipped by the Purview.Containers.Core package) turns into a generated module initializer in the consuming assembly. Referencing this package is therefore all a consumer needs to make "docker" selectable. NOTE: NuGet only auto-imports buildTransitive/.props and buildTransitive/.targets. diff --git a/src/src/MsSql/MsSql.csproj b/src/src/MsSql/MsSql.csproj index 7d36bec..6baa24c 100644 --- a/src/src/MsSql/MsSql.csproj +++ b/src/src/MsSql/MsSql.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/src/MsSql/Sdk/README.md b/src/src/MsSql/Sdk/README.md index 8f19c99..15a2083 100644 --- a/src/src/MsSql/Sdk/README.md +++ b/src/src/MsSql/Sdk/README.md @@ -6,7 +6,7 @@ Throwaway Microsoft SQL Server databases for .NET integration testing on **WSL C dotnet add package Purview.Containers.MsSql ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Microsoft.Data.SqlClient` for +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Microsoft.Data.SqlClient` for connection-string generation and readiness probing. ## Requirements diff --git a/src/src/MySql/MySql.csproj b/src/src/MySql/MySql.csproj index fbba328..0707306 100644 --- a/src/src/MySql/MySql.csproj +++ b/src/src/MySql/MySql.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/src/MySql/Sdk/README.md b/src/src/MySql/Sdk/README.md index 5480449..11185b0 100644 --- a/src/src/MySql/Sdk/README.md +++ b/src/src/MySql/Sdk/README.md @@ -6,7 +6,7 @@ Throwaway MySQL databases for .NET integration testing on **WSL Containers (WSLC dotnet add package Purview.Containers.MySql ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `MySqlConnector` for readiness probing and +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `MySqlConnector` for readiness probing and connection-string generation. ## Requirements diff --git a/src/src/Nats/Nats.csproj b/src/src/Nats/Nats.csproj index 6377eb2..28391cf 100644 --- a/src/src/Nats/Nats.csproj +++ b/src/src/Nats/Nats.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/src/Nats/Sdk/README.md b/src/src/Nats/Sdk/README.md index 4d0d9e2..07620e4 100644 --- a/src/src/Nats/Sdk/README.md +++ b/src/src/Nats/Sdk/README.md @@ -6,7 +6,7 @@ Throwaway NATS brokers for .NET integration testing on **WSL Containers (WSLC)** dotnet add package Purview.Containers.Nats ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `NATS.Client.Core` is not referenced by this +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `NATS.Client.Core` is not referenced by this package — bring your own client. ## Requirements diff --git a/src/src/PostgreSql/PostgreSql.csproj b/src/src/PostgreSql/PostgreSql.csproj index 934168f..5c1ad01 100644 --- a/src/src/PostgreSql/PostgreSql.csproj +++ b/src/src/PostgreSql/PostgreSql.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/src/PostgreSql/Sdk/README.md b/src/src/PostgreSql/Sdk/README.md index 9dd1b4c..db8a80c 100644 --- a/src/src/PostgreSql/Sdk/README.md +++ b/src/src/PostgreSql/Sdk/README.md @@ -6,7 +6,7 @@ Throwaway PostgreSQL databases for .NET integration testing on **WSL Containers dotnet add package Purview.Containers.PostgreSql ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Npgsql` for connection-string generation. +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`) and brings `Npgsql` for connection-string generation. See the [Getting Started guide](https://github.com/purview-dev/containers/blob/main/docs/wiki/Getting-Started.md). ## Requirements diff --git a/src/src/RabbitMq/RabbitMq.csproj b/src/src/RabbitMq/RabbitMq.csproj index 1e96424..b1adda6 100644 --- a/src/src/RabbitMq/RabbitMq.csproj +++ b/src/src/RabbitMq/RabbitMq.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/src/RabbitMq/Sdk/README.md b/src/src/RabbitMq/Sdk/README.md index 7b5bb9d..dfab0fa 100644 --- a/src/src/RabbitMq/Sdk/README.md +++ b/src/src/RabbitMq/Sdk/README.md @@ -6,7 +6,7 @@ Throwaway RabbitMQ brokers for .NET integration testing on **WSL Containers (WSL dotnet add package Purview.Containers.RabbitMq ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `RabbitMQ.Client` is not referenced by this package — +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `RabbitMQ.Client` is not referenced by this package — bring your own client. ## Requirements diff --git a/src/src/Redis/Redis.csproj b/src/src/Redis/Redis.csproj index 0ed8738..db6d562 100644 --- a/src/src/Redis/Redis.csproj +++ b/src/src/Redis/Redis.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/src/Redis/Sdk/README.md b/src/src/Redis/Sdk/README.md index 4c7c914..935d064 100644 --- a/src/src/Redis/Sdk/README.md +++ b/src/src/Redis/Sdk/README.md @@ -6,7 +6,7 @@ Throwaway Redis-compatible instances for .NET integration testing on **WSL Conta dotnet add package Purview.Containers.Redis ``` -Backend-neutral: depends on `Purview.Containers` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `StackExchange.Redis` is not referenced by this +Backend-neutral: depends on `Purview.Containers.Core` and needs a backend package (`Purview.Containers.Wsl` or `Purview.Containers.Docker`). `StackExchange.Redis` is not referenced by this package — bring your own client. ## Requirements diff --git a/src/src/Wsl/Sdk/README.md b/src/src/Wsl/Sdk/README.md index e90237a..e250015 100644 --- a/src/src/Wsl/Sdk/README.md +++ b/src/src/Wsl/Sdk/README.md @@ -1,6 +1,6 @@ # Purview.Containers.Wsl -The **WSL Containers (WSLC) backend** for [`Purview.Containers`](https://www.nuget.org/packages/Purview.Containers): +The **WSL Containers (WSLC) backend** for [`Purview.Containers.Core`](https://www.nuget.org/packages/Purview.Containers.Core): throwaway Linux containers for .NET integration testing built directly on the `Microsoft.WSL.Containers` managed API, with **no Docker installation** and no `wslc.exe`/`wsl.exe`/`docker` CLI, Docker.DotNet or Testcontainers dependency. diff --git a/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props index 9bf81c7..4829422 100644 --- a/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props +++ b/src/src/Wsl/Sdk/buildTransitive/Purview.Containers.Wsl.props @@ -20,8 +20,8 @@ >10.0.26100.80 + initializer is produced by Purview.Containers.Core.targets (shipped by the + Purview.Containers.Core package, which always flows here transitively). --> $(PurviewContainersBackends);Purview.Containers.Wsl.WslContainerBackend diff --git a/src/src/Wsl/Wsl.csproj b/src/src/Wsl/Wsl.csproj index f3dc77a..40e8610 100644 --- a/src/src/Wsl/Wsl.csproj +++ b/src/src/Wsl/Wsl.csproj @@ -37,7 +37,7 @@ - +