From f571cbb84d40df4c09fb7fd1ea480844b0cbd921 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 30 Sep 2026 16:26:55 +0100 Subject: [PATCH 1/4] fix: shared storage --- .editorconfig | 2702 ----------------- Directory.Build.props | 14 - Justfile | 6 +- README.md | 25 + docs/architecture.md | 18 +- docs/wslc-api-investigation.md | 3 +- global.json | 2 +- src/Directory.Build.props | 10 + .../Directory.Build.targets | 0 src/WSLTestContainers.slnx | 65 +- src/WslContainers.MinIo/MinIoContainer.cs | 31 - src/WslContainers/LogBuffer.cs | 137 - src/WslContainers/WslContainerRuntime.cs | 190 -- src/WslContainers/WslInspectParser.cs | 79 - .../Azurite/Azurite.csproj} | 0 .../Azurite}/AzuriteAccount.cs | 0 .../Azurite}/AzuriteBuilder.cs | 11 +- .../Azurite}/AzuriteConfiguration.cs | 0 .../Azurite}/AzuriteContainer.cs | 2 +- .../MinIo/MinIO.csproj} | 0 .../MinIo/MinIOBuilder.cs} | 63 +- .../MinIo/MinIOConfiguration.cs} | 6 +- src/src/MinIo/MinIOContainer.cs | 35 + .../MsSql/MsSql.csproj} | 0 .../MsSql}/MsSqlBuilder.cs | 39 +- .../MsSql}/MsSqlConfiguration.cs | 2 + .../MsSql}/MsSqlContainer.cs | 8 +- .../MySql/MySql.csproj} | 0 .../MySql}/MySqlBuilder.cs | 59 +- .../MySql}/MySqlConfiguration.cs | 2 + .../MySql}/MySqlContainer.cs | 12 +- .../Nats/Nats.csproj} | 0 .../Nats}/NatsBuilder.cs | 6 +- .../Nats}/NatsConfiguration.cs | 0 .../Nats}/NatsContainer.cs | 0 .../PostgreSql/PostgreSql.csproj} | 0 .../PostgreSql}/PostgreSqlBuilder.cs | 42 +- .../PostgreSql}/PostgreSqlConfiguration.cs | 2 + .../PostgreSql}/PostgreSqlContainer.cs | 12 +- .../RabbitMq/RabbitMq.csproj} | 0 .../RabbitMq}/RabbitMqBuilder.cs | 40 +- .../RabbitMq}/RabbitMqConfiguration.cs | 2 + .../RabbitMq}/RabbitMqContainer.cs | 12 +- .../Redis/Redis.csproj} | 0 .../Redis}/RedisBuilder.cs | 6 +- .../Redis}/RedisConfiguration.cs | 0 .../Redis}/RedisContainer.cs | 0 .../WslContainers/ContainerBuilder.cs | 68 +- .../WslContainers/ContainerConfiguration.cs | 9 +- .../Containers/ContainerLogEntry.cs | 2 +- .../Containers/ContainerState.cs | 2 +- .../WslContainers/Containers/ExecOptions.cs | 2 +- .../WslContainers/Containers/ExecResult.cs | 2 +- .../WslContainers/Containers/LogStream.cs | 2 +- .../WslContainers/Diagnostics/Secret.cs | 10 +- .../Diagnostics/SecretRedactor.cs | 16 +- .../Diagnostics/WslContainersActivity.cs | 6 +- src/{ => src}/WslContainers/IContainer.cs | 2 + .../WslContainers/IContainerBuilder.cs | 0 .../WslContainers/IContainerConfiguration.cs | 5 +- .../WslContainers/IContainerRuntime.cs | 0 .../WslContainers/IContainerSession.cs | 0 src/{ => src}/WslContainers/Images/Image.cs | 26 +- .../WslContainers/Images/ImagePullProgress.cs | 0 .../WslContainers/Images/ImageSummary.cs | 0 .../WslContainers/Images/PullPolicy.cs | 2 +- src/src/WslContainers/LogBuffer.cs | 149 + .../WslContainers/Mounts/BindMount.cs | 2 +- .../WslContainers/Mounts/NamedVolume.cs | 2 +- .../Networking/ContainerNetworkingMode.cs | 2 +- .../WslContainers/Networking/PortBinding.cs | 2 +- .../WslContainers/Networking/PortProtocol.cs | 2 +- .../WslContainers/RegistryCredentials.cs | 2 + .../Runtime/WslContainerException.cs | 24 +- src/{ => src}/WslContainers/StorageMode.cs | 2 +- .../WslContainers/Waiting/AllWaitStrategy.cs | 8 +- .../WslContainers/Waiting/AnyWaitStrategy.cs | 8 +- .../Waiting/CommandWaitStrategy.cs | 18 +- .../Waiting/ContainerRunningWaitStrategy.cs | 4 +- .../Waiting/CustomWaitStrategy.cs | 8 +- .../WslContainers/Waiting/HttpWaitStrategy.cs | 68 +- .../WslContainers/Waiting/IWaitStrategy.cs | 2 +- .../Waiting/LogMessageWaitStrategy.cs | 12 +- .../Waiting/TcpPortWaitStrategy.cs | 20 +- src/{ => src}/WslContainers/Waiting/Wait.cs | 2 +- .../WslContainers/Waiting/WaitContext.cs | 10 +- .../WslContainers/Waiting/WaitStrategy.cs | 2 +- .../Waiting/WaitStrategyRunner.cs | 43 +- src/{ => src}/WslContainers/WslContainer.cs | 156 +- src/src/WslContainers/WslContainerRuntime.cs | 246 ++ .../WslContainers/WslContainerRuntimeInfo.cs | 0 .../WslContainerRuntimeOptions.cs | 4 +- .../WslContainers/WslContainerSession.cs | 198 +- .../WslContainers/WslContainers.csproj | 0 src/src/WslContainers/WslInspectParser.cs | 79 + .../WslContainers/WslSettingsMapper.cs | 26 +- .../Azurite.IntegrationTests.csproj | 0 .../AzuriteIntegrationTests.cs | 21 +- .../Azurite.UnitTests.csproj | 0 .../Azurite.UnitTests}/AzuriteBuilderTests.cs | 21 +- .../MinIO.IntegrationTests.csproj | 0 .../MinIOIntegrationTests.cs | 34 +- .../MinIo.UnitTests/MinIO.UnitTests.csproj | 0 .../MinIo.UnitTests/MinIOBuilderTests.cs | 19 +- .../MsSql.IntegrationTests.csproj | 0 .../MsSqlIntegrationTests.cs | 22 +- .../MsSql.UnitTests/MsSql.UnitTests.csproj | 0 .../MsSql.UnitTests}/MsSqlBuilderTests.cs | 21 +- .../MySql.IntegrationTests.csproj | 0 .../MySqlIntegrationTests.cs | 44 + .../MySql.UnitTests/MySql.UnitTests.csproj | 0 .../MySql.UnitTests}/MySqlBuilderTests.cs | 17 +- .../Nats.IntegrationTests.csproj | 0 .../NatsIntegrationTests.cs | 20 +- .../Nats.UnitTests/Nats.UnitTests.csproj | 0 .../tests/Nats.UnitTests}/NatsBuilderTests.cs | 15 +- .../PostgreSql.IntegrationTests.csproj | 0 .../PostgreSqlIntegrationTests.cs | 26 +- .../PostgreSql.UnitTests.csproj | 0 .../PostgreSqlBuilderTests.cs | 25 +- .../RabbitMq.IntegrationTests.csproj | 0 .../RabbitMqIntegrationTests.cs | 25 +- .../RabbitMq.UnitTests.csproj | 0 .../RabbitMqBuilderTests.cs | 20 +- .../Redis.IntegrationTests.csproj | 0 .../RedisIntegrationTests.cs | 40 + .../Redis.UnitTests/Redis.UnitTests.csproj | 0 .../Redis.UnitTests}/RedisBuilderTests.cs | 21 +- .../SharedTestingFramework.csproj | 0 .../tests}/SharedTestingFramework/WslcTest.cs | 9 +- .../AlpineLifecycleTests.cs | 21 +- .../CleanupTests.cs | 18 +- .../ConcurrencyStressTests.cs | 15 +- .../ExecTests.cs | 29 +- .../PerformanceTests.cs | 35 + .../PortMappingTests.cs | 30 +- .../RuntimeInfoTests.cs | 16 + .../SharedStorageTests.cs | 22 +- .../VolumeTests.cs | 74 + .../WaitStrategyTests.cs | 25 +- .../WslContainers.IntegrationTests.csproj | 0 .../ContainerBuilderTests.cs | 39 +- .../WslContainers.UnitTests/FakeContainer.cs | 9 +- .../HttpWaitStrategyTests.cs | 122 + .../WslContainers.UnitTests/ImageTests.cs | 18 +- .../WslContainers.UnitTests/SecretTests.cs | 12 +- .../StorageModeTests.cs | 12 +- .../WaitContextTests.cs | 46 + .../WaitStrategyRunnerTests.cs | 28 +- .../WaitStrategyTests.cs | 108 + .../WslContainerRuntimeTests.cs | 53 + .../WslContainers.UnitTests.csproj | 0 .../WslInspectParserTests.cs | 18 +- .../PerformanceTests.cs | 39 - .../RuntimeInfoTests.cs | 21 - .../VolumeTests.cs | 59 - .../MySqlIntegrationTests.cs | 48 - .../RedisIntegrationTests.cs | 43 - .../HttpWaitStrategyTests.cs | 124 - .../WaitContextTests.cs | 40 - .../WaitStrategyTests.cs | 102 - 161 files changed, 2005 insertions(+), 4519 deletions(-) delete mode 100644 .editorconfig delete mode 100644 Directory.Build.props create mode 100644 src/Directory.Build.props rename Directory.Build.targets => src/Directory.Build.targets (100%) delete mode 100644 src/WslContainers.MinIo/MinIoContainer.cs delete mode 100644 src/WslContainers/LogBuffer.cs delete mode 100644 src/WslContainers/WslContainerRuntime.cs delete mode 100644 src/WslContainers/WslInspectParser.cs rename src/{WslContainers.Azurite/WslContainers.Azurite.csproj => src/Azurite/Azurite.csproj} (100%) rename src/{WslContainers.Azurite => src/Azurite}/AzuriteAccount.cs (100%) rename src/{WslContainers.Azurite => src/Azurite}/AzuriteBuilder.cs (90%) rename src/{WslContainers.Azurite => src/Azurite}/AzuriteConfiguration.cs (100%) rename src/{WslContainers.Azurite => src/Azurite}/AzuriteContainer.cs (97%) rename src/{WslContainers.MinIo/WslContainers.MinIo.csproj => src/MinIo/MinIO.csproj} (100%) rename src/{WslContainers.MinIo/MinIoBuilder.cs => src/MinIo/MinIOBuilder.cs} (55%) rename src/{WslContainers.MinIo/MinIoConfiguration.cs => src/MinIo/MinIOConfiguration.cs} (70%) create mode 100644 src/src/MinIo/MinIOContainer.cs rename src/{WslContainers.MsSql/WslContainers.MsSql.csproj => src/MsSql/MsSql.csproj} (100%) rename src/{WslContainers.MsSql => src/MsSql}/MsSqlBuilder.cs (81%) rename src/{WslContainers.MsSql => src/MsSql}/MsSqlConfiguration.cs (91%) rename src/{WslContainers.MsSql => src/MsSql}/MsSqlContainer.cs (80%) rename src/{WslContainers.MySql/WslContainers.MySql.csproj => src/MySql/MySql.csproj} (100%) rename src/{WslContainers.MySql => src/MySql}/MySqlBuilder.cs (73%) rename src/{WslContainers.MySql => src/MySql}/MySqlConfiguration.cs (94%) rename src/{WslContainers.MySql => src/MySql}/MySqlContainer.cs (68%) rename src/{WslContainers.Nats/WslContainers.Nats.csproj => src/Nats/Nats.csproj} (100%) rename src/{WslContainers.Nats => src/Nats}/NatsBuilder.cs (94%) rename src/{WslContainers.Nats => src/Nats}/NatsConfiguration.cs (100%) rename src/{WslContainers.Nats => src/Nats}/NatsContainer.cs (100%) rename src/{WslContainers.PostgreSql/WslContainers.PostgreSql.csproj => src/PostgreSql/PostgreSql.csproj} (100%) rename src/{WslContainers.PostgreSql => src/PostgreSql}/PostgreSqlBuilder.cs (77%) rename src/{WslContainers.PostgreSql => src/PostgreSql}/PostgreSqlConfiguration.cs (93%) rename src/{WslContainers.PostgreSql => src/PostgreSql}/PostgreSqlContainer.cs (68%) rename src/{WslContainers.RabbitMq/WslContainers.RabbitMq.csproj => src/RabbitMq/RabbitMq.csproj} (100%) rename src/{WslContainers.RabbitMq => src/RabbitMq}/RabbitMqBuilder.cs (79%) rename src/{WslContainers.RabbitMq => src/RabbitMq}/RabbitMqConfiguration.cs (93%) rename src/{WslContainers.RabbitMq => src/RabbitMq}/RabbitMqContainer.cs (76%) rename src/{WslContainers.Redis/WslContainers.Redis.csproj => src/Redis/Redis.csproj} (100%) rename src/{WslContainers.Redis => src/Redis}/RedisBuilder.cs (93%) rename src/{WslContainers.Redis => src/Redis}/RedisConfiguration.cs (100%) rename src/{WslContainers.Redis => src/Redis}/RedisContainer.cs (100%) rename src/{ => src}/WslContainers/ContainerBuilder.cs (88%) rename src/{ => src}/WslContainers/ContainerConfiguration.cs (91%) rename src/{ => src}/WslContainers/Containers/ContainerLogEntry.cs (79%) rename src/{ => src}/WslContainers/Containers/ContainerState.cs (91%) rename src/{ => src}/WslContainers/Containers/ExecOptions.cs (94%) rename src/{ => src}/WslContainers/Containers/ExecResult.cs (85%) rename src/{ => src}/WslContainers/Containers/LogStream.cs (82%) rename src/{ => src}/WslContainers/Diagnostics/Secret.cs (78%) rename src/{ => src}/WslContainers/Diagnostics/SecretRedactor.cs (67%) rename src/{ => src}/WslContainers/Diagnostics/WslContainersActivity.cs (61%) rename src/{ => src}/WslContainers/IContainer.cs (97%) rename src/{ => src}/WslContainers/IContainerBuilder.cs (100%) rename src/{ => src}/WslContainers/IContainerConfiguration.cs (93%) rename src/{ => src}/WslContainers/IContainerRuntime.cs (100%) rename src/{ => src}/WslContainers/IContainerSession.cs (100%) rename src/{ => src}/WslContainers/Images/Image.cs (84%) rename src/{ => src}/WslContainers/Images/ImagePullProgress.cs (100%) rename src/{ => src}/WslContainers/Images/ImageSummary.cs (100%) rename src/{ => src}/WslContainers/Images/PullPolicy.cs (89%) create mode 100644 src/src/WslContainers/LogBuffer.cs rename src/{ => src}/WslContainers/Mounts/BindMount.cs (81%) rename src/{ => src}/WslContainers/Mounts/NamedVolume.cs (83%) rename src/{ => src}/WslContainers/Networking/ContainerNetworkingMode.cs (89%) rename src/{ => src}/WslContainers/Networking/PortBinding.cs (94%) rename src/{ => src}/WslContainers/Networking/PortProtocol.cs (81%) rename src/{ => src}/WslContainers/RegistryCredentials.cs (94%) rename src/{ => src}/WslContainers/Runtime/WslContainerException.cs (58%) rename src/{ => src}/WslContainers/StorageMode.cs (99%) rename src/{ => src}/WslContainers/Waiting/AllWaitStrategy.cs (76%) rename src/{ => src}/WslContainers/Waiting/AnyWaitStrategy.cs (76%) rename src/{ => src}/WslContainers/Waiting/CommandWaitStrategy.cs (72%) rename src/{ => src}/WslContainers/Waiting/ContainerRunningWaitStrategy.cs (81%) rename src/{ => src}/WslContainers/Waiting/CustomWaitStrategy.cs (67%) rename src/{ => src}/WslContainers/Waiting/HttpWaitStrategy.cs (56%) rename src/{ => src}/WslContainers/Waiting/IWaitStrategy.cs (94%) rename src/{ => src}/WslContainers/Waiting/LogMessageWaitStrategy.cs (61%) rename src/{ => src}/WslContainers/Waiting/TcpPortWaitStrategy.cs (52%) rename src/{ => src}/WslContainers/Waiting/Wait.cs (98%) rename src/{ => src}/WslContainers/Waiting/WaitContext.cs (71%) rename src/{ => src}/WslContainers/Waiting/WaitStrategy.cs (96%) rename src/{ => src}/WslContainers/Waiting/WaitStrategyRunner.cs (67%) rename src/{ => src}/WslContainers/WslContainer.cs (60%) create mode 100644 src/src/WslContainers/WslContainerRuntime.cs rename src/{ => src}/WslContainers/WslContainerRuntimeInfo.cs (100%) rename src/{ => src}/WslContainers/WslContainerRuntimeOptions.cs (95%) rename src/{ => src}/WslContainers/WslContainerSession.cs (55%) rename src/{ => src}/WslContainers/WslContainers.csproj (100%) create mode 100644 src/src/WslContainers/WslInspectParser.cs rename src/{ => src}/WslContainers/WslSettingsMapper.cs (64%) rename tests/WslContainers.Azurite.IntegrationTests/WslContainers.Azurite.IntegrationTests.csproj => src/tests/Azurite.IntegrationTests/Azurite.IntegrationTests.csproj (100%) rename {tests/WslContainers.Azurite.IntegrationTests => src/tests/Azurite.IntegrationTests}/AzuriteIntegrationTests.cs (70%) rename tests/WslContainers.Azurite.UnitTests/WslContainers.Azurite.UnitTests.csproj => src/tests/Azurite.UnitTests/Azurite.UnitTests.csproj (100%) rename {tests/WslContainers.Azurite.UnitTests => src/tests/Azurite.UnitTests}/AzuriteBuilderTests.cs (56%) rename tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj => src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj (100%) rename tests/WslContainers.MinIo.IntegrationTests/MinIoIntegrationTests.cs => src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs (53%) rename tests/WslContainers.MinIo.IntegrationTests/WslContainers.MinIo.IntegrationTests.csproj => src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj (100%) rename tests/WslContainers.MinIo.UnitTests/MinIoBuilderTests.cs => src/tests/MinIo.UnitTests/MinIOBuilderTests.cs (64%) rename tests/WslContainers.MinIo.UnitTests/WslContainers.MinIo.UnitTests.csproj => src/tests/MsSql.IntegrationTests/MsSql.IntegrationTests.csproj (100%) rename {tests/WslContainers.MsSql.IntegrationTests => src/tests/MsSql.IntegrationTests}/MsSqlIntegrationTests.cs (52%) rename tests/WslContainers.MsSql.IntegrationTests/WslContainers.MsSql.IntegrationTests.csproj => src/tests/MsSql.UnitTests/MsSql.UnitTests.csproj (100%) rename {tests/WslContainers.MsSql.UnitTests => src/tests/MsSql.UnitTests}/MsSqlBuilderTests.cs (67%) rename tests/WslContainers.MySql.IntegrationTests/WslContainers.MySql.IntegrationTests.csproj => src/tests/MySql.IntegrationTests/MySql.IntegrationTests.csproj (100%) create mode 100644 src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs rename tests/WslContainers.MsSql.UnitTests/WslContainers.MsSql.UnitTests.csproj => src/tests/MySql.UnitTests/MySql.UnitTests.csproj (100%) rename {tests/WslContainers.MySql.UnitTests => src/tests/MySql.UnitTests}/MySqlBuilderTests.cs (62%) rename tests/WslContainers.Nats.IntegrationTests/WslContainers.Nats.IntegrationTests.csproj => src/tests/Nats.IntegrationTests/Nats.IntegrationTests.csproj (100%) rename {tests/WslContainers.Nats.IntegrationTests => src/tests/Nats.IntegrationTests}/NatsIntegrationTests.cs (54%) rename tests/WslContainers.MySql.UnitTests/WslContainers.MySql.UnitTests.csproj => src/tests/Nats.UnitTests/Nats.UnitTests.csproj (100%) rename {tests/WslContainers.Nats.UnitTests => src/tests/Nats.UnitTests}/NatsBuilderTests.cs (52%) rename tests/WslContainers.Nats.UnitTests/WslContainers.Nats.UnitTests.csproj => src/tests/PostgreSql.IntegrationTests/PostgreSql.IntegrationTests.csproj (100%) rename {tests/WslContainers.PostgreSql.IntegrationTests => src/tests/PostgreSql.IntegrationTests}/PostgreSqlIntegrationTests.cs (55%) rename tests/WslContainers.PostgreSql.IntegrationTests/WslContainers.PostgreSql.IntegrationTests.csproj => src/tests/PostgreSql.UnitTests/PostgreSql.UnitTests.csproj (100%) rename {tests/WslContainers.PostgreSql.UnitTests => src/tests/PostgreSql.UnitTests}/PostgreSqlBuilderTests.cs (74%) rename tests/WslContainers.RabbitMq.IntegrationTests/WslContainers.RabbitMq.IntegrationTests.csproj => src/tests/RabbitMq.IntegrationTests/RabbitMq.IntegrationTests.csproj (100%) rename {tests/WslContainers.RabbitMq.IntegrationTests => src/tests/RabbitMq.IntegrationTests}/RabbitMqIntegrationTests.cs (57%) rename tests/WslContainers.PostgreSql.UnitTests/WslContainers.PostgreSql.UnitTests.csproj => src/tests/RabbitMq.UnitTests/RabbitMq.UnitTests.csproj (100%) rename {tests/WslContainers.RabbitMq.UnitTests => src/tests/RabbitMq.UnitTests}/RabbitMqBuilderTests.cs (71%) rename tests/WslContainers.Redis.IntegrationTests/WslContainers.Redis.IntegrationTests.csproj => src/tests/Redis.IntegrationTests/Redis.IntegrationTests.csproj (100%) create mode 100644 src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs rename tests/WslContainers.RabbitMq.UnitTests/WslContainers.RabbitMq.UnitTests.csproj => src/tests/Redis.UnitTests/Redis.UnitTests.csproj (100%) rename {tests/WslContainers.Redis.UnitTests => src/tests/Redis.UnitTests}/RedisBuilderTests.cs (54%) rename {tests => src/tests}/SharedTestingFramework/SharedTestingFramework.csproj (100%) rename {tests => src/tests}/SharedTestingFramework/WslcTest.cs (68%) rename {tests => src/tests}/WslContainers.IntegrationTests/AlpineLifecycleTests.cs (74%) rename {tests => src/tests}/WslContainers.IntegrationTests/CleanupTests.cs (77%) rename {tests => src/tests}/WslContainers.IntegrationTests/ConcurrencyStressTests.cs (71%) rename {tests => src/tests}/WslContainers.IntegrationTests/ExecTests.cs (66%) create mode 100644 src/tests/WslContainers.IntegrationTests/PerformanceTests.cs rename {tests => src/tests}/WslContainers.IntegrationTests/PortMappingTests.cs (68%) create mode 100644 src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs rename {tests => src/tests}/WslContainers.IntegrationTests/SharedStorageTests.cs (62%) create mode 100644 src/tests/WslContainers.IntegrationTests/VolumeTests.cs rename {tests => src/tests}/WslContainers.IntegrationTests/WaitStrategyTests.cs (81%) rename tests/WslContainers.Redis.UnitTests/WslContainers.Redis.UnitTests.csproj => src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj (100%) rename {tests => src/tests}/WslContainers.UnitTests/ContainerBuilderTests.cs (72%) rename {tests => src/tests}/WslContainers.UnitTests/FakeContainer.cs (88%) create mode 100644 src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs rename {tests => src/tests}/WslContainers.UnitTests/ImageTests.cs (81%) rename {tests => src/tests}/WslContainers.UnitTests/SecretTests.cs (63%) rename {tests => src/tests}/WslContainers.UnitTests/StorageModeTests.cs (75%) create mode 100644 src/tests/WslContainers.UnitTests/WaitContextTests.cs rename {tests => src/tests}/WslContainers.UnitTests/WaitStrategyRunnerTests.cs (62%) create mode 100644 src/tests/WslContainers.UnitTests/WaitStrategyTests.cs create mode 100644 src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs rename {tests => src/tests}/WslContainers.UnitTests/WslContainers.UnitTests.csproj (100%) rename {tests => src/tests}/WslContainers.UnitTests/WslInspectParserTests.cs (62%) delete mode 100644 tests/WslContainers.IntegrationTests/PerformanceTests.cs delete mode 100644 tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs delete mode 100644 tests/WslContainers.IntegrationTests/VolumeTests.cs delete mode 100644 tests/WslContainers.MySql.IntegrationTests/MySqlIntegrationTests.cs delete mode 100644 tests/WslContainers.Redis.IntegrationTests/RedisIntegrationTests.cs delete mode 100644 tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs delete mode 100644 tests/WslContainers.UnitTests/WaitContextTests.cs delete mode 100644 tests/WslContainers.UnitTests/WaitStrategyTests.cs diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 27dd15d..0000000 --- a/.editorconfig +++ /dev/null @@ -1,2702 +0,0 @@ -root = true - -# Base Configuration - General Settings, File Formatting, and Naming Conventions - -# All Files -[*] -charset = utf-8 -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_primary_constructors = true:suggestion -csharp_style_prefer_top_level_statements = true:silent -end_of_line = lf -indent_size = 4 -indent_style = tab -insert_final_newline = true -tab_width = 4 -trim_trailing_whitespace = true -max_line_length = 120 - -# Type members -dotnet_hide_advanced_members = false -dotnet_member_insertion_location = with_other_members_of_the_same_kind -dotnet_property_generation_behavior = prefer_throwing_properties - -# Symbol search -dotnet_search_reference_assemblies = true - -# Nullability settings -dotnet_build_property.Nullable = enable - -# Enable or disable the analyzers -dotnet_analyzer_diagnostic.severity = warning - -# Visual Studio XML Project Files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 - -# XML Configuration Files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,xml,stylecop}] -indent_size = 2 - -# JSON Files -[*.{json,json5,webmanifest}] -indent_size = 2 - -# YAML Files -[*.{yml,yaml}] -indent_size = 2 - -# Markdown Files -[*.{md,mdx}] -trim_trailing_whitespace = false - -# Bash Files -[*.sh] -end_of_line = lf - -# Batch Files -[*.{cmd,bat}] -end_of_line = crlf - -# Web Files -[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] -indent_size = 2 -insert_final_newline = true - -# Makefiles -[Makefile] -indent_style = tab - -# .NET Style Rules - Naming Conventions -# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules - -[*.{cs,csx,cake,vb,vbx}] - -# Non-private static fields are PascalCase -dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = warning -dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields -dotnet_naming_rule.private_fields.severity = warning -dotnet_naming_rule.private_fields.style = camel_case_underscore -dotnet_naming_rule.private_fields.symbols = private_fields -dotnet_naming_rule.private_fields_style.severity = warning -dotnet_naming_rule.private_fields_style.style = camel_case -dotnet_naming_rule.private_fields_style.symbols = private_fields -dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case -dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected -dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field -dotnet_naming_symbols.non_private_static_fields.required_modifiers = static - -# Constants are PascalCase -dotnet_naming_rule.constants_should_be_pascal_case.severity = warning -dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants -dotnet_naming_style.constant_style.capitalization = pascal_case -dotnet_naming_symbols.constants.applicable_kinds = field, local -dotnet_naming_symbols.constants.required_modifiers = const - -# Locals and parameters are camelCase -dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters -dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style -dotnet_naming_rule.locals_should_be_camel_case.severity = warning - -# camel_case_style - Define the camelCase style -dotnet_naming_style.camel_case_style.capitalization = camel_case -dotnet_naming_style.static_field_style.required_prefix = s_ -dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local -dotnet_naming_symbols.static_fields.required_modifiers = static - -# first_upper_style - The first character must start with an upper-case character -dotnet_naming_style.first_upper_style.capitalization = first_word_upper - -# prefix_interface_with_i_style - Interfaces must be PascalCase and first character must be 'I' -dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I -dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case - -# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and start with 'T' -dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case -dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T - -# disallowed_style - Anything that has this style applied is marked as disallowed -dotnet_naming_style.disallowed_style.capitalization = pascal_case -dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ -dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ - -# internal_error_style - This style should never occur -dotnet_naming_style.internal_error_style.capitalization = pascal_case -dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ -dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ - -# All public/protected/protected_internal constant fields must be PascalCase -dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning -dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style -dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group -dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal -dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field -dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const - -# All public/protected/protected_internal static readonly fields must be PascalCase -dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning -dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style -dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group -dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal -dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field -dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly - -# No other public/protected/protected_internal fields are allowed -dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error -dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style -dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group -dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal -dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field - -# StyleCop Field Naming Rules - -# All constant fields must be PascalCase -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.severity = warning -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.style = _fieldname -dotnet_naming_rule.private_or_internal_field_should_be__fieldname.symbols = private_or_internal_field -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field -dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const - -# All static readonly fields must be PascalCase -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field -dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly - -# No non-private instance fields are allowed -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field - -# Private fields must be camelCase -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group -dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private -dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field - -# Local variables must be camelCase -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group -dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local -dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local - -# Sanity check - uncovered field case -dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error -dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style -dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group -dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * -dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field - -# All elements (namespaces, classes, enums, etc.) must be PascalCase -dotnet_naming_rule.element_rule.severity = warning -dotnet_naming_rule.element_rule.style = non_private_static_field_style -dotnet_naming_rule.element_rule.symbols = element_group -dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property - -# Interfaces use PascalCase and are prefixed with uppercase 'I' -dotnet_naming_rule.interface_rule.severity = warning -dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style -dotnet_naming_rule.interface_rule.symbols = interface_group -dotnet_naming_symbols.interface_group.applicable_kinds = interface - -# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' -dotnet_naming_rule.type_parameter_rule.severity = warning -dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style -dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group -dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter - -# Function parameters use camelCase -dotnet_naming_rule.parameters_rule.severity = warning -dotnet_naming_rule.parameters_rule.style = camel_case_style -dotnet_naming_rule.parameters_rule.symbols = parameters_group -dotnet_naming_symbols.parameters_group.applicable_kinds = parameter - -# Type Parameters -dotnet_naming_rule.type_parameter_naming.severity = warning -dotnet_naming_rule.type_parameter_naming.style = type_parameter_style -dotnet_naming_rule.type_parameter_naming.symbols = type_parameter_symbol -dotnet_naming_style.type_parameter_style.capitalization = pascal_case -dotnet_naming_style.type_parameter_style.required_prefix = T -dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * -dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter - -# Instance fields are camelCase and start with _ -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion -dotnet_naming_rule.instance_fields_should_be_camel_case.style = camel_case_underscore_style -dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields -dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case -dotnet_naming_style.camel_case_underscore_style.required_prefix = _ -dotnet_naming_style.instance_field_style.capitalization = camel_case -dotnet_naming_style.instance_field_style.required_prefix = _ -dotnet_naming_symbols.instance_fields.applicable_kinds = field -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field - -# Local functions are PascalCase -dotnet_naming_rule.local_functions_should_be_pascal_case.severity = warning -dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = all_members -dotnet_naming_style.local_function_style.capitalization = pascal_case -dotnet_naming_symbols.local_functions.applicable_kinds = local_function - -# "this." and "Me." qualifiers -dotnet_style_qualification_for_event = false:silent -dotnet_style_qualification_for_field = false:silent -dotnet_style_qualification_for_method = false:silent -dotnet_style_qualification_for_property = false:silent - -# Undocumented -dotnet_style_operator_placement_when_wrapping = end_of_line - -# Naming styles -dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning -dotnet_naming_rule.interface_should_be_begins_with_i.style = prefix_interface_with_i_style -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.types_should_be_pascal_case.severity = warning -dotnet_naming_rule.types_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.types_should_be_pascal_case.symbols = types - -# By default, name items with PascalCase -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = non_private_static_field_style -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members - -# pascal_case_style - Define the PascalCase style -dotnet_naming_style.pascal_case_style.capitalization = pascal_case -dotnet_naming_symbols.all_members.applicable_kinds = * - -# Symbol specifications -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.required_modifiers = * - -# Naming styles -dotnet_naming_style._fieldname.capitalization = camel_case -dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = - -# Simplify interpolation -dotnet_diagnostic.IDE0071.severity = warning - -# Accessibility modifiers -dotnet_diagnostic.IDE0040.severity = warning - -# Allow multiple blank lines -dotnet_diagnostic.IDE2000.severity = silent - -# Code Quality Settings -dotnet_code_quality.enable_nullable_reference_types = true -dotnet_code_quality.enableNETAnalyzers = true -dotnet_code_quality.interpolated_string_composite_format = true -dotnet_code_quality.prefer_auto_properties = false -dotnet_code_quality.prefer_const = true -dotnet_code_quality.prefer_inferred_anonymous_type_member_names = true -dotnet_code_quality.prefer_inferred_tuple_names = true -dotnet_code_quality.prefer_readonly = true -dotnet_code_quality.require_accessibility_modifiers = true -dotnet_code_quality.require_explicit_type_arguments = true -dotnet_code_quality.require_explicit_visibility = true -dotnet_code_quality.require_variable_declaration_for_explicit_type = false -dotnet_code_quality_unused_parameters = all:warning -dotnet_enable_roslyn_analyzers = true -dotnet_remove_unnecessary_suppression_exclusions = none - -# RS0016: Add public types and members to the declared API -dotnet_public_api_analyzer.require_api_files = true - -# Do not use generic CodeAction.Create to create CodeAction -dotnet_diagnostic.RS0005.severity = none - -# C# Specific Settings - -[*.{cs,csx,cake}] - -# Newline options -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true - -# C# Unnecessary code rules -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -# Set the C# language version -csharp_language_version = latest - -# Prefer var for inferred expressions such as method calls. -# PDS0003 supplies the narrower explicit-type rule for object creation. -csharp_style_var_elsewhere = true:warning -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning - -# Modifier preferences -csharp_prefer_static_anonymous_function = true -csharp_prefer_static_local_function = true:warning -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning -csharp_style_prefer_readonly_struct = true:suggestion -csharp_style_prefer_readonly_struct_member = true:suggestion -dotnet_style_readonly_field = true:warning -dotnet_style_require_accessibility_modifiers = omit_if_default:warning - -# Code-block preferences -csharp_prefer_braces = when_multiline:suggestion -csharp_prefer_simple_using_statement = false:silent -csharp_prefer_system_threading_lock = true -dotnet_style_prefer_collection_expression = true:suggestion - -# Enable foreach explicit cast preference -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning - -# Prefer System.Threading.Lock -csharp_prefer_system_threading_lock = true:warning - -# Prefer parameter null checking -csharp_style_prefer_parameter_null_checking = true:suggestion - -# Prefer extended property pattern -csharp_style_prefer_extended_property_pattern = true:suggestion - -# Prefer UTF-8 string literals -csharp_style_prefer_utf8_string_literals = true:suggestion - -# Prefer tuple swap -csharp_style_prefer_tuple_swap = true:suggestion - -# Prefer local over anonymous function -csharp_style_prefer_local_over_anonymous_function = true:suggestion - -# Prefer unbound generic type in nameof -csharp_style_prefer_unbound_generic_type_in_nameof = true:warning - -# Expression-level preferences -csharp_prefer_simple_default_expression = true:warning -csharp_style_deconstructed_variable_declaration = true:warning -csharp_style_implicit_object_creation_when_type_is_apparent = true:warning -csharp_style_inlined_variable_declaration = true:warning -csharp_style_pattern_local_over_anonymous_function = true:warning -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_range_operator = true:warning -csharp_style_prefer_null_check_over_type_check = true:warning -csharp_style_throw_expression = true:warning - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace:warning - -# New line preferences -csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent -csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent -csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent - -# Expression-Bodied members -csharp_style_expression_bodied_accessors = true:warning -csharp_style_expression_bodied_constructors = true:warning -csharp_style_expression_bodied_indexers = true:warning -csharp_style_expression_bodied_lambdas = true:warning -csharp_style_expression_bodied_local_functions = true:warning -csharp_style_expression_bodied_methods = true:suggestion -csharp_style_expression_bodied_operators = true:warning -csharp_style_expression_bodied_properties = true:warning - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = true:warning -csharp_style_prefer_not_pattern = true:warning -csharp_style_prefer_pattern_matching = true:warning -csharp_style_prefer_switch_expression = true:warning -csharp_style_prefer_explicit_this = true:warning - -# "Null" checking preferences -csharp_style_conditional_delegate_call = true:warning - -# Spacing options -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Wrap options -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = false - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true - -# Namespace options -csharp_style_namespace_declarations = file_scoped:warning - -# Visual Basic Specific Settings - -[*.{vb}] - -visual_basic_preferred_modifier_order = Partial, Default, Private, Protected, Public, Friend, NotOverridable, Overridable, MustOverride, Overloads, Overrides, MustInherit, NotInheritable, Static, Shared, Shadows, ReadOnly, WriteOnly, Dim, Const, WithEvents, Widening, Narrowing, Custom, Async:warning -visual_basic_style_prefer_simplified_object_creation = all : suggestion -visual_basic_style_prefer_isnot_expression = true : suggestion - -# .NET Style Preferences - -[*.{cs,csx,cake,vb,vbx}] - -# Language keywords instead of framework type names for type references -dotnet_style_predefined_type_for_locals_parameters_members = true:warning -dotnet_style_predefined_type_for_member_access = true:warning - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning - -dotnet_style_collection_initializer = true:warning -dotnet_style_explicit_tuple_names = true:warning -dotnet_style_object_initializer = true:warning -dotnet_style_prefer_auto_properties = true:warning -dotnet_style_prefer_compound_assignment = true:warning -dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:suggestion -dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed -dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning -dotnet_style_prefer_inferred_tuple_names = true:warning -dotnet_style_prefer_simplified_boolean_expressions = false:silent -dotnet_style_prefer_simplified_interpolation = true:warning - -# Expression-level preferences -dotnet_prefer_system_hash_code = true - -# Null-checking preferences -dotnet_style_coalesce_expression = true:warning -dotnet_style_null_propagation = true:warning -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning - -[*.g.cs] -# Generated source-generator output: any diagnostics are worthless, so treat as generated code. -generated_code = true -# Missing XML comment for publicly visible type or member 'Type_or_Member' -dotnet_diagnostic.CS1591.severity = none - -# Design Rules (CA1000-CA1070) -# Design rules support adherence to the .NET Framework design guidelines - - -[*.{cs,vb}] - -# Do not declare static members on generic types -dotnet_diagnostic.CA1000.severity = suggestion - -# Types that own disposable fields should be disposable -dotnet_diagnostic.CA1001.severity = error - -# Do not expose generic lists -dotnet_diagnostic.CA1002.severity = error - -# Use generic event handler instances -dotnet_diagnostic.CA1003.severity = warning - -# Avoid excessive parameters on generic types -dotnet_diagnostic.CA1005.severity = warning - -# Enums should have zero value -dotnet_diagnostic.CA1008.severity = error - -# Collections should implement generic interface -dotnet_diagnostic.CA1010.severity = warning - -# Abstract types should not have public constructors -dotnet_diagnostic.CA1012.severity = error - -# Mark assemblies with CLSCompliantAttribute -dotnet_diagnostic.CA1014.severity = silent - -# Mark assemblies with AssemblyVersionAttribute -dotnet_diagnostic.CA1016.severity = error - -# Mark assemblies with ComVisibleAttribute -dotnet_diagnostic.CA1017.severity = silent - -# Mark attributes with AttributeUsageAttribute -dotnet_diagnostic.CA1018.severity = suggestion - -# Define accessors for attribute arguments -dotnet_diagnostic.CA1019.severity = suggestion - -# Avoid out parameters -dotnet_diagnostic.CA1021.severity = suggestion - -# Use properties where appropriate -dotnet_diagnostic.CA1024.severity = suggestion - -# Mark enums with FlagsAttribute -dotnet_diagnostic.CA1027.severity = error - -# Enum storage should be Int32 -dotnet_diagnostic.CA1028.severity = error - -# Use events where appropriate -dotnet_diagnostic.CA1030.severity = suggestion - -# Do not catch general exception types -dotnet_diagnostic.CA1031.severity = error - -# Implement standard exception constructors -dotnet_diagnostic.CA1032.severity = suggestion - -# Interface methods should be callable by child types -dotnet_diagnostic.CA1033.severity = suggestion - -# Nested types should not be visible -dotnet_diagnostic.CA1034.severity = error - -# Override methods on comparable types -dotnet_diagnostic.CA1036.severity = suggestion - -# Avoid empty interfaces -dotnet_diagnostic.CA1040.severity = suggestion - -# Provide ObsoleteAttribute message -dotnet_diagnostic.CA1041.severity = warning - -# Use integral or string argument for indexers -dotnet_diagnostic.CA1043.severity = suggestion - -# Properties should not be write only -dotnet_diagnostic.CA1044.severity = error - -# Do not pass types by reference -dotnet_diagnostic.CA1045.severity = warning - -# Do not overload operator equals on reference types -dotnet_diagnostic.CA1046.severity = warning - -# Do not declare protected members in sealed types -dotnet_diagnostic.CA1047.severity = error - -# Declare types in namespaces -dotnet_diagnostic.CA1050.severity = error - -# Do not declare visible instance fields -dotnet_diagnostic.CA1051.severity = error - -# Static holder types should be Static or NotInheritable -dotnet_diagnostic.CA1052.severity = warning - -# Static holder types should not have default constructors -dotnet_diagnostic.CA1053.severity = warning - -# URI parameters should not be strings -dotnet_diagnostic.CA1054.severity = warning - -# URI return values should not be strings -dotnet_diagnostic.CA1055.severity = warning - -# URI properties should not be strings -dotnet_diagnostic.CA1056.severity = warning - -# Types should not extend certain base types -dotnet_diagnostic.CA1058.severity = warning - -# Move P/Invokes to NativeMethods class -dotnet_diagnostic.CA1060.severity = warning - -# Do not hide base class methods -dotnet_diagnostic.CA1061.severity = warning - -# Validate arguments of public methods -dotnet_code_quality.CA1062.api_surface = public, protected -dotnet_diagnostic.CA1062.severity = error - -# Implement IDisposable correctly -dotnet_diagnostic.CA1063.severity = error - -# Exceptions should be public -dotnet_diagnostic.CA1064.severity = warning - -# Do not raise exceptions in unexpected locations -dotnet_diagnostic.CA1065.severity = warning - -# Implement IEquatable when overriding Equals -dotnet_diagnostic.CA1066.severity = warning - -# Override Equals when implementing IEquatable -dotnet_diagnostic.CA1067.severity = warning - -# CancellationToken parameters must come last -dotnet_diagnostic.CA1068.severity = warning - -# Enums should not have duplicate values -dotnet_diagnostic.CA1069.severity = error - -# Do not declare event fields as virtual -dotnet_diagnostic.CA1070.severity = warning - -# Documentation Rules (CA1200) -# Documentation rules support writing well-documented libraries - - -[*.{cs,vb}] - -# Avoid using cref tags with a prefix -dotnet_diagnostic.CA1200.severity = suggestion - -# Globalization Rules (CA1303-CA1311 and CA2101) -# Globalization rules support world-ready libraries and applications - - -[*.{cs,vb}] - -# Do not pass literals as localized parameters -dotnet_code_quality.CA1303.use_naming_heuristic = true -dotnet_diagnostic.CA1303.severity = none - -# Specify CultureInfo -dotnet_diagnostic.CA1304.severity = error - -# Specify IFormatProvider -dotnet_diagnostic.CA1305.severity = error - -# Specify StringComparison for clarity -dotnet_diagnostic.CA1307.severity = error - -# Normalize strings to uppercase -dotnet_diagnostic.CA1308.severity = warning - -# Use ordinal StringComparison -dotnet_diagnostic.CA1309.severity = warning - -# Specify StringComparison for correctness -dotnet_diagnostic.CA1310.severity = warning - -# Specify a culture or use an invariant version -dotnet_diagnostic.CA1311.severity = error - -# Specify marshaling for P/Invoke string arguments -dotnet_diagnostic.CA2101.severity = warning - -# Interoperability Rules (CA1400-CA1422) -# Portability rules support portability across different platforms -# Interoperability rules support interaction with COM clients - - -[*.{cs,vb}] - -# P/Invokes should not be visible -dotnet_diagnostic.CA1401.severity = error - -# Validate platform compatibility -dotnet_diagnostic.CA1416.severity = suggestion - -# Do not use OutAttribute on string parameters for P/Invokes -dotnet_diagnostic.CA1417.severity = suggestion - -# Validate platform compatibility -dotnet_diagnostic.CA1418.severity = suggestion - -# Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' -dotnet_diagnostic.CA1419.severity = suggestion - -# Property, type, or attribute requires runtime marshalling -dotnet_diagnostic.CA1420.severity = suggestion - -# Method uses runtime marshalling when DisableRuntimeMarshallingAttribute is applied -dotnet_diagnostic.CA1421.severity = warning - -# Validate platform compatibility - obsoleted APIs -dotnet_diagnostic.CA1422.severity = warning - -# Maintainability Rules (CA1501-CA1516) -# Maintainability rules support library and application maintenance - - -[*.{cs,vb}] - -# Avoid excessive inheritance -dotnet_diagnostic.CA1501.severity = warning - -# Avoid excessive complexity -dotnet_diagnostic.CA1502.severity = error - -# Avoid unmaintainable code -dotnet_diagnostic.CA1505.severity = error - -# Avoid excessive class coupling -dotnet_diagnostic.CA1506.severity = error - -# Use nameof in place of string -dotnet_diagnostic.CA1507.severity = warning - -# Avoid dead conditional code -dotnet_diagnostic.CA1508.severity = error - -# Invalid entry in code metrics configuration file -dotnet_diagnostic.CA1509.severity = error - -# Use ArgumentNullException throw helper -dotnet_diagnostic.CA1510.severity = warning - -# Use ArgumentException throw helper -dotnet_diagnostic.CA1511.severity = warning - -# Use ArgumentOutOfRangeException throw helper -dotnet_diagnostic.CA1512.severity = warning - -# Use ObjectDisposedException throw helper -dotnet_diagnostic.CA1513.severity = warning - -# Avoid redundant length argument -dotnet_diagnostic.CA1514.severity = warning - -# Consider making public types internal -dotnet_diagnostic.CA1515.severity = suggestion - -# Naming Rules (CA1700-CA1727 and IDE0130) -# Naming rules support adherence to the naming conventions of the .NET design guidelines - - -[*.{cs,vb}] - -# Namespace does not match folder structure -dotnet_diagnostic.IDE0130.severity = warning - -# Do not name enum values 'Reserved' -dotnet_code_quality.CA1700.api_surface = public, protected -dotnet_diagnostic.CA1700.severity = error - -# Identifiers should not contain underscores -dotnet_code_quality.CA1707.api_surface = public, protected -dotnet_diagnostic.CA1707.severity = error - -# Identifiers should differ by more than case -dotnet_diagnostic.CA1708.severity = error - -# Identifiers should have correct suffix -dotnet_diagnostic.CA1710.severity = error - -# Identifiers should not have incorrect suffix -dotnet_diagnostic.CA1711.severity = error - -# Do not prefix enum values with type name -dotnet_diagnostic.CA1712.severity = error - -# Events should not have Before or After prefix -dotnet_diagnostic.CA1713.severity = error - -# Flags enums should have plural names -dotnet_diagnostic.CA1714.severity = error - -# Identifiers should have correct prefix -dotnet_diagnostic.CA1715.severity = error - -# Identifiers should not match keywords -dotnet_diagnostic.CA1716.severity = error - -# Only FlagsAttribute enums should have plural names -dotnet_diagnostic.CA1717.severity = error - -# Identifiers should not contain type names -dotnet_diagnostic.CA1720.severity = warning - -# Property names should not match get methods -dotnet_diagnostic.CA1721.severity = error - -# Type names should not match namespaces -dotnet_diagnostic.CA1724.severity = warning - -# Parameter names should match base declaration -dotnet_diagnostic.CA1725.severity = warning - -# Use PascalCase for named placeholders -dotnet_diagnostic.CA1727.severity = error - -# Performance Rules (CA1802-CA1877) -# Performance rules support high-performance libraries and applications - - - -[*.{cs,vb}] - -# Use Literals Where Appropriate -dotnet_diagnostic.CA1802.severity = suggestion - -# Do not initialize unnecessarily -dotnet_diagnostic.CA1805.severity = error - -# Do not ignore method results -dotnet_diagnostic.CA1806.severity = error - -# Initialize reference type static fields inline -dotnet_diagnostic.CA1810.severity = suggestion - -# Avoid uninstantiated internal classes -dotnet_diagnostic.CA1812.severity = suggestion - -# Avoid unsealed attributes -dotnet_diagnostic.CA1813.severity = suggestion - -# Prefer jagged arrays over multidimensional -dotnet_diagnostic.CA1814.severity = warning - -# Override equals and operator equals on value types -dotnet_diagnostic.CA1815.severity = warning - -# Properties should not return arrays -dotnet_diagnostic.CA1819.severity = warning -dotnet_code_quality.CA1819.api_surface = public, protected - -# Test for empty strings using string length -dotnet_diagnostic.CA1820.severity = error - -# Remove empty finalizers -dotnet_diagnostic.CA1821.severity = error - -# Mark members as static -dotnet_diagnostic.CA1822.severity = suggestion - -# Avoid unused private fields -dotnet_diagnostic.CA1823.severity = error - -# Mark assemblies with NeutralResourcesLanguageAttribute -dotnet_diagnostic.CA1824.severity = suggestion - -# Avoid zero-length array allocations -dotnet_diagnostic.CA1825.severity = suggestion - -# Use property instead of Linq Enumerable method -dotnet_diagnostic.CA1826.severity = error - -# Do not use Count/LongCount when Any can be used -dotnet_diagnostic.CA1827.severity = silent - -# Do not use CountAsync/LongCountAsync when AnyAsync can be used -dotnet_diagnostic.CA1828.severity = silent - -# Use Length/Count property instead of Enumerable.Count() method -dotnet_diagnostic.CA1829.severity = error - -# Prefer strongly-typed Append and Insert method overloads on StringBuilder -dotnet_diagnostic.CA1830.severity = warning - -# Use AsSpan instead of Range-based indexers for string when appropriate -dotnet_diagnostic.CA1831.severity = error - -# Use AsSpan or AsMemory instead of Range-based indexers for getting ReadOnlySpan or ReadOnlyMemory portion of an array * -dotnet_diagnostic.CA1832.severity = error - -# Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array -dotnet_diagnostic.CA1833.severity = error - -# Use StringBuilder.Append(char) for single character strings -dotnet_diagnostic.CA1834.severity = suggestion - -# Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes -dotnet_diagnostic.CA1835.severity = warning - -# Prefer IsEmpty over Count when available -dotnet_diagnostic.CA1836.severity = error - -# Use Environment.ProcessId instead of Process.GetCurrentProcess().Id -dotnet_diagnostic.CA1837.severity = suggestion - -# Avoid StringBuilder parameters for P/Invokes -dotnet_diagnostic.CA1838.severity = warning - -# Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName -dotnet_diagnostic.CA1839.severity = error - -# Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId -dotnet_diagnostic.CA1840.severity = silent - -# Prefer Dictionary Contains methods -dotnet_diagnostic.CA1841.severity = silent - -# Do not use 'WhenAll' with a single task - * -dotnet_diagnostic.CA1842.severity = error - -# Do not use 'WaitAll' with a single task - * -dotnet_diagnostic.CA1843.severity = error - -# Provide memory-based overrides of async methods when subclassing 'Stream' * -dotnet_diagnostic.CA1844.severity = warning - -# Use span-based 'string.Concat' -dotnet_diagnostic.CA1845.severity = error - -# Prefer AsSpan over Substring -dotnet_diagnostic.CA1846.severity = error - -dotnet_diagnostic.ignore_internalsvisibleto = true -dotnet_diagnostic.CA1852.ignore_internalsvisibleto = true - -# Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks -dotnet_diagnostic.CA2352.severity = error - -# Unsafe DataSet or DataTable in serializable type -dotnet_diagnostic.CA2353.severity = error - -# Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attack -dotnet_diagnostic.CA2354.severity = error - -# Unsafe DataSet or DataTable in deserialized object graph -dotnet_diagnostic.CA2355.severity = error - -# Unsafe DataSet or DataTable type in web deserialized object graph -dotnet_diagnostic.CA2356.severity = error - -# Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data -dotnet_diagnostic.CA2361.severity = error - -# Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks -dotnet_diagnostic.CA2362.severity = error - -# Review code for SQL injection vulnerabilities -dotnet_diagnostic.CA3001.severity = warning - -# Review code for XSS vulnerabilities -dotnet_diagnostic.CA3002.severity = warning - -# Review code for file path injection vulnerabilities -dotnet_diagnostic.CA3003.severity = warning - -# Review code for information disclosure vulnerabilities -dotnet_diagnostic.CA3004.severity = warning - -# Review code for LDAP injection vulnerabilities -dotnet_diagnostic.CA3005.severity = error - -# Review code for process command injection vulnerabilities -dotnet_diagnostic.CA3006.severity = error - -# Review code for open redirect vulnerabilities -dotnet_diagnostic.CA3007.severity = warning - -# Review code for XPath injection vulnerabilities -dotnet_diagnostic.CA3008.severity = error - -# Review code for XML injection vulnerabilities -dotnet_diagnostic.CA3009.severity = error - -# Review code for XAML injection vulnerabilities -dotnet_diagnostic.CA3010.severity = error - -# Review code for DLL injection vulnerabilities -dotnet_diagnostic.CA3011.severity = warning - -# Review code for regex injection vulnerabilities -dotnet_diagnostic.CA3012.severity = warning - -# Do Not Add Schema By URL -dotnet_diagnostic.CA3061.severity = warning - -# Insecure DTD processing in XML -dotnet_diagnostic.CA3075.severity = warning - -# Insecure XSLT script processing -dotnet_diagnostic.CA3076.severity = warning - -# Insecure Processing in API Design, XmlDocument and XmlTextReader -dotnet_diagnostic.CA3077.severity = warning - -# Mark Verb Handlers With Validate Antiforgery Token -dotnet_diagnostic.CA3147.severity = warning - -# Do Not Use Weak Cryptographic Algorithms -dotnet_diagnostic.CA5350.severity = warning - -# Do Not Use Broken Cryptographic Algorithms -dotnet_diagnostic.CA5351.severity = error - -# Do Not Use Unsafe Cipher Modes -dotnet_diagnostic.CA5358.severity = warning - -# Do Not Disable Certificate Validation -dotnet_diagnostic.CA5359.severity = warning - -# Do Not Call Dangerous Methods In Deserialization -dotnet_diagnostic.CA5360.severity = warning - -# Do Not Disable SChannel Use of Strong Crypto -dotnet_diagnostic.CA5361.severity = warning - -# Do Not Refer Self In Serializable Class -dotnet_diagnostic.CA5362.severity = error - -# Do Not Disable Request Validation -dotnet_diagnostic.CA5363.severity = warning - -# Do Not Use Deprecated Security Protocols -dotnet_diagnostic.CA5364.severity = error - -# Do Not Disable HTTP Header Checking -dotnet_diagnostic.CA5365.severity = warning - -# Use XmlReader For DataSet Read Xml -dotnet_diagnostic.CA5366.severity = warning - -# Do Not Serialize Types With Pointer Fields -dotnet_diagnostic.CA5367.severity = warning - -# Set ViewStateUserKey For Classes Derived From Page -dotnet_diagnostic.CA5368.severity = warning - -# Use XmlReader For Deserialize -dotnet_diagnostic.CA5369.severity = warning - -# Use XmlReader For Validating Reader -dotnet_diagnostic.CA5370.severity = warning - -# Use XmlReader For Schema Read -dotnet_diagnostic.CA5371.severity = warning - -# Use XmlReader For XPathDocument -dotnet_diagnostic.CA5372.severity = warning - -# Do not use obsolete key derivation function -dotnet_diagnostic.CA5373.severity = warning - -# Do Not Use XslTransform -dotnet_diagnostic.CA5374.severity = warning - -# Do Not Use Account Shared Access Signature -dotnet_diagnostic.CA5375.severity = warning - -# Use SharedAccessProtocol HttpsOnly -dotnet_diagnostic.CA5376.severity = warning - -# Use Container Level Access Policy -dotnet_diagnostic.CA5377.severity = warning - -# Do not disable ServicePointManagerSecurityProtocols -dotnet_diagnostic.CA5378.severity = warning - -# Do Not Use Weak Key Derivation Function Algorithm -dotnet_diagnostic.CA5379.severity = warning - -# Do Not Add Certificates To Root Store -dotnet_diagnostic.CA5380.severity = warning - -# Ensure Certificates Are Not Added To Root Store -dotnet_diagnostic.CA5381.severity = warning - -# Use Secure Cookies In ASP.Net Core -dotnet_diagnostic.CA5382.severity = warning - -# Ensure Use Secure Cookies In ASP.Net Core -dotnet_diagnostic.CA5383.severity = warning - -# Do Not Use Digital Signature Algorithm (DSA) -dotnet_diagnostic.CA5384.severity = warning - -# Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size -dotnet_diagnostic.CA5385.severity = warning - -# Avoid hardcoding SecurityProtocolType value -dotnet_diagnostic.CA5386.severity = warning - -# Do Not Use Weak Key Derivation Function With Insufficient Iteration Count -dotnet_diagnostic.CA5387.severity = warning - -# Ensure Sufficient Iteration Count When Using Weak Key Derivation Function -dotnet_diagnostic.CA5388.severity = warning - -# Do Not Add Archive Item's Path To The Target File System Path -dotnet_diagnostic.CA5389.severity = warning - -# Do Not Hard Code Encryption Key -dotnet_diagnostic.CA5390.severity = error - -# Use antiforgery tokens in ASP.NET Core MVC controllers -dotnet_diagnostic.CA5391.severity = warning - -# Use DefaultDllImportSearchPaths attribute for P/Invokes -dotnet_diagnostic.CA5392.severity = warning - -# Do not use unsafe DllImportSearchPath value -dotnet_diagnostic.CA5393.severity = warning - -# Do not use insecure randomness -dotnet_diagnostic.CA5394.severity = warning - -# Miss HttpVerb attribute for action methods -dotnet_diagnostic.CA5395.severity = warning - -# Set HttpOnly to true for HttpCookie -dotnet_diagnostic.CA5396.severity = warning - -# Do not use deprecated SslProtocols values -dotnet_diagnostic.CA5397.severity = warning - -# Avoid hardcoded SslProtocols values -dotnet_diagnostic.CA5398.severity = warning - -# Definitely disable HttpClient certificate revocation list check -dotnet_diagnostic.CA5399.severity = warning - -# Ensure HttpClient certificate revocation list check is not disabled -dotnet_diagnostic.CA5400.severity = warning - -# Do not use CreateEncryptor with non-default IV -dotnet_diagnostic.CA5401.severity = suggestion - -# Use CreateEncryptor with the default IV -dotnet_diagnostic.CA5402.severity = warning - -# Do not hard-code certificate -dotnet_diagnostic.CA5403.severity = error - -# Do not disable token validation checks -dotnet_diagnostic.CA5404.severity = error - -# Do not always skip token validation in delegates -dotnet_diagnostic.CA5405.severity = error - -dotnet_diagnostic.SEC001.severity = error - -# SingleFile Rules (IL3000-IL3005) -# Single-file rules support single-file applications - - -[*.{cs,vb}] - -# Avoid using accessing Assembly file path when publishing as a single-file -dotnet_diagnostic.IL3000.severity = error - -# Avoid accessing Assembly file path when publishing as a single file -dotnet_diagnostic.IL3001.severity = error - -# Avoid calling members annotated with 'RequiresAssemblyFilesAttribute' when publishing as a single file -dotnet_diagnostic.IL3002.severity = error - -# RequiresAssemblyFilesAttribute annotations must match across all interface implementations or overrides -dotnet_diagnostic.IL3003.severity = error - -# RequiresAssemblyFilesAttribute cannot be placed directly on application entry point -dotnet_diagnostic.IL3005.severity = error - -# Style Rules (IDE0001-IDE0380) -# Style rules support consistent code style in your codebase - - -[*.{cs,vb}] - -# Simplify name -dotnet_diagnostic.IDE0001.severity = warning - -# Simplify member access -dotnet_diagnostic.IDE0002.severity = warning - -# this and Me preferences -dotnet_diagnostic.IDE0003.severity = suggestion -dotnet_diagnostic.IDE0009.severity = suggestion - -# Remove unnecessary cast -dotnet_diagnostic.IDE0004.severity = error - -# Remove unnecessary import -dotnet_diagnostic.IDE0005.severity = warning - -# Add missing cases to switch statement -dotnet_diagnostic.IDE0010.severity = warning - -# Add braces -dotnet_diagnostic.IDE0011.severity = silent - -# Use throw expression -dotnet_diagnostic.IDE0016.severity = warning - -# Use object initializers -dotnet_diagnostic.IDE0017.severity = suggestion - -# Inline variable declaration -dotnet_diagnostic.IDE0018.severity = suggestion - -# Use pattern matching to avoid 'as' followed by a 'null' check -dotnet_diagnostic.IDE0019.severity = warning - -# Use pattern matching to avoid 'is' check followed by a cast -dotnet_diagnostic.IDE0020.severity = warning - -# Use expression body for constructors -dotnet_diagnostic.IDE0021.severity = silent - -# Use expression body for methods -dotnet_diagnostic.IDE0022.severity = silent - -# Use expression body for operators -dotnet_diagnostic.IDE0023.severity = silent -dotnet_diagnostic.IDE0024.severity = silent - -# Use expression body for properties -dotnet_diagnostic.IDE0025.severity = silent - -# Use expression body for indexers -dotnet_diagnostic.IDE0026.severity = suggestion - -# Use expression body for accessors -dotnet_diagnostic.IDE0027.severity = silent - -# Use collection initializers -dotnet_diagnostic.IDE0028.severity = suggestion - -# Use coalesce expression -dotnet_diagnostic.IDE0029.severity = suggestion - -# Use coalesce expression (nullable types) -dotnet_diagnostic.IDE0030.severity = suggestion - -# Use null propagation -dotnet_diagnostic.IDE0031.severity = suggestion - -# Use auto property -dotnet_diagnostic.IDE0032.severity = suggestion - -# Use explicitly provided tuple name -dotnet_diagnostic.IDE0033.severity = suggestion - -# Simplify 'default' expression -dotnet_diagnostic.IDE0034.severity = suggestion - -# Remove unreachable code -dotnet_diagnostic.IDE0035.severity = error - -# Order modifiers -dotnet_diagnostic.IDE0036.severity = warning - -# Use inferred member name -dotnet_diagnostic.IDE0037.severity = suggestion - -# Use pattern matching to avoid is check followed by a cast (without variable) -dotnet_diagnostic.IDE0038.severity = warning - -# Use local function instead of lambda -dotnet_diagnostic.IDE0039.severity = suggestion - -# Add accessibility modifiers -dotnet_diagnostic.IDE0040.severity = error - -# Use is null check -dotnet_diagnostic.IDE0041.severity = suggestion - -# Deconstruct variable declaration -dotnet_diagnostic.IDE0042.severity = suggestion - -# Format string contains invalid placeholder -dotnet_diagnostic.IDE0043.severity = warning - -# Add readonly modifier -dotnet_diagnostic.IDE0044.severity = warning - -# Use conditional expression for assignment -dotnet_diagnostic.IDE0045.severity = suggestion - -# Use conditional expression for return -dotnet_diagnostic.IDE0046.severity = suggestion - -# Parentheses preferences -dotnet_diagnostic.IDE0047.severity = warning -dotnet_diagnostic.IDE0048.severity = warning - -# Use language keywords instead of framework type names for type references -dotnet_diagnostic.IDE0049.severity = warning - -# Convert anonymous type to tuple -dotnet_diagnostic.IDE0050.severity = suggestion - -# Remove unused private member -dotnet_diagnostic.IDE0051.severity = warning - -# Remove unread private member -dotnet_diagnostic.IDE0052.severity = error - -# Use expression body for lambdas -dotnet_diagnostic.IDE0053.severity = suggestion - -# Use compound assignment -dotnet_diagnostic.IDE0054.severity = suggestion - -# Fix formatting -dotnet_diagnostic.IDE0055.severity = suggestion - -# Use index operator -dotnet_diagnostic.IDE0056.severity = suggestion - -# Use range operator -dotnet_diagnostic.IDE0057.severity = suggestion - -# Remove unnecessary expression value -dotnet_diagnostic.IDE0058.severity = silent - -# Remove unnecessary value assignment -dotnet_diagnostic.IDE0059.severity = error - -# Remove unused parameter -dotnet_diagnostic.IDE0060.severity = error - -# Use expression body for local functions -dotnet_diagnostic.IDE0061.severity = suggestion - -# Make local function static -dotnet_diagnostic.IDE0062.severity = suggestion - -# Use simple 'using' statement -dotnet_diagnostic.IDE0063.severity = suggestion - -# Make struct fields writable -dotnet_diagnostic.IDE0064.severity = suggestion - -# 'using' directive placement -dotnet_diagnostic.IDE0065.severity = error - -# Use switch expression -dotnet_diagnostic.IDE0066.severity = warning - -# Use 'System.HashCode.Combine' -dotnet_diagnostic.IDE0070.severity = warning - -# Add missing cases to switch expression -dotnet_diagnostic.IDE0072.severity = suggestion - -# Require file header -dotnet_diagnostic.IDE0073.severity = warning - -# Use compound assignment -dotnet_diagnostic.IDE0074.severity = suggestion - -# Simplify conditional expression -dotnet_diagnostic.IDE0075.severity = suggestion - -# Remove invalid global 'SuppressMessageAttribute' -dotnet_diagnostic.IDE0076.severity = silent - -# Avoid legacy format target in global 'SuppressMessageAttribute' -dotnet_diagnostic.IDE0077.severity = warning - -# Use pattern matching -dotnet_diagnostic.IDE0078.severity = warning - -# Remove unnecessary suppression -dotnet_diagnostic.IDE0079.severity = error - -# Remove unnecessary suppression operator -dotnet_diagnostic.IDE0080.severity = suggestion - -# Remove ByVal -dotnet_diagnostic.IDE0081.severity = error - -# Convert typeof to nameof -dotnet_diagnostic.IDE0082.severity = warning - -# Use pattern matching (not operator) -dotnet_diagnostic.IDE0083.severity = warning - -# Use pattern matching (IsNot operator) -dotnet_diagnostic.IDE0084.severity = suggestion - -# Simplify new expression -dotnet_diagnostic.IDE0090.severity = warning - -# Remove unnecessary equality operator -dotnet_diagnostic.IDE0100.severity = silent - -# Use conditional delegate call -dotnet_diagnostic.IDE0105.severity = warning - -# Remove unnecessary discard -dotnet_diagnostic.IDE0110.severity = warning - -# Simplify LINQ expression -dotnet_diagnostic.IDE0120.severity = warning - -# Simplify LINQ type check and cast -dotnet_diagnostic.IDE0121.severity = warning - -# Simplify object creation -dotnet_diagnostic.IDE0140.severity = suggestion - -# Prefer 'null' check over type check -dotnet_diagnostic.IDE0150.severity = warning - -# Use block-scoped namespace -dotnet_diagnostic.IDE0160.severity = suggestion - -# Use file-scoped namespace -dotnet_diagnostic.IDE0161.severity = suggestion - -# Simplify property pattern -dotnet_diagnostic.IDE0170.severity = warning - -# Use tuple to swap values -dotnet_diagnostic.IDE0180.severity = suggestion - -# Unnecessary lambda expression -dotnet_diagnostic.IDE0200.severity = warning - -# Convert to top-level statements -dotnet_diagnostic.IDE0210.severity = warning - -# Convert to 'Program.Main' style program -dotnet_diagnostic.IDE0211.severity = suggestion - -# Add explicit cast in foreach loop -dotnet_diagnostic.IDE0220.severity = error - -# Use UTF-8 string literal -dotnet_diagnostic.IDE0230.severity = warning - -# Nullable directive is redundant -dotnet_diagnostic.IDE0240.severity = error - -# Nullable directive is unnecessary -dotnet_diagnostic.IDE0241.severity = error - -# Struct can be made 'readonly' -dotnet_diagnostic.IDE0250.severity = warning - -# Member can be made 'readonly' -dotnet_diagnostic.IDE0251.severity = warning - -# Use pattern matching -dotnet_diagnostic.IDE0260.severity = warning - -# Null check can be simplified -dotnet_diagnostic.IDE0270.severity = warning - -# Use 'nameof' -dotnet_diagnostic.IDE0280.severity = error - -# Use primary constructor -dotnet_diagnostic.IDE0290.severity = suggestion - -# Use collection expression for array -dotnet_diagnostic.IDE0300.severity = warning - -# Use collection expression for empty -dotnet_diagnostic.IDE0301.severity = suggestion - -# Use collection expression for stackalloc -dotnet_diagnostic.IDE0302.severity = warning - -# Use collection expression for Create() -dotnet_diagnostic.IDE0303.severity = warning - -# Use collection expression for builder -dotnet_diagnostic.IDE0304.severity = warning - -# Use collection expression for fluent -dotnet_diagnostic.IDE0305.severity = warning - -# Use collection expression for new -dotnet_diagnostic.IDE0306.severity = warning - -# Make anonymous function static -dotnet_diagnostic.IDE0320.severity = warning - -# Prefer 'System.Threading.Lock' -dotnet_diagnostic.IDE0330.severity = warning - -# Use unbound generic type -dotnet_diagnostic.IDE0340.severity = warning - -# Use implicitly typed lambda -dotnet_diagnostic.IDE0350.severity = warning - -# Simplify property accessor -dotnet_diagnostic.IDE0360.severity = suggestion - -# Remove unnecessary `unsafe` modifier -dotnet_diagnostic.IDE0380.severity = warning - -# Remove unnecessary suppression (null-forgiving operator) -dotnet_diagnostic.IDE0370.severity = warning - -# Naming rule violation -dotnet_diagnostic.IDE1006.severity = silent - -# Embedded statements must be on their own line -dotnet_diagnostic.IDE2001.severity = warning - -# Consecutive braces must not have blank line between them -dotnet_diagnostic.IDE2002.severity = warning - -# Blank line required between block and subsequent statement -dotnet_diagnostic.IDE2003.severity = warning - -# Blank line not allowed after constructor initializer colon -dotnet_diagnostic.IDE2004.severity = warning - -# Blank line not allowed after conditional expression token -dotnet_diagnostic.IDE2005.severity = warning - -# Blank line not allowed after arrow expression clause token -dotnet_diagnostic.IDE2006.severity = warning - -# C# Style Rules - var preferences - -[*.{cs,csx,cake}] - -dotnet_diagnostic.IDE0007.severity = warning -dotnet_diagnostic.IDE0008.severity = none -dotnet_diagnostic.PDS0003.severity = warning -dotnet_diagnostic.PDS0004.severity = warning - -# Usage Rules (CA1801, CA1816, CA2200-CA2267) -# Usage rules support proper usage of .NET - - -[*.{cs,vb}] - -# Review unused parameters -dotnet_diagnostic.CA1801.severity = warning - -# Call GC.SuppressFinalize correctly -dotnet_diagnostic.CA1816.severity = error - -# Rethrow to preserve stack details -dotnet_diagnostic.CA2200.severity = error - -# Do not raise reserved exception types -dotnet_diagnostic.CA2201.severity = error - -# Initialize value type static fields inline -dotnet_diagnostic.CA2207.severity = warning - -# Instantiate argument exceptions correctly -dotnet_diagnostic.CA2208.severity = error - -# Non-constant fields should not be visible -dotnet_diagnostic.CA2211.severity = error - -# Disposable fields should be disposed -dotnet_diagnostic.CA2213.severity = error - -# Do not call overridable methods in constructors -dotnet_diagnostic.CA2214.severity = warning - -# Dispose methods should call base class dispose -dotnet_diagnostic.CA2215.severity = error - -# Disposable types should declare finalizer -dotnet_diagnostic.CA2216.severity = warning - -# Do not mark enums with FlagsAttribute -dotnet_diagnostic.CA2217.severity = error - -# Override GetHashCode on overriding Equals -dotnet_diagnostic.CA2218.severity = warning - -# Do not raise exceptions in exception clauses -dotnet_diagnostic.CA2219.severity = error - -# Override Equals on overloading operator equals -dotnet_diagnostic.CA2224.severity = warning - -# Operator overloads have named alternates -dotnet_diagnostic.CA2225.severity = warning - -# Operators should have symmetrical overloads -dotnet_diagnostic.CA2226.severity = warning - -# Collection properties should be read only -dotnet_diagnostic.CA2227.severity = error - -# Implement serialization constructors -dotnet_diagnostic.CA2229.severity = warning - -# Overload operator equals on overriding ValueType.Equals -dotnet_diagnostic.CA2231.severity = warning - -# Pass System.Uri objects instead of strings -dotnet_diagnostic.CA2234.severity = warning - -# Mark all non-serializable fields -dotnet_diagnostic.CA2235.severity = suggestion - -# Mark ISerializable types with SerializableAttribute -dotnet_diagnostic.CA2237.severity = warning - -# Provide correct arguments to formatting methods -dotnet_diagnostic.CA2241.severity = warning - -# Test for NaN correctly -dotnet_diagnostic.CA2242.severity = warning - -# Attribute string literals should parse correctly -dotnet_diagnostic.CA2243.severity = warning - -# Do not duplicate indexed element initializations -dotnet_diagnostic.CA2244.severity = warning - -# Do not assign a property to itself -dotnet_diagnostic.CA2245.severity = error - -# Do not assign a symbol and its member in the same statement -dotnet_diagnostic.CA2246.severity = warning - -# Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum -dotnet_diagnostic.CA2247.severity = warning - -# Provide correct enum argument to Enum.HasFlag -dotnet_diagnostic.CA2248.severity = warning - -# Consider using String.Contains instead of String.IndexOf -dotnet_diagnostic.CA2249.severity = error - -# Use ThrowIfCancellationRequested -dotnet_diagnostic.CA2250.severity = warning - -# Use String.Equals over String.Compare -dotnet_diagnostic.CA2251.severity = warning - -# Opt in to preview features -dotnet_diagnostic.CA2252.severity = suggestion - -# Named placeholders should not be numeric values -dotnet_diagnostic.CA2253.severity = warning - -# Template should be a static expression -dotnet_diagnostic.CA2254.severity = warning - -# The ModuleInitializer attribute should not be used in libraries -dotnet_diagnostic.CA2255.severity = warning - -# All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface -dotnet_diagnostic.CA2256.severity = warning - -# Members defined on an interface with 'DynamicInterfaceCastableImplementationAttribute' should be 'static' -dotnet_diagnostic.CA2257.severity = warning - -# Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported -dotnet_diagnostic.CA2258.severity = warning - -# Ensure ThreadStatic is only used with static fields -dotnet_diagnostic.CA2259.severity = error - -# Implement generic math interfaces correctly -dotnet_diagnostic.CA2260.severity = warning - -# Do not use ConfigureAwaitOptions.SuppressThrowing with Task -dotnet_diagnostic.CA2261.severity = warning - -# Set 'MaxResponseHeadersLength' properly -dotnet_diagnostic.CA2262.severity = warning - -# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' -dotnet_diagnostic.CA2264.severity = warning - -# Do not compare Span to null or default -dotnet_diagnostic.CA2265.severity = error - -# Use correct type parameter -dotnet_diagnostic.CA2266.severity = warning - -# Use correct type parameter for 'IAsyncEnumerable.ConfigureAwait' -dotnet_diagnostic.CA2267.severity = warning - -# External Analyzer Rules -# Rules from StyleCop, Async, Dispose, Regex, JSON, MSTest, and other third-party analyzers - - - -[*.{cs,vb}] - -# StyleCop Analyzer Rules (SA) - -# A violation of this rule occurs when a compilation (project) contains files with DocumentationMode set to None -dotnet_diagnostic.SA0001.severity = warning - -# The spacing around a C# keyword is incorrect -dotnet_diagnostic.SA1000.severity = warning - -# DoNotPrefixCallsWithBaseUnlessLocalImplementationExists -dotnet_diagnostic.SA1100.severity = warning - -# StatementMustNotUseUnnecessaryParenthesis -dotnet_diagnostic.SA1119.severity = suggestion - -# CommentsMustContainText -dotnet_diagnostic.SA1120.severity = warning - -# DoNotUseRegions -dotnet_diagnostic.SA1124.severity = warning - -# UsingDirectivesMustBePlacedCorrectly -dotnet_diagnostic.SA1200.severity = warning - -# ElementsMustAppearInTheCorrectOrder -dotnet_diagnostic.SA1201.severity = warning - -# ElementsMustBeOrderedByAccess -dotnet_diagnostic.SA1202.severity = warning - -# ElementMustBeginWithUpperCaseLetter -dotnet_diagnostic.SA1300.severity = error - -# InterfaceNamesMustBeginWithI -dotnet_diagnostic.SA1302.severity = error - -# ConstFieldNamesMustBeginWithUpperCaseLetter -dotnet_diagnostic.SA1303.severity = error - -# FieldNamesMustNotUseHungarianNotation -dotnet_diagnostic.SA1305.severity = warning - -# VariableNamesMustNotBePrefixed -dotnet_diagnostic.SA1308.severity = error - -# A field name in C# begins with an underscore -dotnet_diagnostic.SA1309.severity = silent - -# VariableNamesMustBeginWithLowerCaseLetter -dotnet_diagnostic.SA1312.severity = warning - -# ParameterNamesMustBeginWithLowerCaseLetter -dotnet_diagnostic.SA1313.severity = error - -# TupleElementNamesShouldUseCorrectCasing -dotnet_diagnostic.SA1316.severity = warning - -# AccessModifierMustBeDeclared -dotnet_diagnostic.SA1400.severity = warning - -# FieldsMustBePrivate -dotnet_diagnostic.SA1401.severity = error - -# FileMayOnlyContainASingleNamespace -dotnet_diagnostic.SA1403.severity = error - -# A Code Analysis SuppressMessage attribute does not include a justification -dotnet_diagnostic.SA1404.severity = error - -# DebugAssertMustProvideMessageText -dotnet_diagnostic.SA1405.severity = warning - -# DebugFailMustProvideMessageText -dotnet_diagnostic.SA1406.severity = warning - -# RemoveUnnecessaryCode -dotnet_diagnostic.SA1409.severity = error - -# BracesForMultiLineStatementsMustNotShareLine -dotnet_diagnostic.SA1500.severity = error - -# ElementMustNotBeOnSingleLine -dotnet_diagnostic.SA1502.severity = error - -# BracesMustNotBeOmitted -dotnet_diagnostic.SA1503.severity = error - -# ClosingBraceMustBeFollowedByBlankLine -dotnet_diagnostic.SA1513.severity = warning - -# UseBracesConsistently -dotnet_diagnostic.SA1520.severity = error - -# File header copyright text should match -dotnet_diagnostic.SA1636.severity = none - -# ElementDocumentationMustBeSpelledCorrectly -dotnet_diagnostic.SA1650.severity = warning - -# Dispose Analyzer Rules (IDISP) - -# Dispose created -dotnet_diagnostic.IDISP001.severity = error - -# Dispose member -dotnet_diagnostic.IDISP002.severity = error - -# Dispose previous before re-assigning -dotnet_diagnostic.IDISP003.severity = error - -# Don't ignore created IDisposable -dotnet_diagnostic.IDISP004.severity = error - -# Return type should indicate that the value should be disposed -dotnet_diagnostic.IDISP005.severity = error - -# Implement IDisposable -dotnet_diagnostic.IDISP006.severity = error - -# Don't dispose injected -dotnet_diagnostic.IDISP007.severity = error - -# Don't assign member with injected and created disposables -dotnet_diagnostic.IDISP008.severity = error - -# Add IDisposable interface -dotnet_diagnostic.IDISP009.severity = error - -# Call base.Dispose(disposing) -dotnet_diagnostic.IDISP010.severity = error - -# Don't return disposed instance -dotnet_diagnostic.IDISP011.severity = warning - -# Property should not return created disposable -dotnet_diagnostic.IDISP012.severity = error - -# Await in using -dotnet_diagnostic.IDISP013.severity = error - -# Use a single instance of HttpClient -dotnet_diagnostic.IDISP014.severity = warning - -# Member should not return created and cached instance -dotnet_diagnostic.IDISP015.severity = error - -# Don't use disposed instance -dotnet_diagnostic.IDISP016.severity = error - -# Prefer using -dotnet_diagnostic.IDISP017.severity = error - -# Inline variable declaration -dotnet_diagnostic.IDISP018.severity = error - -# Call SuppressFinalize -dotnet_diagnostic.IDISP019.severity = error - -# Use pattern matching to avoid is check followed by a cast (with variable) -dotnet_diagnostic.IDISP020.severity = error - -# Call this.Dispose(true) -dotnet_diagnostic.IDISP021.severity = error - -# Call this.Dispose(false) -dotnet_diagnostic.IDISP022.severity = error - -# Don't use reference types in finalizer context -dotnet_diagnostic.IDISP023.severity = error - -# Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer -dotnet_diagnostic.IDISP024.severity = error - -# Class with no virtual dispose method should be sealed -dotnet_diagnostic.IDISP025.severity = error - -# Class with no virtual DisposeAsyncCore method should be sealed -dotnet_diagnostic.IDISP026.severity = error - -# Async Analyzer Rules (ASYNC, VSTHRD, RCS) - -# Asynchronous method names should end with Async -dotnet_diagnostic.ASYNC0001.severity = error - -# Non asynchronous method names should not end with Async -dotnet_diagnostic.ASYNC0002.severity = error - -# Avoid void returning asynchronous method -dotnet_diagnostic.ASYNC0003.severity = warning - -# Use ConfigureAwait(false) on await expression -dotnet_diagnostic.ASYNC0004.severity = error - -# Do not use blocking call (make method async) -dotnet_diagnostic.MA0045.severity = none - -# Call 'ConfigureAwait(false)' -dotnet_diagnostic.RCS1090.severity = error - -# Return completed task instead of returning null -dotnet_diagnostic.RCS1210.severity = error - -# AsyncifyInvocation: Use Task Async -dotnet_diagnostic.AsyncifyInvocation.severity = error - -# AsyncifyVariable: Use Task Async -dotnet_diagnostic.AsyncifyVariable.severity = error - -# Avoid legacy thread switching methods -dotnet_diagnostic.VSTHRD001.severity = error - -# Avoid problematic synchronous waits -dotnet_diagnostic.VSTHRD002.severity = error - -# Avoid awaiting foreign Tasks -dotnet_diagnostic.VSTHRD003.severity = error - -# Await SwitchToMainThreadAsync -dotnet_diagnostic.VSTHRD004.severity = error - -# Invoke single-threaded types on Main thread -dotnet_diagnostic.VSTHRD010.severity = error - -# Use AsyncLazy -dotnet_diagnostic.VSTHRD011.severity = error - -# Provide JoinableTaskFactory where allowed -dotnet_diagnostic.VSTHRD012.severity = error - -# Avoid async void methods -dotnet_diagnostic.VSTHRD100.severity = error - -# Avoid unsupported async delegates -dotnet_diagnostic.VSTHRD101.severity = error - -# Implement internal logic asynchronously -dotnet_diagnostic.VSTHRD102.severity = error - -# Call async methods when in an async method -dotnet_diagnostic.VSTHRD103.severity = error - -# Offer async option -dotnet_diagnostic.VSTHRD104.severity = warning - -# Avoid method overloads that assume TaskScheduler.Current -dotnet_diagnostic.VSTHRD105.severity = warning - -# Use InvokeAsync to raise async events -dotnet_diagnostic.VSTHRD106.severity = error - -# Await Task within using expression -dotnet_diagnostic.VSTHRD107.severity = error - -# Assert thread affinity unconditionally -dotnet_diagnostic.VSTHRD108.severity = error - -# Switch instead of assert in async methods -dotnet_diagnostic.VSTHRD109.severity = error - -# Observe result of async calls -dotnet_diagnostic.VSTHRD110.severity = error - -# Use .ConfigureAwait(bool) -dotnet_diagnostic.VSTHRD111.severity = error - -# Implement System.IAsyncDisposable -dotnet_diagnostic.VSTHRD112.severity = error - -# Check for System.IAsyncDisposable -dotnet_diagnostic.VSTHRD113.severity = error - -# Avoid returning a null Task -dotnet_diagnostic.VSTHRD114.severity = error - -# Use "Async" suffix for async methods -dotnet_diagnostic.VSTHRD200.severity = error - -# Asynchronous method name should end with 'Async' -dotnet_diagnostic.RCS1046.severity = error - -# Non-asynchronous method name should not end with 'Async' -dotnet_diagnostic.RCS1047.severity = error - -# Class Analyzer Rules - -# Seal Class -dotnet_diagnostic.CLASS0001.severity = warning - -# Enum Analyzer Rules - -# Default switch label -dotnet_diagnostic.ENUM0001.severity = error - -# Merge switch sections -dotnet_diagnostic.ENUM0002.severity = warning - -# Populate switch -dotnet_diagnostic.ENUM0003.severity = warning - -# Regex Analyzer Rules (RE) - -# Invalid regex pattern -dotnet_diagnostic.RE0001.severity = warning - -# JSON Analyzer Rules - -# Invalid JSON pattern -dotnet_diagnostic.JSON001.severity = suggestion - -# Probable JSON string detected -dotnet_diagnostic.JSON002.severity = suggestion - -# Return Analyzer Rules - -# Do not return null -dotnet_diagnostic.RETURN0001.severity = warning - -# Roslynator Analyzer Rules (RCS) - -# Simplify boolean comparison -dotnet_diagnostic.RCS1049.severity = silent - -# Remove unnecessary 'Imports' or 'using' directive -dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = warning - -# MSTest Analyzer Rules (MSTEST) - -# Explicitly enable or disable tests parallelization -dotnet_diagnostic.MSTEST0001.severity = warning - -# Test classes should have valid layout -dotnet_diagnostic.MSTEST0002.severity = warning - -# Test methods should have valid layout -dotnet_diagnostic.MSTEST0003.severity = warning - -# Public types should be test classes -dotnet_diagnostic.MSTEST0004.severity = none - -# Test context property should have valid layout -dotnet_diagnostic.MSTEST0005.severity = warning - -# Avoid [ExpectedException] -dotnet_diagnostic.MSTEST0006.severity = warning - -# Use test attributes only on test methods -dotnet_diagnostic.MSTEST0007.severity = warning - -# TestInitialize method should have valid layout -dotnet_diagnostic.MSTEST0008.severity = warning - -# TestCleanup method should have valid layout -dotnet_diagnostic.MSTEST0009.severity = warning - -# ClassInitialize method should have valid layout -dotnet_diagnostic.MSTEST0010.severity = warning - -# ClassCleanup method should have valid layout -dotnet_diagnostic.MSTEST0011.severity = warning - -# AssemblyInitialize method should have valid layout -dotnet_diagnostic.MSTEST0012.severity = warning - -# AssemblyCleanup method should have valid layout -dotnet_diagnostic.MSTEST0013.severity = warning - -# DataRow should be valid -dotnet_diagnostic.MSTEST0014.severity = warning - -# Test method should not be ignored -dotnet_diagnostic.MSTEST0015.severity = warning - -# Test class should have test method -dotnet_diagnostic.MSTEST0016.severity = warning - -# Assertion arguments should be passed in the correct order -dotnet_diagnostic.MSTEST0017.severity = warning - -# DynamicData should be valid -dotnet_diagnostic.MSTEST0018.severity = warning - -# Prefer TestInitialize methods over constructors -dotnet_diagnostic.MSTEST0019.severity = warning - -# Prefer constructors over TestInitialize methods -dotnet_diagnostic.MSTEST0020.severity = warning - -# Prefer Dispose over TestCleanup methods -dotnet_diagnostic.MSTEST0021.severity = warning - -# Prefer TestCleanup methods over Dispose -dotnet_diagnostic.MSTEST0022.severity = warning - -# Do not negate boolean assertions -dotnet_diagnostic.MSTEST0023.severity = warning - -# Do not store TestContext in a static member -dotnet_diagnostic.MSTEST0024.severity = warning - -# Use 'Assert.Fail' instead of an always-failing assert -dotnet_diagnostic.MSTEST0025.severity = warning - -# Avoid conditional access in assertions -dotnet_diagnostic.MSTEST0026.severity = warning - -# Non-nullable reference not initialized suppressor -dotnet_diagnostic.MSTEST0027.severity = none - -# Non-nullable reference not initialized suppressor -dotnet_diagnostic.MSTEST0028.severity = none - -# Public method should be test method -dotnet_diagnostic.MSTEST0029.severity = warning - -# Type containing [TestMethod] should be marked with [TestClass] -dotnet_diagnostic.MSTEST0030.severity = warning - -# System.ComponentModel.DescriptionAttribute has no effect on test methods -dotnet_diagnostic.MSTEST0031.severity = warning - -# Review or remove the assertion as its condition is known to be always true -dotnet_diagnostic.MSTEST0032.severity = warning - -# Non-nullable reference not initialized suppressor -dotnet_diagnostic.MSTEST0033.severity = none - -# Use ClassCleanupBehavior.EndOfClass with the [ClassCleanup] -dotnet_diagnostic.MSTEST0034.severity = warning - -# [DeploymentItem] can be specified only on test class or test method -dotnet_diagnostic.MSTEST0035.severity = warning - -# Do not use shadowing inside test class -dotnet_diagnostic.MSTEST0036.severity = warning - -# Use proper 'Assert' methods -dotnet_diagnostic.MSTEST0037.severity = warning - -# Don't use 'Assert.AreSame' or 'Assert.AreNotSame' with value types -dotnet_diagnostic.MSTEST0038.severity = warning - -# Use newer 'Assert.Throws' methods -dotnet_diagnostic.MSTEST0039.severity = warning - -# Do not assert inside 'async void' contexts -dotnet_diagnostic.MSTEST0040.severity = warning - -# Use 'ConditionBaseAttribute' on test classes -dotnet_diagnostic.MSTEST0041.severity = warning - -# Obsolete API Rules (SYSLIB) -# Rules for obsolete .NET APIs and platform-specific warnings - - -[*.{cs,vb}] - -# The UTF-7 encoding is insecure -dotnet_diagnostic.SYSLIB0001.severity = error - -# PrincipalPermissionAttribute is obsolete -dotnet_diagnostic.SYSLIB0002.severity = error - -# Code access security is not supported -dotnet_diagnostic.SYSLIB0003.severity = error - -# The constrained execution region (CER) feature is not supported -dotnet_diagnostic.SYSLIB0004.severity = error - -# The global assembly cache (GAC) is not supported -dotnet_diagnostic.SYSLIB0005.severity = error - -# Thread.Abort is not supported -dotnet_diagnostic.SYSLIB0006.severity = error - -# Default implementations of cryptography algorithms not supported -dotnet_diagnostic.SYSLIB0007.severity = error - -# CreatePdbGenerator is not supported -dotnet_diagnostic.SYSLIB0008.severity = error - -# The AuthenticationManager Authenticate and PreAuthenticate methods are not supported -dotnet_diagnostic.SYSLIB0009.severity = error - -# Unsupported remoting APIs -dotnet_diagnostic.SYSLIB0010.severity = error - -# BinaryFormatter serialization is obsolete -dotnet_diagnostic.SYSLIB0011.severity = error - -# Type or member is obsolete -dotnet_diagnostic.SYSLIB0012.severity = error - -# EscapeUriString is obsolete -dotnet_diagnostic.SYSLIB0013.severity = error - -# WebRequest, HttpWebRequest, ServicePoint, WebClient are obsolete -dotnet_diagnostic.SYSLIB0014.severity = error - -# DisablePrivateReflectionAttribute is obsolete -dotnet_diagnostic.SYSLIB0015.severity = error - -# GetContextInfo() is obsolete -dotnet_diagnostic.SYSLIB0016.severity = error - -# Strong-name signing is not supported and throws PlatformNotSupportedException -dotnet_diagnostic.SYSLIB0017.severity = error - -# Reflection-only loading is not supported and throws PlatformNotSupportedException -dotnet_diagnostic.SYSLIB0018.severity = error - -# Some RuntimeEnvironment APIs are obsolete -dotnet_diagnostic.SYSLIB0019.severity = error - -# IgnoreNullValues is obsolete -dotnet_diagnostic.SYSLIB0020.severity = error - -# Derived cryptographic types are obsolete -dotnet_diagnostic.SYSLIB0021.severity = error - -# The Rijndael and RijndaelManaged types are obsolete -dotnet_diagnostic.SYSLIB0022.severity = error - -# RNGCryptoServiceProvider is obsolete -dotnet_diagnostic.SYSLIB0023.severity = error - -# Creating and unloading AppDomains is not supported and throws an exception -dotnet_diagnostic.SYSLIB0024.severity = error - -# SuppressIldasmAttribute is obsolete -dotnet_diagnostic.SYSLIB0025.severity = error - -# X509Certificate and X509Certificate2 are immutable -dotnet_diagnostic.SYSLIB0026.severity = error - -# PublicKey.Key is obsolete -dotnet_diagnostic.SYSLIB0027.severity = error - -# X509Certificate2.PrivateKey is obsolete -dotnet_diagnostic.SYSLIB0028.severity = error - -# ProduceLegacyHmacValues is obsolete -dotnet_diagnostic.SYSLIB0029.severity = error - -# HMACSHA1 always uses the algorithm implementation provided by the platform -dotnet_diagnostic.SYSLIB0030.severity = error - -# EncodeOID is obsolete -dotnet_diagnostic.SYSLIB0031.severity = error - -# Recovery from corrupted process state exceptions is not supported -dotnet_diagnostic.SYSLIB0032.severity = error - -# Rfc2898DeriveBytes.CryptDeriveKey is obsolete -dotnet_diagnostic.SYSLIB0033.severity = error - -# CmsSigner(CspParameters) constructor is obsolete -dotnet_diagnostic.SYSLIB0034.severity = error - -# ComputeCounterSignature without specifying a CmsSigner is obsolete -dotnet_diagnostic.SYSLIB0035.severity = error - -# Regex.CompileToAssembly is obsolete -dotnet_diagnostic.SYSLIB0036.severity = error - -# AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete -dotnet_diagnostic.SYSLIB0037.severity = error - -# SerializationFormat.Binary is obsolete -dotnet_diagnostic.SYSLIB0038.severity = error - -# SslProtocols.Tls and SslProtocols.Tls11 are obsolete -dotnet_diagnostic.SYSLIB0039.severity = error - -# EncryptionPolicy.NoEncryption and EncryptionPolicy.AllowNoEncryption are obsolete -dotnet_diagnostic.SYSLIB0040.severity = error - -# Some Rfc2898DeriveBytes constructors are obsolete -dotnet_diagnostic.SYSLIB0041.severity = error - -# FromXmlString and ToXmlString on ECC types are obsolete -dotnet_diagnostic.SYSLIB0042.severity = error - -# ECDiffieHellmanPublicKey.ToByteArray is obsolete -dotnet_diagnostic.SYSLIB0043.severity = error - -# AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete -dotnet_diagnostic.SYSLIB0044.severity = error - -# Some cryptographic factory methods are obsolete -dotnet_diagnostic.SYSLIB0045.severity = error - -# ControlledExecution.Run should not be used -dotnet_diagnostic.SYSLIB0046.severity = error - -# XmlSecureResolver is obsolete -dotnet_diagnostic.SYSLIB0047.severity = error - -# RSA.EncryptValue(Byte[]) and RSA.DecryptValue(Byte[]) are obsolete -dotnet_diagnostic.SYSLIB0048.severity = error - -# JsonSerializerOptions.AddContext is obsolete -dotnet_diagnostic.SYSLIB0049.severity = error - -# Formatter-based serialization is obsolete -dotnet_diagnostic.SYSLIB0050.severity = error - -# APIs that support obsolete formatter-based serialization are obsolete -dotnet_diagnostic.SYSLIB0051.severity = error - -# APIs that support obsolete mechanisms for Regex extensibility are obsolete -dotnet_diagnostic.SYSLIB0052.severity = error - -# AesGcm should indicate the required tag size for encryption and decryption -dotnet_diagnostic.SYSLIB0053.severity = error - -# Thread.VolatileRead and Thread.VolatileWrite are obsolete -dotnet_diagnostic.SYSLIB0054.severity = warning - -# AdvSimd.ShiftRightLogicalRoundedNarrowingSaturate* methods with signed parameters are obsolete -dotnet_diagnostic.SYSLIB0055.severity = warning - -# Assembly.LoadFrom that takes an AssemblyHashAlgorithm is obsolete -dotnet_diagnostic.SYSLIB0056.severity = warning - -# X509Certificate2 and X509Certificate constructors for binary and file content are obsolete -dotnet_diagnostic.SYSLIB0057.severity = warning - -# The KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherAlgorithmStrength, HashAlgorithm, and HashStrength properties of SslStream are obsolete -dotnet_diagnostic.SYSLIB0058.severity = warning - -# SystemEvents.EventsThreadShutdown callbacks aren't run before the process exits -dotnet_diagnostic.SYSLIB0059.severity = warning - -# Rfc2898DeriveBytes constructors are obsolete -dotnet_diagnostic.SYSLIB0060.severity = error - -# System.Linq.Queryable.MaxBy and System.Linq.Queryable.MinBy taking an IComparer are obsolete -dotnet_diagnostic.SYSLIB0061.severity = error - -# Source Generator Diagnostics (SYSLIB1xxx) - -# Logging method names can't start with an underscore -dotnet_diagnostic.SYSLIB1001.severity = error - -# Don't include log level parameters as templates in the logging message -dotnet_diagnostic.SYSLIB1002.severity = error - -# Logging method parameter names can't start with an underscore -dotnet_diagnostic.SYSLIB1003.severity = error - -# Could not find a required type definition -dotnet_diagnostic.SYSLIB1005.severity = error - -# Multiple logging methods cannot use the same event ID -dotnet_diagnostic.SYSLIB1006.severity = error - -# Logging methods must return void -dotnet_diagnostic.SYSLIB1007.severity = error - -# One of the arguments to a logging method must implement the ILogger interface -dotnet_diagnostic.SYSLIB1008.severity = error - -# Logging methods must be static -dotnet_diagnostic.SYSLIB1009.severity = error - -# Logging methods must be partial -dotnet_diagnostic.SYSLIB1010.severity = error - -# Logging methods cannot be generic -dotnet_diagnostic.SYSLIB1011.severity = error - -# Redundant qualifier in logging message -dotnet_diagnostic.SYSLIB1012.severity = error - -# Don't include exception parameters as templates in the logging message -dotnet_diagnostic.SYSLIB1013.severity = error - -# Logging template has no corresponding method argument -dotnet_diagnostic.SYSLIB1014.severity = error - -# Argument is not referenced from the logging message -dotnet_diagnostic.SYSLIB1015.severity = error - -# Logging methods cannot have a body -dotnet_diagnostic.SYSLIB1016.severity = error - -# A LogLevel value must be supplied in the LoggerMessage attribute or as a parameter to the logging method -dotnet_diagnostic.SYSLIB1017.severity = error - -# Don't include logger parameters as templates in the logging message -dotnet_diagnostic.SYSLIB1018.severity = error - -# Couldn't find a field of type ILogger -dotnet_diagnostic.SYSLIB1019.severity = error - -# Found multiple fields of type ILogger -dotnet_diagnostic.SYSLIB1020.severity = error - -# Multiple message-template item names differ only by case -dotnet_diagnostic.SYSLIB1021.severity = error - -# Can't have malformed format strings -dotnet_diagnostic.SYSLIB1022.severity = error - -# Generating more than six arguments is not supported -dotnet_diagnostic.SYSLIB1023.severity = error - -# System.Text.Json source generator did not generate output for type -dotnet_diagnostic.SYSLIB1030.severity = error - -# System.Text.Json source generator encountered a duplicate type info property name -dotnet_diagnostic.SYSLIB1031.severity = error - -# Context classes to be augmented by the System.Text.Json source generator must be declared as partial -dotnet_diagnostic.SYSLIB1032.severity = error - -# System.Text.Json source generator encountered a type with multiple [JsonConstructor] annotations -dotnet_diagnostic.SYSLIB1033.severity = warning - -# JsonSourceGenerator encountered a [JsonStringEnumConverter] annotation -dotnet_diagnostic.SYSLIB1034.severity = warning - -# System.Text.Json source generator encountered a type with multiple [JsonExtensionData] annotations -dotnet_diagnostic.SYSLIB1035.severity = error - -# System.Text.Json source generator encountered an invalid [JsonExtensionData] annotation -dotnet_diagnostic.SYSLIB1036.severity = error - -# System.Text.Json source generator encountered a type with init-only properties which are not supported for deserialization -dotnet_diagnostic.SYSLIB1037.severity = error - -# System.Text.Json source generator encountered a property annotated with [JsonInclude] but with inaccessible accessors -dotnet_diagnostic.SYSLIB1038.severity = error - -# Invalid GeneratedRegexAttribute usage -dotnet_diagnostic.SYSLIB1040.severity = error - -# Multiple GeneratedRegexAttribute attributes were applied to the same method, but only one is allowed -dotnet_diagnostic.SYSLIB1041.severity = error - -# The specified regular expression is invalid -dotnet_diagnostic.SYSLIB1042.severity = error - -# A GeneratedRegexAttribute method must be partial, parameterless, non-generic, and non-abstract, and return Regex -dotnet_diagnostic.SYSLIB1043.severity = error - -# The regex generator couldn't generate a complete source implementation for the specified regular expression due to an internal limitation -dotnet_diagnostic.SYSLIB1044.severity = error - -# Use GeneratedRegexAttribute to generate the regular expression implementation at compile time -dotnet_diagnostic.SYSLIB1045.severity = warning - -# Invalid LibraryImportAttribute usage -dotnet_diagnostic.SYSLIB1050.severity = error - -# The specified type is not supported by source-generated p/invokes -dotnet_diagnostic.SYSLIB1051.severity = error - -# The specified configuration is not supported by source-generated p/invokes -dotnet_diagnostic.SYSLIB1052.severity = error - -# The specified LibraryImportAttribute arguments cannot be forwarded to DllImportAttribute -dotnet_diagnostic.SYSLIB1053.severity = error - -# Use LibraryImportAttribute instead of DllImportAttribute to generate p/invoke marshalling code at compile time -dotnet_diagnostic.SYSLIB1054.severity = error - -# Invalid CustomMarshallerAttribute usage -dotnet_diagnostic.SYSLIB1055.severity = error - -# The specified native type is invalid -dotnet_diagnostic.SYSLIB1056.severity = error - -# The marshaller type does not have the required shape -dotnet_diagnostic.SYSLIB1057.severity = error - -# Invalid NativeMarshallingAttribute usage -dotnet_diagnostic.SYSLIB1058.severity = error - -# The marshaller type does not support an allocating constructor -dotnet_diagnostic.SYSLIB1059.severity = error - -# The specified marshaller type is invalid -dotnet_diagnostic.SYSLIB1060.severity = error - -# The marshaller type has incompatible method signatures -dotnet_diagnostic.SYSLIB1061.severity = error - -# The project must be updated with true -dotnet_diagnostic.SYSLIB1062.severity = error - -# Invalid JSImportAttribute usage -dotnet_diagnostic.SYSLIB1070.severity = error - -# Invalid JSExportAttribute usage -dotnet_diagnostic.SYSLIB1071.severity = error - -# The specified type is not supported by source-generated JavaScript interop -dotnet_diagnostic.SYSLIB1072.severity = error - -# The specified configuration is not supported by source-generated JavaScript interop -dotnet_diagnostic.SYSLIB1073.severity = error - -# JSImportAttribute requires unsafe code -dotnet_diagnostic.SYSLIB1074.severity = error - -# JSExportAttribute requires unsafe code -dotnet_diagnostic.SYSLIB1075.severity = error - -# Invalid GeneratedComInterfaceAttribute usage -dotnet_diagnostic.SYSLIB1090.severity = error - -# Method is declared in different partial declaration than the GeneratedComInterface attribute -dotnet_diagnostic.SYSLIB1091.severity = error - -# Usage of LibraryImport or GeneratedComInterface attribute does not follow recommendation -dotnet_diagnostic.SYSLIB1092.severity = error - -# Analysis for COM interface generation has failed -dotnet_diagnostic.SYSLIB1093.severity = error - -# The base COM interface failed to generate source. Code will not be generated for this interface -dotnet_diagnostic.SYSLIB1094.severity = error - -# Invalid GeneratedComClassAttribute usage -dotnet_diagnostic.SYSLIB1095.severity = error - -# Use GeneratedComInterfaceAttribute instead of ComImportAttribute to generate COM marshalling code at compile time -dotnet_diagnostic.SYSLIB1096.severity = error - -# This type implements at least one type with the GeneratedComInterfaceAttribute attribute -dotnet_diagnostic.SYSLIB1097.severity = error - -# .NET COM hosting with EnableComHosting only supports built-in COM interop -dotnet_diagnostic.SYSLIB1098.severity = error - -# COM Interop APIs on System.Runtime.InteropServices.Marshal do not support source-generated COM and will fail at run time -dotnet_diagnostic.SYSLIB1099.severity = error - -# Type is not supported -dotnet_diagnostic.SYSLIB1100.severity = error - -# Property on type is not supported -dotnet_diagnostic.SYSLIB1101.severity = error - -# Project's language version must be at least C# 11 -dotnet_diagnostic.SYSLIB1102.severity = error - -# Value types are invalid inputs to configuration 'Bind' methods -dotnet_diagnostic.SYSLIB1103.severity = error - -# Generator cannot determine the target configuration type -dotnet_diagnostic.SYSLIB1104.severity = error - -# Can't use ValidateObjectMembersAttribute or ValidateEnumeratedItemsAttribute on fields or properties with open generic types -dotnet_diagnostic.SYSLIB1201.severity = error - -# A member type has no fields or properties to validate -dotnet_diagnostic.SYSLIB1202.severity = error - -# A type has no fields or properties to validate -dotnet_diagnostic.SYSLIB1203.severity = error - -# Deriving from a GeneratedComInterface-attributed interface defined in another assembly is not supported -dotnet_diagnostic.SYSLIB1230.severity = error - -# Other Platform Diagnostics - -# SVE is a preview feature can be used by enabling EnablePreviewFeatures flag -dotnet_diagnostic.SYSLIB5003.severity = error - -# CS9035 - should be warning or error -dotnet_diagnostic.CS9035.severity = none - -# For Verify package based content -[*.{received,verified}.{cs,txt}] -charset = "utf-8-bom" -end_of_line = lf -indent_size = unset -indent_style = unset -insert_final_newline = false -tab_width = unset -trim_trailing_whitespace = false - -# The following sections are about -# ignoring certain rules for specific files or folders, such as settings, models, DTOs, etc. -# including when using the Extensions/ folder for matching non-local namespace extensions such as -# - Extensions/System/Linq/EnumerableExtensions.cs -# - Extensions/Microsoft/Extensions/DepdendencyInjection/ServiceCollectionExtensions.cs - -# If it's a settings, model, dto, etc file, ignore the 'properties' cannot have arrays -# and other annoying rules - the following section is duplicated -[**/*{ValueObjects,Settings,Options,Model,Models,DTO,Entity,Response,Request}{.Nav,}.{cs,vb}] -dotnet_diagnostic.CA1002.severity = none -dotnet_diagnostic.CA1024.severity = none -dotnet_diagnostic.CA1056.severity = none -dotnet_diagnostic.CA1819.severity = none -dotnet_diagnostic.CA2227.severity = none -dotnet_diagnostic.CS8618.severity = none -dotnet_diagnostic.CS8767.severity = none - -[**/{ValueObjects,Migrations,Entities,Models,DTOs,Settings,Options,Configuration,Requests,Responses}/**.{cs,vb}] -dotnet_diagnostic.CA1002.severity = none -dotnet_diagnostic.CA1024.severity = none -dotnet_diagnostic.CA1056.severity = none -dotnet_diagnostic.CA1819.severity = none -dotnet_diagnostic.CA2227.severity = none -dotnet_diagnostic.CS8618.severity = none -dotnet_diagnostic.CS8767.severity = none - -[**/Migrations/**.{cs,vb}] -# EF Core migrations (and their .Designer.cs snapshots) are generated; any diagnostics are worthless. -generated_code = true -dotnet_diagnostic.CA1062.severity = none -dotnet_diagnostic.CA1825.severity = none -dotnet_diagnostic.CA1861.severity = none -dotnet_diagnostic.IDE0053.severity = none -dotnet_diagnostic.IDE0300.severity = none -dotnet_diagnostic.RCS1021.severity = none -dotnet_diagnostic.RCS1205.severity = none -# PDS0004: Use correct acronym capitalization -dotnet_diagnostic.PDS0004.severity = none - -[**/Extensions/**.{cs,vb}] -# Files under the Extensions/ namespace-reset convention must not force developers to add pragmas; -# suppress the namespace-conflict diagnostics that arise from declaring framework-style namespaces. -dotnet_diagnostic.IDE0130.severity = none -dotnet_diagnostic.CA1034.severity = none -dotnet_diagnostic.CA1724.severity = none -dotnet_diagnostic.CS0436.severity = none -dotnet_diagnostic.CS1591.severity = none -dotnet_diagnostic.IDE0005.severity = none - -[**/{Extension,Extensions}.{cs,vb}] -dotnet_diagnostic.CA1034.severity = none - -[**/Generated/**/*.{cs,vb}] -generated_code = true -dotnet_diagnostic.CS8602.severity = none - -[**/*.Designer.cs] -# Designer/design-time generated content -generated_code = true - -[**/obj/**/*.cs] -[**/bin/**/*.cs] -# Compiler/build output (generated AssemblyInfo, GlobalUsings, etc.) -generated_code = true - -# Repo convention: standard accessibility modifiers are always written explicitly -# (overrides the SDK default of omit_if_default, which is uncommon for .NET libraries). -[*.cs] -dotnet_style_require_accessibility_modifiers = always -dotnet_diagnostic.IDE0040.severity = none diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 4ddf908..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,14 +0,0 @@ - - - Purview - $(MSBuildThisFileDirectory)package.json - net11.0-windows10.0.19041.0 - x64 - 10.0.26100.80 - - true - true - - - - diff --git a/Justfile b/Justfile index 1dc4535..cc6e479 100644 --- a/Justfile +++ b/Justfile @@ -84,10 +84,14 @@ restore *args: dotnet restore {{ solution_file }} {{ args }} # Runs tests for the solution with the specified configuration (default: Debug) +# Test modules run serially by default: a WSLC session exclusively locks its image-store VHD, so parallel +# test assemblies contend for the shared store and fail with 0x80070020 ("file is being used by another +# process"). Override by appending the flag, e.g. +# just test '/*/*/*/*/' --max-parallel-test-modules 4 [group('Build and Test')] test filter="/*/*/*/*/" *args: echo "Running tests for {{ BLUE }}{{ test_solution }}{{ NORMAL }} with {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}..." - dotnet test --solution "{{ test_solution }}" --configuration "{{ build_configuration }}" --treenode-filter={{ filter }} {{ args }} + dotnet test --solution "{{ test_solution }}" --configuration "{{ build_configuration }}" --max-parallel-test-modules 1 --treenode-filter={{ filter }} {{ args }} # Cleans the solution with the specified configuration (default: Debug) [group('Build and Test')] diff --git a/README.md b/README.md index 2bbfe49..b7762a4 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,31 @@ string connectionString = azurite.GetConnectionString(); Uri blob = azurite.GetBlobEndpoint(); ``` +## Running the tests + +Every test process owns a single shared WSLC session and, by default, uses the shared image store +(`%LOCALAPPDATA%\Purview\WslContainers\images`). A WSLC session **exclusively locks its +`storage.vhdx`**, and the lock is taken lazily on the first store access — so running test assemblies +in parallel makes the losers fail with `0x80070020`: + +``` +The process cannot access the file because it is being used by another process. +``` + +The runtime now verifies the store on first use and transparently falls back to an isolated +per-process store (removed when that process's session terminates), but running test modules serially +keeps the warm shared image cache (no per-process re-pull) and is the fastest option: + +```powershell +just test # dotnet test, one test module at a time + +# Visual Studio: Test > Options > untick "Run Tests in Parallel" (or set +# "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 +> minutes because WSLC serialises container operations; expect slow-test warnings while it runs. + ## Running the Phase 0 spikes ```powershell diff --git a/docs/architecture.md b/docs/architecture.md index 3c34ccb..0623a3f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -49,8 +49,9 @@ Design rules: process runs. Set `StorageMode.PerSession` (or an explicit `StoragePath` / `WSL_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). When the default shared store is - contended by a concurrent process, the runtime automatically falls back to an isolated per-process store. + (a second session on the same path fails with `0x80070020` — EXP S18). The lock is taken lazily on the + first store access, so contention can surface on `GetImages()` rather than at session start; the runtime + detects it there too and falls back to an isolated per-process store. Sequential reuse works (EXP S2/S13): after a session ends, a new session on the same path sees its images. - Cleanup: normal shutdown terminates+disposes the session (frees the name, EXP S14). Orphaned sessions from crashed processes block only their own name (EXP S13); they do not block other sessions or storage-path reuse. @@ -72,9 +73,19 @@ Design rules: ## Concurrency - All session-mutating operations (`Pull`, `CreateContainer`, `Start`, `Stop`, `Delete`, `Tag`, `DeleteImage`, VHD ops) go through a per-session `SemaphoreSlim`. This avoids the racy `0x8000FFFF` observed with concurrent `Start`. -- Read-only ops (`Inspect`, `GetImages`, `Exec`?) are allowed concurrently once the container is running; `Exec` still funnels through the lock because it creates a process on the container. +- `GetImages()` reads (and locks) `storage.vhdx`, so it is **not** side-effect-free and goes through the same gate. Container-handle reads (`Inspect`) are allowed concurrently once the container is running; `Exec` also funnels through the lock because it creates a process on the container. - Bounded per-container output buffers prevent runaway memory. +### Shared-store contention (cross-process) + +`storage.vhdx` is opened lazily on the **first store access**, not at session start, so a second process +on the same path can start its session successfully and only fail later on its first read +(`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 +fallback. Isolated stores are transient and are removed when their session terminates. + ## Cleanup & reaper decision **No Ryuk-style sidecar process is needed.** @@ -87,6 +98,7 @@ Design rules: ## 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. +- 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. ## Extensibility diff --git a/docs/wslc-api-investigation.md b/docs/wslc-api-investigation.md index a6882e1..ed727a3 100644 --- a/docs/wslc-api-investigation.md +++ b/docs/wslc-api-investigation.md @@ -100,6 +100,7 @@ Enums (SRC): `ContainerState{Invalid=0,Created,Running,Exited,Deleted}` · `Proc - `Session.Dispose()` alone removes the session from the session manager and frees the name while the process is still alive. `EXP (S14)` - **If a process exits without disposing its session, the session is orphaned** — it remains in `wslc info` with a dead `Creator PID` and the name stays reserved. It does **not** block other session names, nor reuse of the same storage path. `EXP (S10, S13)` - `Container.Stop(signal, timeout)` is **idempotent** (stopping an already-stopped container succeeds). `EXP` +- `Container.Stop(signal, timeout)` can **block far longer than the `timeout` argument** (observed: minutes with `SIGTERM`/10 s while the session held several containers). Because the library serialises session operations, a single slow `Stop` stalls every other container operation in that process. `EXP` - `Container.Delete(...)` on an already-deleted container throws `COMException 0x80010108` (`RPC_E_DISCONNECTED`). `EXP` - `OpenContainer` on a missing container throws `COMException` with `HResult == (int)Error.ContainerNotFound` (`0x80040603`, i.e. `-2147219965`). `EXP` — matches docs. - `Delete(DeleteContainerOption.Force)` works on a running container (state → `Deleted`). `EXP` @@ -178,7 +179,7 @@ The library parses this for `GetMappedPublicPort` and container IP. ### A session exclusively locks its storage VHD (EXP, S18) -- Each session's image store lives in `{storagePath}\storage.vhdx`. A running session locks that VHD; a **concurrent** session on the same path fails reads with `0x80070020` (`ERROR_SHARING_VIOLATION`, "file is being used by another process"). +- Each session's image store lives in `{storagePath}\storage.vhdx`. A running session locks that VHD; a **concurrent** session on the same path fails reads with `0x80070020` (`ERROR_SHARING_VIOLATION`, "file is being used by another process"). The VHD is opened **lazily on the first store access** (e.g. the first `GetImages()` call), not at session creation or `Session.Start()`, so the violation can surface well after `Session.Start()` succeeded. `EXP` - **Sequential** reuse works: after a session ends, a new session on the same path sees the previous session's images (EXP S2/S13). There is no global/shared image cache in WSLC, so sharing images across sessions requires a shared storage path — safe only when sessions do not overlap. The library defaults to a shared store and falls back to an isolated per-process store on contention. ## Known gaps in the C# projection (DOC) diff --git a/global.json b/global.json index d68cc15..186257a 100644 --- a/global.json +++ b/global.json @@ -4,7 +4,7 @@ "allowPrerelease": true }, "msbuild-sdks": { - "Purview.BuildSdk": "1.0.0" + "Purview.BuildSdk": "1.0.2.0" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..c6a3b70 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,10 @@ + + + Purview.WslContainers + net11.0-windows10.0.19041.0 + x64 + 10.0.26100.80 + + + + diff --git a/Directory.Build.targets b/src/Directory.Build.targets similarity index 100% rename from Directory.Build.targets rename to src/Directory.Build.targets diff --git a/src/WSLTestContainers.slnx b/src/WSLTestContainers.slnx index 25962f4..10548b4 100644 --- a/src/WSLTestContainers.slnx +++ b/src/WSLTestContainers.slnx @@ -1,35 +1,44 @@ + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/WslContainers.MinIo/MinIoContainer.cs b/src/WslContainers.MinIo/MinIoContainer.cs deleted file mode 100644 index 84872cb..0000000 --- a/src/WslContainers.MinIo/MinIoContainer.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Purview.WslContainers.MinIo; - -/// A throwaway MinIO (S3-compatible object storage) instance running on WSL Containers. -public sealed class MinIoContainer : WslContainer -{ - private readonly MinIoConfiguration configuration; - - internal MinIoContainer(MinIoConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) - { - this.configuration = configuration; - } - - /// S3 API endpoint. Safe to call after . - public Uri GetApiEndpoint() - { - return new Uri($"http://127.0.0.1:{GetMappedPublicPort(MinIoBuilder.ApiPort)}"); - } - - /// Web console endpoint. - public Uri GetConsoleEndpoint() - { - return new Uri($"http://127.0.0.1:{GetMappedPublicPort(MinIoBuilder.ConsolePort)}"); - } - - /// Root access key. - public string GetAccessKey() => configuration.AccessKey; - - /// Root secret key. Handle as a secret. - public Secret GetSecretKey() => configuration.SecretKey; -} diff --git a/src/WslContainers/LogBuffer.cs b/src/WslContainers/LogBuffer.cs deleted file mode 100644 index 7e12e52..0000000 --- a/src/WslContainers/LogBuffer.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System.Text; -using System.Threading.Channels; -using Microsoft.WSL.Containers; - -namespace Purview.WslContainers; - -/// -/// Bounded buffer for init-process output. Accumulates decoded lines for diagnostics while publishing -/// every entry to a channel so wait strategies and consumers can tail live output. -/// -internal sealed class LogBuffer -{ - private const int MaxEntries = 4096; - private readonly object sync = new(); - private readonly List entries = new(MaxEntries); - private readonly Channel channel = Channel.CreateBounded( - new BoundedChannelOptions(1024) - { - FullMode = BoundedChannelFullMode.DropOldest, - SingleReader = true, - SingleWriter = false, - } - ); - private readonly Decoder stdoutDecoder = Encoding.UTF8.GetDecoder(); - private readonly Decoder stderrDecoder = Encoding.UTF8.GetDecoder(); - private readonly StringBuilder stdoutPending = new(); - private readonly StringBuilder stderrPending = new(); - private int attached; - - public void Attach(Process process) - { - if (Interlocked.Exchange(ref attached, 1) == 1) - { - return; - } - - process.OutputReceived += OnStdout; - process.ErrorReceived += OnStderr; - process.Exited += _ => FlushRemaining(); - } - - public IReadOnlyList Snapshot() - { - lock (sync) - { - return entries.ToArray(); - } - } - - public string GetLogs(LogOutput? stream) - { - lock (sync) - { - IEnumerable source = stream is LogOutput s - ? entries.Where(entry => entry.Stream == s) - : entries; - return string.Join('\n', source.Select(entry => entry.Text)); - } - } - - public ChannelReader Reader => channel.Reader; - - private void OnStdout(byte[] data) - { - Append(LogOutput.Stdout, stdoutDecoder, stdoutPending, data); - } - - private void OnStderr(byte[] data) - { - Append(LogOutput.Stderr, stderrDecoder, stderrPending, data); - } - - private void Append(LogOutput stream, Decoder decoder, StringBuilder pending, byte[] data) - { - if (data.Length == 0) - { - return; - } - - char[] chars = new char[decoder.GetCharCount(data, 0, data.Length, flush: false)]; - int count = decoder.GetChars(data, 0, data.Length, chars, 0, flush: false); - lock (sync) - { - pending.Append(chars, 0, count); - FlushLines(stream, pending); - } - } - - private void FlushLines(LogOutput stream, StringBuilder pending) - { - string text = pending.ToString(); - pending.Clear(); - int start = 0; - for (int i = 0; i < text.Length; i++) - { - if (text[i] == '\n') - { - Add(new ContainerLogEntry(DateTimeOffset.UtcNow, stream, text[start..i].TrimEnd('\r'))); - start = i + 1; - } - } - - if (start < text.Length) - { - pending.Append(text[start..]); - } - } - - private void FlushRemaining() - { - lock (sync) - { - if (stdoutPending.Length > 0) - { - Add(new ContainerLogEntry(DateTimeOffset.UtcNow, LogOutput.Stdout, stdoutPending.ToString())); - stdoutPending.Clear(); - } - - if (stderrPending.Length > 0) - { - Add(new ContainerLogEntry(DateTimeOffset.UtcNow, LogOutput.Stderr, stderrPending.ToString())); - stderrPending.Clear(); - } - } - } - - private void Add(ContainerLogEntry entry) - { - entries.Add(entry); - while (entries.Count > MaxEntries) - { - entries.RemoveAt(0); - } - - channel.Writer.TryWrite(entry); - } -} diff --git a/src/WslContainers/WslContainerRuntime.cs b/src/WslContainers/WslContainerRuntime.cs deleted file mode 100644 index 27f99d7..0000000 --- a/src/WslContainers/WslContainerRuntime.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.WSL.Containers; - -namespace Purview.WslContainers; - -/// -/// The process-wide WSL Containers runtime. Owns a single lazily started WSLC session. -/// Images are shared by default: sessions use a stable shared storage directory so the image store is -/// pulled once and reused across process runs. When a concurrent process holds that store, the runtime -/// falls back to an isolated per-process store automatically. -/// -[SuppressMessage( - "Design", - "CA2213:Disposable fields should be disposed", - Justification = "The session is released asynchronously in DisposeAsync/TerminateAsync; CA2213 cannot see the async path." -)] -public sealed class WslContainerRuntime : IContainerRuntime -{ - private const int ErrorSharingViolation = unchecked((int)0x80070020); - - private static readonly Lazy InstanceHolder = new(() => new WslContainerRuntime()); - - private readonly WslContainerRuntimeOptions options; - private readonly object sync = new(); - private WslContainerSession? session; - private int disposed; - - /// Creates a runtime with default options. Registers a process-exit cleanup hook. - public WslContainerRuntime() - : this(WslContainerRuntimeOptions.Default) { } - - /// Creates a runtime with the given options. - public WslContainerRuntime(WslContainerRuntimeOptions options) - { - this.options = options ?? WslContainerRuntimeOptions.Default; - if (!this.options.DisableProcessExitCleanup) - { - AppDomain.CurrentDomain.ProcessExit += OnProcessExit; - } - } - - /// The default process-wide runtime. - public static WslContainerRuntime Instance => InstanceHolder.Value; - - /// - public async Task GetInfoAsync(CancellationToken cancellationToken = default) - { - await Task.Yield(); - ServiceVersion version = WslcService.GetVersion(); - IReadOnlyList missing = WslcService.GetMissingComponents(); - bool compatible = !missing.Contains(Component.SdkNeedsUpdate); - return new WslContainerRuntimeInfo( - $"{version.Major}.{version.Minor}.{version.Revision}", - missing.Select(component => component.ToString()).ToArray(), - missing.Count == 0, - compatible - ); - } - - /// - public async Task GetSessionAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(Volatile.Read(ref disposed) == 1, this); - WslContainerSession local = GetOrCreateSession(); - try - { - await local.EnsureStartedAsync(cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) when (IsSharingViolation(ex) && IsUsingDefaultSharedStore()) - { - // A concurrent process holds the shared image-store VHD. Isolate this process instead of failing. - local.DisposeSession(); - local = CreateIsolatedSession(); - session = local; - await local.EnsureStartedAsync(cancellationToken).ConfigureAwait(false); - } - - return local; - } - - /// - public async ValueTask DisposeAsync() - { - if (Interlocked.Exchange(ref disposed, 1) == 1) - { - return; - } - - WslContainerSession? local = session; - if (local is not null) - { - await local.TerminateAsync().ConfigureAwait(false); - } - - GC.SuppressFinalize(this); - } - - private WslContainerSession GetOrCreateSession() - { - WslContainerSession? local = session; - if (local is not null) - { - return local; - } - - lock (sync) - { - local = session; - if (local is null) - { - local = CreateSession(); - session = local; - } - - return local; - } - } - - private WslContainerSession CreateSession() - { - string name = NextSessionName(); - string storagePath = ResolveStoragePath(name); - return new WslContainerSession(name, storagePath, options); - } - - private WslContainerSession CreateIsolatedSession() - { - string name = NextSessionName(); - string storagePath = Path.Combine(PerSessionRoot, name); - return new WslContainerSession(name, storagePath, options); - } - - private string ResolveStoragePath(string sessionName) - { - if (options.StoragePath is not null) - { - return options.StoragePath; - } - - string? envPath = Environment.GetEnvironmentVariable("WSL_CONTAINERS_STORAGE_PATH"); - if (!string.IsNullOrEmpty(envPath)) - { - return envPath; - } - - if (options.StorageMode == StorageMode.PerSession) - { - return Path.Combine(PerSessionRoot, sessionName); - } - - return SharedImagesRoot; - } - - private bool IsUsingDefaultSharedStore() - { - return options.StoragePath is null - && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_CONTAINERS_STORAGE_PATH")) - && options.StorageMode == StorageMode.Shared; - } - - private static string NextSessionName() - { - return $"wslc-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..8]}"; - } - - private static string PerSessionRoot => Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "Purview", - "WslContainers", - "sessions" - ); - - private static string SharedImagesRoot => Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "Purview", - "WslContainers", - "images" - ); - - private static bool IsSharingViolation(Exception exception) - { - return exception.HResult == ErrorSharingViolation; - } - - private void OnProcessExit(object? sender, EventArgs e) - { - WslContainerSession? local = session; - local?.TerminateBestEffort(); - } -} \ No newline at end of file diff --git a/src/WslContainers/WslInspectParser.cs b/src/WslContainers/WslInspectParser.cs deleted file mode 100644 index 559211f..0000000 --- a/src/WslContainers/WslInspectParser.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Text.Json; - -namespace Purview.WslContainers; - -/// Parses the Docker-compatible JSON returned by Container.Inspect(). -internal static class WslInspectParser -{ - public static IReadOnlyDictionary ParseHostPorts(string inspectJson) - { - Dictionary result = new(); - try - { - using JsonDocument document = JsonDocument.Parse(inspectJson); - if ( - !document.RootElement.TryGetProperty("Ports", out JsonElement ports) - || ports.ValueKind != JsonValueKind.Object - ) - { - return result; - } - - foreach (JsonProperty property in ports.EnumerateObject()) - { - // Key format: "8080/tcp" - string key = property.Name; - int slash = key.IndexOf('/', StringComparison.Ordinal); - if ( - slash <= 0 - || !ushort.TryParse(key.AsSpan(0, slash), out ushort containerPort) - || property.Value.ValueKind != JsonValueKind.Array - ) - { - continue; - } - - foreach (JsonElement binding in property.Value.EnumerateArray()) - { - if ( - binding.TryGetProperty("HostPort", out JsonElement hostPort) - && hostPort.ValueKind == JsonValueKind.String - && ushort.TryParse(hostPort.GetString(), out ushort host) - ) - { - result[containerPort] = host; - break; - } - } - } - } - catch (JsonException) { } - - return result; - } - - public static string? TryGetNetworkIp(string inspectJson, string network = "bridge") - { - try - { - using JsonDocument document = JsonDocument.Parse(inspectJson); - if ( - document.RootElement.TryGetProperty("NetworkSettings", out JsonElement networkSettings) - && networkSettings.TryGetProperty("Networks", out JsonElement networks) - && networks.TryGetProperty(network, out JsonElement net) - && net.TryGetProperty("IPAddress", out JsonElement ip) - && ip.ValueKind == JsonValueKind.String - ) - { - string value = ip.GetString() ?? string.Empty; - return string.IsNullOrEmpty(value) ? null : value; - } - - return null; - } - catch (JsonException) - { - return null; - } - } -} diff --git a/src/WslContainers.Azurite/WslContainers.Azurite.csproj b/src/src/Azurite/Azurite.csproj similarity index 100% rename from src/WslContainers.Azurite/WslContainers.Azurite.csproj rename to src/src/Azurite/Azurite.csproj diff --git a/src/WslContainers.Azurite/AzuriteAccount.cs b/src/src/Azurite/AzuriteAccount.cs similarity index 100% rename from src/WslContainers.Azurite/AzuriteAccount.cs rename to src/src/Azurite/AzuriteAccount.cs diff --git a/src/WslContainers.Azurite/AzuriteBuilder.cs b/src/src/Azurite/AzuriteBuilder.cs similarity index 90% rename from src/WslContainers.Azurite/AzuriteBuilder.cs rename to src/src/Azurite/AzuriteBuilder.cs index 0f18999..22554d5 100644 --- a/src/WslContainers.Azurite/AzuriteBuilder.cs +++ b/src/src/Azurite/AzuriteBuilder.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Waiting; + namespace Purview.WslContainers.Azurite; /// Fluent builder for an Azurite (Azure Storage emulator) test container. @@ -46,8 +48,8 @@ public AzuriteBuilder(IContainerRuntime runtime) /// protected override AzuriteConfiguration BuildConfiguration() { - AzuriteConfiguration configuration = base.BuildConfiguration(); - IReadOnlyList waitStrategies = + var configuration = base.BuildConfiguration(); + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { (IWaitStrategy)Wait.ForLogMessage("successfully listening") }; @@ -55,8 +57,5 @@ protected override AzuriteConfiguration BuildConfiguration() } /// - protected override AzuriteContainer CreateContainer(AzuriteConfiguration configuration) - { - return new AzuriteContainer(configuration, Runtime ?? WslContainerRuntime.Instance); - } + protected override AzuriteContainer CreateContainer(AzuriteConfiguration configuration) => new(configuration, Runtime ?? WslContainerRuntime.Instance); } diff --git a/src/WslContainers.Azurite/AzuriteConfiguration.cs b/src/src/Azurite/AzuriteConfiguration.cs similarity index 100% rename from src/WslContainers.Azurite/AzuriteConfiguration.cs rename to src/src/Azurite/AzuriteConfiguration.cs diff --git a/src/WslContainers.Azurite/AzuriteContainer.cs b/src/src/Azurite/AzuriteContainer.cs similarity index 97% rename from src/WslContainers.Azurite/AzuriteContainer.cs rename to src/src/Azurite/AzuriteContainer.cs index 4a6c865..973d5de 100644 --- a/src/WslContainers.Azurite/AzuriteContainer.cs +++ b/src/src/Azurite/AzuriteContainer.cs @@ -31,7 +31,7 @@ public Uri GetTableEndpoint() /// (a placeholder until the consuming repository supplies it). public string GetConnectionString() { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.Append("DefaultEndpointsProtocol=http;"); builder.Append(CultureInfo.InvariantCulture, $"AccountName={AzuriteAccount.Name};"); builder.Append(CultureInfo.InvariantCulture, $"AccountKey={AzuriteAccount.Key};"); diff --git a/src/WslContainers.MinIo/WslContainers.MinIo.csproj b/src/src/MinIo/MinIO.csproj similarity index 100% rename from src/WslContainers.MinIo/WslContainers.MinIo.csproj rename to src/src/MinIo/MinIO.csproj diff --git a/src/WslContainers.MinIo/MinIoBuilder.cs b/src/src/MinIo/MinIOBuilder.cs similarity index 55% rename from src/WslContainers.MinIo/MinIoBuilder.cs rename to src/src/MinIo/MinIOBuilder.cs index e0aeaba..0d0ffd6 100644 --- a/src/WslContainers.MinIo/MinIoBuilder.cs +++ b/src/src/MinIo/MinIOBuilder.cs @@ -1,82 +1,87 @@ -namespace Purview.WslContainers.MinIo; +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; + +namespace Purview.WslContainers.MinIO; /// Fluent builder for a MinIO (S3-compatible object storage) test container. -public class MinIoBuilder : ContainerBuilder +public class MinIOBuilder : ContainerBuilder { /// Default S3 API port. - public const ushort ApiPort = 9000; + public const ushort APIPort = 9000; /// Default console port. public const ushort ConsolePort = 9001; /// Default image (MinIO's official registry is Quay.io). - public const string MinIoImage = "quay.io/minio/minio:latest"; + public const string MinIOImage = "quay.io/minio/minio:latest"; + + string _accessKey = "minioadmin"; - private string accessKey = "minioadmin"; - private Secret secretKey = Secret.From("minioadmin"); + Secret _secretKey = Secret.From("minioadmin"); /// Creates a builder with the default image. - public MinIoBuilder() - : this(MinIoImage) { } + public MinIOBuilder() + : this(MinIOImage) { } /// Creates a builder with a custom image. - public MinIoBuilder(string image) + public MinIOBuilder(string image) { WithImage(image) .WithCommand("minio", "server", "/data", "--console-address", ":9001") - .WithPortBinding(ApiPort, assignRandomHostPort: true) + .WithPortBinding(APIPort, assignRandomHostPort: true) .WithPortBinding(ConsolePort, assignRandomHostPort: true); } /// Creates a builder using an explicit runtime. - public MinIoBuilder(IContainerRuntime runtime) + public MinIOBuilder(IContainerRuntime runtime) : base(runtime) { - WithImage(MinIoImage) + WithImage(MinIOImage) .WithCommand("minio", "server", "/data", "--console-address", ":9001") - .WithPortBinding(ApiPort, assignRandomHostPort: true) + .WithPortBinding(APIPort, assignRandomHostPort: true) .WithPortBinding(ConsolePort, assignRandomHostPort: true); } /// Sets the root access key. - public MinIoBuilder WithAccessKey(string accessKey) + public MinIOBuilder WithAccessKey(string accessKey) { - this.accessKey = accessKey; + _accessKey = accessKey; WithEnvironment("MINIO_ROOT_USER", accessKey); return this; } /// Sets the root secret key. - public MinIoBuilder WithSecretKey(string secretKey) + public MinIOBuilder WithSecretKey(string secretKey) { - this.secretKey = Secret.From(secretKey); + _secretKey = Secret.From(secretKey); WithEnvironment("MINIO_ROOT_PASSWORD", secretKey); return this; } /// Builds the immutable configuration (internal; used by the module's own tests). - internal MinIoConfiguration BuildConfigurationForTesting() => BuildConfiguration(); + internal MinIOConfiguration BuildConfigurationForTesting() => BuildConfiguration(); /// - protected override MinIoConfiguration BuildConfiguration() + protected override MinIOConfiguration BuildConfiguration() { - MinIoConfiguration configuration = base.BuildConfiguration(); - Dictionary environment = new Dictionary( + var configuration = base.BuildConfiguration(); + Dictionary environment = new( configuration.Environment, StringComparer.Ordinal ) { - ["MINIO_ROOT_USER"] = accessKey, - ["MINIO_ROOT_PASSWORD"] = secretKey.Value, + ["MINIO_ROOT_USER"] = _accessKey, + ["MINIO_ROOT_PASSWORD"] = _secretKey.Value, }; - IReadOnlyList waitStrategies = + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { (IWaitStrategy)Wait.ForLogMessage("Console:") }; return configuration with { - AccessKey = accessKey, - SecretKey = secretKey, + AccessKey = _accessKey, + SecretKey = _secretKey, Environment = environment, WaitStrategies = waitStrategies, }; @@ -86,15 +91,15 @@ protected override MinIoConfiguration BuildConfiguration() protected override void Validate(ContainerConfiguration configuration) { base.Validate(configuration); - if (string.IsNullOrWhiteSpace(accessKey) || string.IsNullOrEmpty(secretKey.Value)) + if (string.IsNullOrWhiteSpace(_accessKey) || string.IsNullOrEmpty(_secretKey.Value)) { throw new WslContainerConfigurationException("MinIO access key and secret key cannot be empty."); } } /// - protected override MinIoContainer CreateContainer(MinIoConfiguration configuration) + protected override MinIOContainer CreateContainer(MinIOConfiguration configuration) { - return new MinIoContainer(configuration, Runtime ?? WslContainerRuntime.Instance); + return new MinIOContainer(configuration, Runtime ?? WslContainerRuntime.Instance); } } diff --git a/src/WslContainers.MinIo/MinIoConfiguration.cs b/src/src/MinIo/MinIOConfiguration.cs similarity index 70% rename from src/WslContainers.MinIo/MinIoConfiguration.cs rename to src/src/MinIo/MinIOConfiguration.cs index 6f42dca..1cf623f 100644 --- a/src/WslContainers.MinIo/MinIoConfiguration.cs +++ b/src/src/MinIo/MinIOConfiguration.cs @@ -1,7 +1,9 @@ -namespace Purview.WslContainers.MinIo; +using Purview.WslContainers.Diagnostics; + +namespace Purview.WslContainers.MinIO; /// Immutable configuration for a MinIO test container. -public sealed record MinIoConfiguration : ContainerConfiguration +public sealed record MinIOConfiguration : ContainerConfiguration { /// Root access key (MINIO_ROOT_USER). public string AccessKey { get; init; } = "minioadmin"; diff --git a/src/src/MinIo/MinIOContainer.cs b/src/src/MinIo/MinIOContainer.cs new file mode 100644 index 0000000..848c559 --- /dev/null +++ b/src/src/MinIo/MinIOContainer.cs @@ -0,0 +1,35 @@ +using Purview.WslContainers.Diagnostics; + +namespace Purview.WslContainers.MinIO; + +/// A throwaway MinIO (S3-compatible object storage) instance running on WSL Containers. +public sealed class MinIOContainer : WslContainer +{ + readonly MinIOConfiguration _configuration; + + internal MinIOContainer(MinIOConfiguration configuration, IContainerRuntime runtime) + : base(configuration, runtime) + { + _configuration = configuration; + } + + /// S3 API endpoint. Safe to call after . + public Uri GetAPIEndpoint() + { + return new Uri($"http://127.0.0.1:{GetMappedPublicPort(MinIOBuilder.APIPort)}"); + } + + /// Web console endpoint. + public Uri GetConsoleEndpoint() + { + return new Uri($"http://127.0.0.1:{GetMappedPublicPort(MinIOBuilder.ConsolePort)}"); + } + + /// Root access key. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate")] + public string GetAccessKey() => _configuration.AccessKey; + + /// Root secret key. Handle as a secret. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate")] + public Secret GetSecretKey() => _configuration.SecretKey; +} diff --git a/src/WslContainers.MsSql/WslContainers.MsSql.csproj b/src/src/MsSql/MsSql.csproj similarity index 100% rename from src/WslContainers.MsSql/WslContainers.MsSql.csproj rename to src/src/MsSql/MsSql.csproj diff --git a/src/WslContainers.MsSql/MsSqlBuilder.cs b/src/src/MsSql/MsSqlBuilder.cs similarity index 81% rename from src/WslContainers.MsSql/MsSqlBuilder.cs rename to src/src/MsSql/MsSqlBuilder.cs index 3a9bec3..d2f0269 100644 --- a/src/WslContainers.MsSql/MsSqlBuilder.cs +++ b/src/src/MsSql/MsSqlBuilder.cs @@ -1,4 +1,7 @@ using Microsoft.Data.SqlClient; +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; namespace Purview.WslContainers.MsSql; @@ -11,8 +14,8 @@ public class MsSqlBuilder : ContainerBuilderDefault image. public const string MsSqlImage = "mcr.microsoft.com/mssql/server:2022-latest"; - private Secret password = Secret.From("YourStrong!Passw0rd"); - private bool acceptLicense; + Secret _password = Secret.From("YourStrong!Passw0rd"); + bool _acceptLicense; /// Creates a builder with the default image. public MsSqlBuilder() @@ -34,7 +37,7 @@ public MsSqlBuilder(IContainerRuntime runtime) /// Sets the SA password. Must satisfy SQL Server password complexity (at least 8 characters). public MsSqlBuilder WithPassword(string password) { - this.password = Secret.From(password); + _password = Secret.From(password); WithEnvironment("MSSQL_SA_PASSWORD", password); return this; } @@ -45,7 +48,7 @@ public MsSqlBuilder WithPassword(string password) /// public MsSqlBuilder AcceptLicense() { - acceptLicense = true; + _acceptLicense = true; WithEnvironment("ACCEPT_EULA", "Y"); return this; } @@ -56,26 +59,26 @@ public MsSqlBuilder AcceptLicense() /// protected override MsSqlConfiguration BuildConfiguration() { - MsSqlConfiguration configuration = base.BuildConfiguration(); - Dictionary environment = new Dictionary( + var configuration = base.BuildConfiguration(); + Dictionary environment = new( configuration.Environment, StringComparer.Ordinal ) { - ["MSSQL_SA_PASSWORD"] = password.Value, + ["MSSQL_SA_PASSWORD"] = _password.Value, ["MSSQL_PID"] = "Developer", }; - if (acceptLicense) + if (_acceptLicense) { environment["ACCEPT_EULA"] = "Y"; } - IReadOnlyList waitStrategies = + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { BuildReadinessWait() }; return configuration with { - Password = password, - AcceptLicense = acceptLicense, + Password = _password, + AcceptLicense = _acceptLicense, Environment = environment, WaitStrategies = waitStrategies, }; @@ -85,14 +88,14 @@ protected override MsSqlConfiguration BuildConfiguration() protected override void Validate(ContainerConfiguration configuration) { base.Validate(configuration); - if (!acceptLicense) + if (!_acceptLicense) { throw new WslContainerConfigurationException( "The SQL Server image requires accepting the EULA. Call AcceptLicense() explicitly before Build()." ); } - if (password.Value.Length < 8) + if (_password.Value.Length < 8) { throw new WslContainerConfigurationException( "The SQL Server SA password must be at least 8 characters long." @@ -106,30 +109,30 @@ protected override MsSqlContainer CreateContainer(MsSqlConfiguration configurati return new MsSqlContainer(configuration, Runtime ?? WslContainerRuntime.Instance); } - private IWaitStrategy BuildReadinessWait() + WaitStrategy BuildReadinessWait() { // SQL Server is ready only when it accepts a real connection; a listening TCP port is not enough. return Wait.ForCustom( async (context, cancellationToken) => { - int? hostPort = context.GetHostPort(MsSqlPort); + var hostPort = context.GetHostPort(MsSqlPort); if (hostPort is null) { return false; } - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder + SqlConnectionStringBuilder builder = new() { // 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}", UserID = "sa", - Password = password.Value, + Password = _password.Value, TrustServerCertificate = true, }; try { - await using SqlConnection connection = new SqlConnection(builder.ConnectionString); + await using SqlConnection connection = new(builder.ConnectionString); await connection.OpenAsync(cancellationToken); return connection.State == System.Data.ConnectionState.Open; } diff --git a/src/WslContainers.MsSql/MsSqlConfiguration.cs b/src/src/MsSql/MsSqlConfiguration.cs similarity index 91% rename from src/WslContainers.MsSql/MsSqlConfiguration.cs rename to src/src/MsSql/MsSqlConfiguration.cs index 4c76385..de960c4 100644 --- a/src/WslContainers.MsSql/MsSqlConfiguration.cs +++ b/src/src/MsSql/MsSqlConfiguration.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Diagnostics; + namespace Purview.WslContainers.MsSql; /// Immutable configuration for a Microsoft SQL Server test container. diff --git a/src/WslContainers.MsSql/MsSqlContainer.cs b/src/src/MsSql/MsSqlContainer.cs similarity index 80% rename from src/WslContainers.MsSql/MsSqlContainer.cs rename to src/src/MsSql/MsSqlContainer.cs index c96f0c6..eacce1e 100644 --- a/src/WslContainers.MsSql/MsSqlContainer.cs +++ b/src/src/MsSql/MsSqlContainer.cs @@ -5,24 +5,24 @@ namespace Purview.WslContainers.MsSql; /// A throwaway Microsoft SQL Server instance running on WSL Containers. public sealed class MsSqlContainer : WslContainer { - private readonly MsSqlConfiguration configuration; + readonly MsSqlConfiguration _configuration; internal MsSqlContainer(MsSqlConfiguration configuration, IContainerRuntime runtime) : base(configuration, runtime) { - this.configuration = configuration; + _configuration = configuration; } /// Connection string pointing at the mapped host port. Safe to call after . public string GetConnectionString() { - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder + SqlConnectionStringBuilder builder = new() { // 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)}", UserID = "sa", - Password = configuration.Password.Value, + Password = _configuration.Password.Value, TrustServerCertificate = true, }; return builder.ConnectionString; diff --git a/src/WslContainers.MySql/WslContainers.MySql.csproj b/src/src/MySql/MySql.csproj similarity index 100% rename from src/WslContainers.MySql/WslContainers.MySql.csproj rename to src/src/MySql/MySql.csproj diff --git a/src/WslContainers.MySql/MySqlBuilder.cs b/src/src/MySql/MySqlBuilder.cs similarity index 73% rename from src/WslContainers.MySql/MySqlBuilder.cs rename to src/src/MySql/MySqlBuilder.cs index c902847..05cc758 100644 --- a/src/WslContainers.MySql/MySqlBuilder.cs +++ b/src/src/MySql/MySqlBuilder.cs @@ -1,4 +1,7 @@ using MySqlConnector; +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; namespace Purview.WslContainers.MySql; @@ -11,10 +14,10 @@ public class MySqlBuilder : ContainerBuilderDefault image. public const string MySqlImage = "mysql:8"; - private string database = "test"; - private string username = "test"; - private Secret password = Secret.From("test"); - private Secret rootPassword = Secret.From("test"); + string _database = "test"; + string _username = "test"; + Secret _password = Secret.From("test"); + Secret _rootPassword = Secret.From("test"); /// Creates a builder with the default image. public MySqlBuilder() @@ -36,7 +39,7 @@ public MySqlBuilder(IContainerRuntime runtime) /// Sets the database name. public MySqlBuilder WithDatabase(string database) { - this.database = database; + _database = database; WithEnvironment("MYSQL_DATABASE", database); return this; } @@ -44,7 +47,7 @@ public MySqlBuilder WithDatabase(string database) /// Sets the application user name. public MySqlBuilder WithUsername(string username) { - this.username = username; + _username = username; WithEnvironment("MYSQL_USER", username); return this; } @@ -52,7 +55,7 @@ public MySqlBuilder WithUsername(string username) /// Sets the application user password. public MySqlBuilder WithPassword(string password) { - this.password = Secret.From(password); + _password = Secret.From(password); WithEnvironment("MYSQL_PASSWORD", password); return this; } @@ -60,7 +63,7 @@ public MySqlBuilder WithPassword(string password) /// Sets the root password. public MySqlBuilder WithRootPassword(string rootPassword) { - this.rootPassword = Secret.From(rootPassword); + _rootPassword = Secret.From(rootPassword); WithEnvironment("MYSQL_ROOT_PASSWORD", rootPassword); return this; } @@ -71,25 +74,25 @@ public MySqlBuilder WithRootPassword(string rootPassword) /// protected override MySqlConfiguration BuildConfiguration() { - MySqlConfiguration configuration = base.BuildConfiguration(); - Dictionary environment = new Dictionary( + var configuration = base.BuildConfiguration(); + Dictionary environment = new( configuration.Environment, StringComparer.Ordinal ) { - ["MYSQL_DATABASE"] = database, - ["MYSQL_USER"] = username, - ["MYSQL_PASSWORD"] = password.Value, - ["MYSQL_ROOT_PASSWORD"] = rootPassword.Value, + ["MYSQL_DATABASE"] = _database, + ["MYSQL_USER"] = _username, + ["MYSQL_PASSWORD"] = _password.Value, + ["MYSQL_ROOT_PASSWORD"] = _rootPassword.Value, }; - IReadOnlyList waitStrategies = + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { BuildReadinessWait() }; return configuration with { - Database = database, - Username = username, - Password = password, - RootPassword = rootPassword, + Database = _database, + Username = _username, + Password = _password, + RootPassword = _rootPassword, Environment = environment, WaitStrategies = waitStrategies, }; @@ -99,12 +102,12 @@ protected override MySqlConfiguration BuildConfiguration() protected override void Validate(ContainerConfiguration configuration) { base.Validate(configuration); - if (string.IsNullOrWhiteSpace(database)) + if (string.IsNullOrWhiteSpace(_database)) { throw new WslContainerConfigurationException("MySQL database cannot be empty."); } - if (string.IsNullOrEmpty(password.Value)) + if (string.IsNullOrEmpty(_password.Value)) { throw new WslContainerConfigurationException("MySQL password cannot be empty."); } @@ -116,30 +119,30 @@ protected override MySqlContainer CreateContainer(MySqlConfiguration configurati return new MySqlContainer(configuration, Runtime ?? WslContainerRuntime.Instance); } - private IWaitStrategy BuildReadinessWait() + WaitStrategy BuildReadinessWait() { // The image logs "ready for connections" during its temporary init server, so the readiness // check must be a real host-side connection rather than a log match. return Wait.ForCustom( async (context, cancellationToken) => { - int? hostPort = context.GetHostPort(MySqlPort); + var hostPort = context.GetHostPort(MySqlPort); if (hostPort is null) { return false; } - MySqlConnectionStringBuilder builder = new MySqlConnectionStringBuilder + MySqlConnectionStringBuilder builder = new() { Server = "127.0.0.1", Port = (uint)hostPort.Value, - Database = database, - UserID = username, - Password = password.Value, + Database = _database, + UserID = _username, + Password = _password.Value, }; try { - await using MySqlConnection connection = new MySqlConnection(builder.ConnectionString); + await using MySqlConnection connection = new(builder.ConnectionString); await connection.OpenAsync(cancellationToken); return connection.State == System.Data.ConnectionState.Open; } diff --git a/src/WslContainers.MySql/MySqlConfiguration.cs b/src/src/MySql/MySqlConfiguration.cs similarity index 94% rename from src/WslContainers.MySql/MySqlConfiguration.cs rename to src/src/MySql/MySqlConfiguration.cs index 03813a8..299841f 100644 --- a/src/WslContainers.MySql/MySqlConfiguration.cs +++ b/src/src/MySql/MySqlConfiguration.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Diagnostics; + namespace Purview.WslContainers.MySql; /// Immutable configuration for a MySQL test container. diff --git a/src/WslContainers.MySql/MySqlContainer.cs b/src/src/MySql/MySqlContainer.cs similarity index 68% rename from src/WslContainers.MySql/MySqlContainer.cs rename to src/src/MySql/MySqlContainer.cs index 0933d19..8d78fb5 100644 --- a/src/WslContainers.MySql/MySqlContainer.cs +++ b/src/src/MySql/MySqlContainer.cs @@ -5,24 +5,24 @@ namespace Purview.WslContainers.MySql; /// A throwaway MySQL database running on WSL Containers. public sealed class MySqlContainer : WslContainer { - private readonly MySqlConfiguration configuration; + readonly MySqlConfiguration _configuration; internal MySqlContainer(MySqlConfiguration configuration, IContainerRuntime runtime) : base(configuration, runtime) { - this.configuration = configuration; + _configuration = configuration; } /// Connection string pointing at the mapped host port. Safe to call after . public string GetConnectionString() { - MySqlConnectionStringBuilder builder = new MySqlConnectionStringBuilder + MySqlConnectionStringBuilder builder = new() { Server = "127.0.0.1", Port = GetMappedPublicPort(MySqlBuilder.MySqlPort), - Database = configuration.Database, - UserID = configuration.Username, - Password = configuration.Password.Value, + Database = _configuration.Database, + UserID = _configuration.Username, + Password = _configuration.Password.Value, }; return builder.ConnectionString; } diff --git a/src/WslContainers.Nats/WslContainers.Nats.csproj b/src/src/Nats/Nats.csproj similarity index 100% rename from src/WslContainers.Nats/WslContainers.Nats.csproj rename to src/src/Nats/Nats.csproj diff --git a/src/WslContainers.Nats/NatsBuilder.cs b/src/src/Nats/NatsBuilder.cs similarity index 94% rename from src/WslContainers.Nats/NatsBuilder.cs rename to src/src/Nats/NatsBuilder.cs index 9d6457d..52d5872 100644 --- a/src/WslContainers.Nats/NatsBuilder.cs +++ b/src/src/Nats/NatsBuilder.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Waiting; + namespace Purview.WslContainers.Nats; /// Fluent builder for a NATS test container. @@ -39,8 +41,8 @@ public NatsBuilder(IContainerRuntime runtime) /// protected override NatsConfiguration BuildConfiguration() { - NatsConfiguration configuration = base.BuildConfiguration(); - IReadOnlyList waitStrategies = + var configuration = base.BuildConfiguration(); + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { (IWaitStrategy)Wait.ForLogMessage("Listening for client connections") }; diff --git a/src/WslContainers.Nats/NatsConfiguration.cs b/src/src/Nats/NatsConfiguration.cs similarity index 100% rename from src/WslContainers.Nats/NatsConfiguration.cs rename to src/src/Nats/NatsConfiguration.cs diff --git a/src/WslContainers.Nats/NatsContainer.cs b/src/src/Nats/NatsContainer.cs similarity index 100% rename from src/WslContainers.Nats/NatsContainer.cs rename to src/src/Nats/NatsContainer.cs diff --git a/src/WslContainers.PostgreSql/WslContainers.PostgreSql.csproj b/src/src/PostgreSql/PostgreSql.csproj similarity index 100% rename from src/WslContainers.PostgreSql/WslContainers.PostgreSql.csproj rename to src/src/PostgreSql/PostgreSql.csproj diff --git a/src/WslContainers.PostgreSql/PostgreSqlBuilder.cs b/src/src/PostgreSql/PostgreSqlBuilder.cs similarity index 77% rename from src/WslContainers.PostgreSql/PostgreSqlBuilder.cs rename to src/src/PostgreSql/PostgreSqlBuilder.cs index afdf4fb..456f1bc 100644 --- a/src/WslContainers.PostgreSql/PostgreSqlBuilder.cs +++ b/src/src/PostgreSql/PostgreSqlBuilder.cs @@ -1,3 +1,7 @@ +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; + namespace Purview.WslContainers.PostgreSql; /// Fluent builder for a PostgreSQL test container. @@ -9,9 +13,9 @@ public class PostgreSqlBuilder : ContainerBuilderDefault image. public const string PostgreSqlImage = "postgres:17"; - private string database = "postgres"; - private string username = "postgres"; - private Secret password = Secret.From("postgres"); + string _database = "postgres"; + string _username = "postgres"; + Secret _password = Secret.From("postgres"); /// Creates a builder with the default image. public PostgreSqlBuilder() @@ -33,7 +37,7 @@ public PostgreSqlBuilder(IContainerRuntime runtime) /// Sets the database name. public PostgreSqlBuilder WithDatabase(string database) { - this.database = database; + _database = database; WithEnvironment("POSTGRES_DB", database); return this; } @@ -41,7 +45,7 @@ public PostgreSqlBuilder WithDatabase(string database) /// Sets the superuser name. public PostgreSqlBuilder WithUsername(string username) { - this.username = username; + _username = username; WithEnvironment("POSTGRES_USER", username); return this; } @@ -49,7 +53,7 @@ public PostgreSqlBuilder WithUsername(string username) /// Sets the superuser password. public PostgreSqlBuilder WithPassword(string password) { - this.password = Secret.From(password); + _password = Secret.From(password); WithEnvironment("POSTGRES_PASSWORD", password); return this; } @@ -60,25 +64,25 @@ public PostgreSqlBuilder WithPassword(string password) /// protected override PostgreSqlConfiguration BuildConfiguration() { - PostgreSqlConfiguration configuration = base.BuildConfiguration(); - Dictionary environment = new Dictionary( + var configuration = base.BuildConfiguration(); + Dictionary environment = new( configuration.Environment, StringComparer.Ordinal ) { - ["POSTGRES_DB"] = database, - ["POSTGRES_USER"] = username, - ["POSTGRES_PASSWORD"] = password.Value, + ["POSTGRES_DB"] = _database, + ["POSTGRES_USER"] = _username, + ["POSTGRES_PASSWORD"] = _password.Value, }; - IReadOnlyList waitStrategies = + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies - : new[] { (IWaitStrategy)Wait.ForCommand("pg_isready", "-U", username, "-d", database) }; + : new[] { (IWaitStrategy)Wait.ForCommand("pg_isready", "-U", _username, "-d", _database) }; return configuration with { - Database = database, - Username = username, - Password = password, + Database = _database, + Username = _username, + Password = _password, Environment = environment, WaitStrategies = waitStrategies, }; @@ -88,17 +92,17 @@ protected override PostgreSqlConfiguration BuildConfiguration() protected override void Validate(ContainerConfiguration configuration) { base.Validate(configuration); - if (string.IsNullOrWhiteSpace(database)) + if (string.IsNullOrWhiteSpace(_database)) { throw new WslContainerConfigurationException("PostgreSQL database cannot be empty."); } - if (string.IsNullOrWhiteSpace(username)) + if (string.IsNullOrWhiteSpace(_username)) { throw new WslContainerConfigurationException("PostgreSQL username cannot be empty."); } - if (string.IsNullOrEmpty(password.Value)) + if (string.IsNullOrEmpty(_password.Value)) { throw new WslContainerConfigurationException("PostgreSQL password cannot be empty."); } diff --git a/src/WslContainers.PostgreSql/PostgreSqlConfiguration.cs b/src/src/PostgreSql/PostgreSqlConfiguration.cs similarity index 93% rename from src/WslContainers.PostgreSql/PostgreSqlConfiguration.cs rename to src/src/PostgreSql/PostgreSqlConfiguration.cs index a2f6302..a24f585 100644 --- a/src/WslContainers.PostgreSql/PostgreSqlConfiguration.cs +++ b/src/src/PostgreSql/PostgreSqlConfiguration.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Diagnostics; + namespace Purview.WslContainers.PostgreSql; /// Immutable configuration for a PostgreSQL test container. diff --git a/src/WslContainers.PostgreSql/PostgreSqlContainer.cs b/src/src/PostgreSql/PostgreSqlContainer.cs similarity index 68% rename from src/WslContainers.PostgreSql/PostgreSqlContainer.cs rename to src/src/PostgreSql/PostgreSqlContainer.cs index 95f3ca5..77a7a0d 100644 --- a/src/WslContainers.PostgreSql/PostgreSqlContainer.cs +++ b/src/src/PostgreSql/PostgreSqlContainer.cs @@ -5,24 +5,24 @@ namespace Purview.WslContainers.PostgreSql; /// A throwaway PostgreSQL database running on WSL Containers. public sealed class PostgreSqlContainer : WslContainer { - private readonly PostgreSqlConfiguration configuration; + readonly PostgreSqlConfiguration _configuration; internal PostgreSqlContainer(PostgreSqlConfiguration configuration, IContainerRuntime runtime) : base(configuration, runtime) { - this.configuration = configuration; + _configuration = configuration; } /// Connection string pointing at the mapped host port. Safe to call after . public string GetConnectionString() { - NpgsqlConnectionStringBuilder builder = new NpgsqlConnectionStringBuilder + NpgsqlConnectionStringBuilder builder = new() { Host = "localhost", Port = GetMappedPublicPort(PostgreSqlBuilder.PostgreSqlPort), - Database = configuration.Database, - Username = configuration.Username, - Password = configuration.Password.Value, + Database = _configuration.Database, + Username = _configuration.Username, + Password = _configuration.Password.Value, }; return builder.ConnectionString; } diff --git a/src/WslContainers.RabbitMq/WslContainers.RabbitMq.csproj b/src/src/RabbitMq/RabbitMq.csproj similarity index 100% rename from src/WslContainers.RabbitMq/WslContainers.RabbitMq.csproj rename to src/src/RabbitMq/RabbitMq.csproj diff --git a/src/WslContainers.RabbitMq/RabbitMqBuilder.cs b/src/src/RabbitMq/RabbitMqBuilder.cs similarity index 79% rename from src/WslContainers.RabbitMq/RabbitMqBuilder.cs rename to src/src/RabbitMq/RabbitMqBuilder.cs index 8f31fd3..10ebf04 100644 --- a/src/WslContainers.RabbitMq/RabbitMqBuilder.cs +++ b/src/src/RabbitMq/RabbitMqBuilder.cs @@ -1,3 +1,7 @@ +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; + namespace Purview.WslContainers.RabbitMq; /// Fluent builder for a RabbitMQ test container. @@ -12,9 +16,9 @@ public class RabbitMqBuilder : ContainerBuilderDefault image (includes the management plugin). public const string RabbitMqImage = "rabbitmq:3-management"; - private string username = "guest"; - private Secret password = Secret.From("guest"); - private string virtualHost = "/"; + string _username = "guest"; + Secret _password = Secret.From("guest"); + string _virtualHost = "/"; /// Creates a builder with the default image. public RabbitMqBuilder() @@ -40,7 +44,7 @@ public RabbitMqBuilder(IContainerRuntime runtime) /// Sets the AMQP username. public RabbitMqBuilder WithUsername(string username) { - this.username = username; + _username = username; WithEnvironment("RABBITMQ_DEFAULT_USER", username); return this; } @@ -48,7 +52,7 @@ public RabbitMqBuilder WithUsername(string username) /// Sets the AMQP password. public RabbitMqBuilder WithPassword(string password) { - this.password = Secret.From(password); + _password = Secret.From(password); WithEnvironment("RABBITMQ_DEFAULT_PASS", password); return this; } @@ -56,7 +60,7 @@ public RabbitMqBuilder WithPassword(string password) /// Sets the default virtual host. public RabbitMqBuilder WithVirtualHost(string virtualHost) { - this.virtualHost = virtualHost; + _virtualHost = virtualHost; WithEnvironment("RABBITMQ_DEFAULT_VHOST", virtualHost); return this; } @@ -67,25 +71,25 @@ public RabbitMqBuilder WithVirtualHost(string virtualHost) /// protected override RabbitMqConfiguration BuildConfiguration() { - RabbitMqConfiguration configuration = base.BuildConfiguration(); - Dictionary environment = new Dictionary( + var configuration = base.BuildConfiguration(); + Dictionary environment = new( configuration.Environment, StringComparer.Ordinal ) { - ["RABBITMQ_DEFAULT_USER"] = username, - ["RABBITMQ_DEFAULT_PASS"] = password.Value, - ["RABBITMQ_DEFAULT_VHOST"] = virtualHost, + ["RABBITMQ_DEFAULT_USER"] = _username, + ["RABBITMQ_DEFAULT_PASS"] = _password.Value, + ["RABBITMQ_DEFAULT_VHOST"] = _virtualHost, }; - IReadOnlyList waitStrategies = + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { (IWaitStrategy)Wait.ForLogMessage("Server startup complete") }; return configuration with { - Username = username, - Password = password, - VirtualHost = virtualHost, + Username = _username, + Password = _password, + VirtualHost = _virtualHost, Environment = environment, WaitStrategies = waitStrategies, }; @@ -95,17 +99,17 @@ protected override RabbitMqConfiguration BuildConfiguration() protected override void Validate(ContainerConfiguration configuration) { base.Validate(configuration); - if (string.IsNullOrWhiteSpace(username)) + if (string.IsNullOrWhiteSpace(_username)) { throw new WslContainerConfigurationException("RabbitMQ username cannot be empty."); } - if (string.IsNullOrEmpty(password.Value)) + if (string.IsNullOrEmpty(_password.Value)) { throw new WslContainerConfigurationException("RabbitMQ password cannot be empty."); } - if (string.IsNullOrEmpty(virtualHost)) + if (string.IsNullOrEmpty(_virtualHost)) { throw new WslContainerConfigurationException("RabbitMQ virtual host cannot be empty."); } diff --git a/src/WslContainers.RabbitMq/RabbitMqConfiguration.cs b/src/src/RabbitMq/RabbitMqConfiguration.cs similarity index 93% rename from src/WslContainers.RabbitMq/RabbitMqConfiguration.cs rename to src/src/RabbitMq/RabbitMqConfiguration.cs index 5a29445..7df0cf8 100644 --- a/src/WslContainers.RabbitMq/RabbitMqConfiguration.cs +++ b/src/src/RabbitMq/RabbitMqConfiguration.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Diagnostics; + namespace Purview.WslContainers.RabbitMq; /// Immutable configuration for a RabbitMQ test container. diff --git a/src/WslContainers.RabbitMq/RabbitMqContainer.cs b/src/src/RabbitMq/RabbitMqContainer.cs similarity index 76% rename from src/WslContainers.RabbitMq/RabbitMqContainer.cs rename to src/src/RabbitMq/RabbitMqContainer.cs index c1417de..bfe4c1d 100644 --- a/src/WslContainers.RabbitMq/RabbitMqContainer.cs +++ b/src/src/RabbitMq/RabbitMqContainer.cs @@ -3,12 +3,12 @@ namespace Purview.WslContainers.RabbitMq; /// A throwaway RabbitMQ broker running on WSL Containers. public sealed class RabbitMqContainer : WslContainer { - private readonly RabbitMqConfiguration configuration; + readonly RabbitMqConfiguration _configuration; internal RabbitMqContainer(RabbitMqConfiguration configuration, IContainerRuntime runtime) : base(configuration, runtime) { - this.configuration = configuration; + _configuration = configuration; } /// @@ -23,15 +23,15 @@ public string GetConnectionString() /// AMQP endpoint (RabbitMQ.Client ConnectionFactory.Uri). public Uri GetAmqpEndpoint() { - UriBuilder builder = new UriBuilder + UriBuilder builder = new() { Scheme = "amqp", Host = "localhost", Port = GetMappedPublicPort(RabbitMqBuilder.AmqpPort), - UserName = configuration.Username, - Password = configuration.Password.Value, + UserName = _configuration.Username, + Password = _configuration.Password.Value, + Path = _configuration.VirtualHost is "/" or "" ? "/" : "/" + _configuration.VirtualHost.TrimStart('/') }; - builder.Path = configuration.VirtualHost is "/" or "" ? "/" : "/" + configuration.VirtualHost.TrimStart('/'); return builder.Uri; } diff --git a/src/WslContainers.Redis/WslContainers.Redis.csproj b/src/src/Redis/Redis.csproj similarity index 100% rename from src/WslContainers.Redis/WslContainers.Redis.csproj rename to src/src/Redis/Redis.csproj diff --git a/src/WslContainers.Redis/RedisBuilder.cs b/src/src/Redis/RedisBuilder.cs similarity index 93% rename from src/WslContainers.Redis/RedisBuilder.cs rename to src/src/Redis/RedisBuilder.cs index c374551..3260466 100644 --- a/src/WslContainers.Redis/RedisBuilder.cs +++ b/src/src/Redis/RedisBuilder.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Waiting; + namespace Purview.WslContainers.Redis; /// @@ -35,8 +37,8 @@ public RedisBuilder(IContainerRuntime runtime) /// protected override RedisConfiguration BuildConfiguration() { - RedisConfiguration configuration = base.BuildConfiguration(); - IReadOnlyList waitStrategies = + var configuration = base.BuildConfiguration(); + var waitStrategies = configuration.WaitStrategies.Count > 0 ? configuration.WaitStrategies : new[] { (IWaitStrategy)Wait.ForCommand("redis-cli", "ping") }; diff --git a/src/WslContainers.Redis/RedisConfiguration.cs b/src/src/Redis/RedisConfiguration.cs similarity index 100% rename from src/WslContainers.Redis/RedisConfiguration.cs rename to src/src/Redis/RedisConfiguration.cs diff --git a/src/WslContainers.Redis/RedisContainer.cs b/src/src/Redis/RedisContainer.cs similarity index 100% rename from src/WslContainers.Redis/RedisContainer.cs rename to src/src/Redis/RedisContainer.cs diff --git a/src/WslContainers/ContainerBuilder.cs b/src/src/WslContainers/ContainerBuilder.cs similarity index 88% rename from src/WslContainers/ContainerBuilder.cs rename to src/src/WslContainers/ContainerBuilder.cs index 376e145..fc83484 100644 --- a/src/WslContainers/ContainerBuilder.cs +++ b/src/src/WslContainers/ContainerBuilder.cs @@ -1,4 +1,8 @@ using Purview.WslContainers.Images; +using Purview.WslContainers.Mounts; +using Purview.WslContainers.Networking; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; namespace Purview.WslContainers; @@ -7,34 +11,34 @@ namespace Purview.WslContainers; /// Builder instances accumulate configuration; produces an immutable configuration /// and a container bound to a runtime. /// +[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1005:Avoid excessive parameters on generic types")] public abstract class ContainerBuilder where TBuilder : ContainerBuilder where TContainer : WslContainer where TConfiguration : ContainerConfiguration, new() { - private string? _image; - private Image? _parsedImage; - private string? _name; - private string? _hostname; - private string? _domainName; - private readonly Dictionary _environment = new(StringComparer.Ordinal); - private readonly List _command = new(); - private string? _workingDirectory; - private readonly List _portBindings = new(); - private readonly List _bindMounts = new(); - private readonly List _namedVolumes = new(); - private ContainerNetworkingMode _networkingMode = ContainerNetworkingMode.Bridged; - private bool _privileged; - private bool _enableGpu; - private bool _enableAutoRemove; - private PullPolicy _pullPolicy = PullPolicy.Missing; - private TimeSpan _startupTimeout = TimeSpan.FromMinutes(5); - private readonly List _waitStrategies = new(); - private RegistryCredentials? _registryCredentials; - private IContainerRuntime? _runtime; + string? _image; + Image? _parsedImage; + string? _name; + string? _hostname; + string? _domainName; + readonly Dictionary _environment = [with(StringComparer.Ordinal)]; + readonly List _command = []; + string? _workingDirectory; + readonly List _portBindings = []; + readonly List _bindMounts = []; + readonly List _namedVolumes = []; + ContainerNetworkingMode _networkingMode = ContainerNetworkingMode.Bridged; + bool _privileged; + bool _enableGPU; + bool _enableAutoRemove; + PullPolicy _pullPolicy = PullPolicy.Missing; + TimeSpan _startupTimeout = TimeSpan.FromMinutes(5); + readonly List _waitStrategies = []; + RegistryCredentials? _registryCredentials; /// The runtime the built container will use. Defaults to the process-wide . - protected IContainerRuntime? Runtime => _runtime; + protected IContainerRuntime? Runtime { get; private set; } /// Creates a builder using the default process-wide runtime. protected ContainerBuilder() { } @@ -43,7 +47,7 @@ protected ContainerBuilder() { } protected ContainerBuilder(IContainerRuntime runtime) { ArgumentNullException.ThrowIfNull(runtime); - _runtime = runtime; + Runtime = runtime; } /// Sets the container image. Invalid references fail fast (at configuration time). @@ -123,7 +127,7 @@ public TBuilder WithEnvironment(string key, string value) public TBuilder WithEnvironment(IReadOnlyDictionary environment) { ArgumentNullException.ThrowIfNull(environment); - foreach ((string key, string value) in environment) + foreach ((var key, var value) in environment) { _environment[key] = value; } @@ -215,9 +219,9 @@ public TBuilder WithPrivileged(bool privileged = true) } /// Exposes GPU devices to the container. - public TBuilder WithGpu(bool enable = true) + public TBuilder WithGPU(bool enable = true) { - _enableGpu = enable; + _enableGPU = enable; return (TBuilder)this; } @@ -273,14 +277,14 @@ public TBuilder WithRegistryCredentials(Uri serverAddress, string username, stri public TBuilder WithRuntime(IContainerRuntime runtime) { ArgumentNullException.ThrowIfNull(runtime); - _runtime = runtime; + Runtime = runtime; return (TBuilder)this; } /// Validates the configuration and builds the container. public virtual TContainer Build() { - TConfiguration configuration = BuildConfiguration(); + var configuration = BuildConfiguration(); Validate(configuration); return CreateContainer(configuration); } @@ -306,7 +310,7 @@ protected virtual TConfiguration BuildConfiguration() NamedVolumes = _namedVolumes, NetworkingMode = _networkingMode, Privileged = _privileged, - EnableGpu = _enableGpu, + EnableGPU = _enableGPU, EnableAutoRemove = _enableAutoRemove, PullPolicy = _pullPolicy, StartupTimeout = _startupTimeout, @@ -332,7 +336,7 @@ protected virtual void Validate(ContainerConfiguration configuration) ); } - foreach (PortBinding portBinding in configuration.PortBindings) + foreach (var portBinding in configuration.PortBindings) { if (portBinding.Protocol == PortProtocol.Udp) { @@ -352,7 +356,7 @@ protected virtual void Validate(ContainerConfiguration configuration) } } - foreach (BindMount bindMount in configuration.BindMounts) + foreach (var bindMount in configuration.BindMounts) { if (string.IsNullOrWhiteSpace(bindMount.HostPath) || string.IsNullOrWhiteSpace(bindMount.ContainerPath)) { @@ -360,7 +364,7 @@ protected virtual void Validate(ContainerConfiguration configuration) } } - foreach (NamedVolume namedVolume in configuration.NamedVolumes) + foreach (var namedVolume in configuration.NamedVolumes) { if (string.IsNullOrWhiteSpace(namedVolume.Name) || string.IsNullOrWhiteSpace(namedVolume.ContainerPath)) { @@ -388,7 +392,7 @@ public ContainerBuilder(IContainerRuntime runtime) /// protected override WslContainer CreateContainer(ContainerConfiguration configuration) { - IContainerRuntime runtime = Runtime ?? WslContainerRuntime.Instance; + var runtime = Runtime ?? WslContainerRuntime.Instance; return new WslContainer(configuration, runtime); } } diff --git a/src/WslContainers/ContainerConfiguration.cs b/src/src/WslContainers/ContainerConfiguration.cs similarity index 91% rename from src/WslContainers/ContainerConfiguration.cs rename to src/src/WslContainers/ContainerConfiguration.cs index de08360..c205abf 100644 --- a/src/WslContainers/ContainerConfiguration.cs +++ b/src/src/WslContainers/ContainerConfiguration.cs @@ -1,3 +1,8 @@ +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Images; +using Purview.WslContainers.Mounts; +using Purview.WslContainers.Networking; +using Purview.WslContainers.Waiting; using System.Text; namespace Purview.WslContainers; @@ -43,7 +48,7 @@ public record ContainerConfiguration : IContainerConfiguration public bool Privileged { get; init; } /// - public bool EnableGpu { get; init; } + public bool EnableGPU { get; init; } /// public bool EnableAutoRemove { get; init; } @@ -84,7 +89,7 @@ protected virtual bool PrintMembers(StringBuilder builder) builder.Append(", ").Append(nameof(NamedVolumes)).Append(" = ").Append(NamedVolumes); builder.Append(", ").Append(nameof(NetworkingMode)).Append(" = ").Append(NetworkingMode); builder.Append(", ").Append(nameof(Privileged)).Append(" = ").Append(Privileged); - builder.Append(", ").Append(nameof(EnableGpu)).Append(" = ").Append(EnableGpu); + builder.Append(", ").Append(nameof(EnableGPU)).Append(" = ").Append(EnableGPU); builder.Append(", ").Append(nameof(EnableAutoRemove)).Append(" = ").Append(EnableAutoRemove); builder.Append(", ").Append(nameof(PullPolicy)).Append(" = ").Append(PullPolicy); builder.Append(", ").Append(nameof(StartupTimeout)).Append(" = ").Append(StartupTimeout); diff --git a/src/WslContainers/Containers/ContainerLogEntry.cs b/src/src/WslContainers/Containers/ContainerLogEntry.cs similarity index 79% rename from src/WslContainers/Containers/ContainerLogEntry.cs rename to src/src/WslContainers/Containers/ContainerLogEntry.cs index 95fc0f7..7f8e6c1 100644 --- a/src/WslContainers/Containers/ContainerLogEntry.cs +++ b/src/src/WslContainers/Containers/ContainerLogEntry.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Containers; /// A single line of container init-process output. public sealed record ContainerLogEntry(DateTimeOffset Timestamp, LogOutput Stream, string Text); diff --git a/src/WslContainers/Containers/ContainerState.cs b/src/src/WslContainers/Containers/ContainerState.cs similarity index 91% rename from src/WslContainers/Containers/ContainerState.cs rename to src/src/WslContainers/Containers/ContainerState.cs index 4b8c099..09df5b4 100644 --- a/src/WslContainers/Containers/ContainerState.cs +++ b/src/src/WslContainers/Containers/ContainerState.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Containers; /// Lifecycle state of a WSLC container. public enum ContainerState diff --git a/src/WslContainers/Containers/ExecOptions.cs b/src/src/WslContainers/Containers/ExecOptions.cs similarity index 94% rename from src/WslContainers/Containers/ExecOptions.cs rename to src/src/WslContainers/Containers/ExecOptions.cs index 8ff1428..ccf2980 100644 --- a/src/WslContainers/Containers/ExecOptions.cs +++ b/src/src/WslContainers/Containers/ExecOptions.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Containers; /// Options for . public sealed record ExecOptions diff --git a/src/WslContainers/Containers/ExecResult.cs b/src/src/WslContainers/Containers/ExecResult.cs similarity index 85% rename from src/WslContainers/Containers/ExecResult.cs rename to src/src/WslContainers/Containers/ExecResult.cs index 038f076..4ef5489 100644 --- a/src/WslContainers/Containers/ExecResult.cs +++ b/src/src/WslContainers/Containers/ExecResult.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Containers; /// Result of executing a command inside a container. public sealed record ExecResult(long ExitCode, string Stdout, string Stderr) diff --git a/src/WslContainers/Containers/LogStream.cs b/src/src/WslContainers/Containers/LogStream.cs similarity index 82% rename from src/WslContainers/Containers/LogStream.cs rename to src/src/WslContainers/Containers/LogStream.cs index fef2d2f..27e7268 100644 --- a/src/WslContainers/Containers/LogStream.cs +++ b/src/src/WslContainers/Containers/LogStream.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Containers; /// Standard output stream of a container or exec process. public enum LogOutput diff --git a/src/WslContainers/Diagnostics/Secret.cs b/src/src/WslContainers/Diagnostics/Secret.cs similarity index 78% rename from src/WslContainers/Diagnostics/Secret.cs rename to src/src/WslContainers/Diagnostics/Secret.cs index 4082587..bc8ee2c 100644 --- a/src/WslContainers/Diagnostics/Secret.cs +++ b/src/src/WslContainers/Diagnostics/Secret.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Diagnostics; /// /// Wraps a sensitive value so it is never printed by default diagnostics. @@ -6,11 +6,9 @@ namespace Purview.WslContainers; /// public readonly record struct Secret { - private readonly string _value; - - private Secret(string value) + Secret(string value) { - _value = value; + Value = value; } /// Creates a secret from a plain value. @@ -21,7 +19,7 @@ public static Secret From(string value) } /// The underlying value. Handle with care. - public string Value => _value; + public string Value { get; } /// public override string ToString() => ""; diff --git a/src/WslContainers/Diagnostics/SecretRedactor.cs b/src/src/WslContainers/Diagnostics/SecretRedactor.cs similarity index 67% rename from src/WslContainers/Diagnostics/SecretRedactor.cs rename to src/src/WslContainers/Diagnostics/SecretRedactor.cs index b5ebc03..2824eee 100644 --- a/src/WslContainers/Diagnostics/SecretRedactor.cs +++ b/src/src/WslContainers/Diagnostics/SecretRedactor.cs @@ -1,10 +1,10 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Diagnostics; /// Redacts sensitive values from diagnostics and configuration rendering. -internal static class SecretRedactor +static class SecretRedactor { - private static readonly string[] SensitiveKeyMarkers = - { + static readonly string[] SensitiveKeyMarkers = + [ "PASSWORD", "PASS", "TOKEN", @@ -12,7 +12,7 @@ internal static class SecretRedactor "PWD", "CREDENTIAL", "KEY", - }; + ]; public static string RedactEnvironment(IReadOnlyDictionary environment) { @@ -21,15 +21,15 @@ public static string RedactEnvironment(IReadOnlyDictionary envir return "{}"; } - IEnumerable parts = environment.Select(pair => + var parts = environment.Select(pair => IsSensitive(pair.Key) ? $"{pair.Key} = " : $"{pair.Key} = {pair.Value}" ); return "{ " + string.Join(", ", parts) + " }"; } - private static bool IsSensitive(string key) + static bool IsSensitive(string key) { - foreach (string marker in SensitiveKeyMarkers) + foreach (var marker in SensitiveKeyMarkers) { if (key.Contains(marker, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/WslContainers/Diagnostics/WslContainersActivity.cs b/src/src/WslContainers/Diagnostics/WslContainersActivity.cs similarity index 61% rename from src/WslContainers/Diagnostics/WslContainersActivity.cs rename to src/src/WslContainers/Diagnostics/WslContainersActivity.cs index c49cf31..66deb2e 100644 --- a/src/WslContainers/Diagnostics/WslContainersActivity.cs +++ b/src/src/WslContainers/Diagnostics/WslContainersActivity.cs @@ -1,10 +1,10 @@ using System.Diagnostics; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Diagnostics; /// Activity source for WSL Containers operations. Listener names: Purview.WslContainers. -internal static class WslContainersActivity +static class WslContainersActivity { /// Activities: session.start, image.pull, container.create/start/stop/delete, exec.create, wait. - public static readonly ActivitySource Source = new ActivitySource("Purview.WslContainers"); + public static readonly ActivitySource Source = new("Purview.WslContainers"); } diff --git a/src/WslContainers/IContainer.cs b/src/src/WslContainers/IContainer.cs similarity index 97% rename from src/WslContainers/IContainer.cs rename to src/src/WslContainers/IContainer.cs index 3161e72..2db4bbc 100644 --- a/src/WslContainers/IContainer.cs +++ b/src/src/WslContainers/IContainer.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Containers; + namespace Purview.WslContainers; /// A throwaway Linux container running on WSL Containers. diff --git a/src/WslContainers/IContainerBuilder.cs b/src/src/WslContainers/IContainerBuilder.cs similarity index 100% rename from src/WslContainers/IContainerBuilder.cs rename to src/src/WslContainers/IContainerBuilder.cs diff --git a/src/WslContainers/IContainerConfiguration.cs b/src/src/WslContainers/IContainerConfiguration.cs similarity index 93% rename from src/WslContainers/IContainerConfiguration.cs rename to src/src/WslContainers/IContainerConfiguration.cs index 1e6d9c3..b9d5631 100644 --- a/src/WslContainers/IContainerConfiguration.cs +++ b/src/src/WslContainers/IContainerConfiguration.cs @@ -1,4 +1,7 @@ using Purview.WslContainers.Images; +using Purview.WslContainers.Mounts; +using Purview.WslContainers.Networking; +using Purview.WslContainers.Waiting; namespace Purview.WslContainers; @@ -42,7 +45,7 @@ public interface IContainerConfiguration bool Privileged { get; } /// Expose GPU devices to the container. - bool EnableGpu { get; } + bool EnableGPU { get; } /// Automatically remove the container when it stops. bool EnableAutoRemove { get; } diff --git a/src/WslContainers/IContainerRuntime.cs b/src/src/WslContainers/IContainerRuntime.cs similarity index 100% rename from src/WslContainers/IContainerRuntime.cs rename to src/src/WslContainers/IContainerRuntime.cs diff --git a/src/WslContainers/IContainerSession.cs b/src/src/WslContainers/IContainerSession.cs similarity index 100% rename from src/WslContainers/IContainerSession.cs rename to src/src/WslContainers/IContainerSession.cs diff --git a/src/WslContainers/Images/Image.cs b/src/src/WslContainers/Images/Image.cs similarity index 84% rename from src/WslContainers/Images/Image.cs rename to src/src/WslContainers/Images/Image.cs index 5685fa2..be23cb4 100644 --- a/src/WslContainers/Images/Image.cs +++ b/src/src/WslContainers/Images/Image.cs @@ -6,10 +6,10 @@ namespace Purview.WslContainers.Images; /// public readonly record struct Image { - private const string DefaultRegistry = "docker.io"; - private const string DefaultTag = "latest"; + const string DefaultRegistry = "docker.io"; + const string DefaultTag = "latest"; - private Image(string registry, string repository, string tag) + Image(string registry, string repository, string tag) { Registry = registry; Repository = repository; @@ -44,9 +44,9 @@ public static Image Parse(string reference) throw new ArgumentException("Image reference cannot be empty.", nameof(reference)); } - string tag = DefaultTag; - int lastSlash = reference.LastIndexOf('/', StringComparison.Ordinal); - int lastColon = reference.LastIndexOf(':', StringComparison.Ordinal); + var tag = DefaultTag; + var lastSlash = reference.LastIndexOf('/', StringComparison.Ordinal); + var lastColon = reference.LastIndexOf(':', StringComparison.Ordinal); if (lastColon > lastSlash) { tag = NormalizeTag(reference[(lastColon + 1)..]); @@ -55,7 +55,7 @@ public static Image Parse(string reference) string registry; string repository; - int firstSlash = reference.IndexOf('/', StringComparison.Ordinal); + var firstSlash = reference.IndexOf('/', StringComparison.Ordinal); if (firstSlash < 0) { registry = DefaultRegistry; @@ -63,7 +63,7 @@ public static Image Parse(string reference) } else { - string firstSegment = reference[..firstSlash]; + var firstSegment = reference[..firstSlash]; if ( firstSegment.Contains('.', StringComparison.Ordinal) || firstSegment.Contains(':', StringComparison.Ordinal) @@ -80,8 +80,11 @@ public static Image Parse(string reference) } } +#pragma warning disable CA1308 // Normalize strings to uppercase registry = registry.ToLowerInvariant(); repository = repository.ToLowerInvariant(); +#pragma warning restore CA1308 // Normalize strings to uppercase + if (registry == DefaultRegistry && repository.IndexOf('/', StringComparison.Ordinal) < 0) { repository = $"library/{repository}"; @@ -110,7 +113,7 @@ public bool MatchesStoredName(string storedName) try { - Image other = Parse(storedName); + var other = Parse(storedName); return string.Equals(other.Tag, Tag, StringComparison.OrdinalIgnoreCase) && string.Equals(other.ShortName, ShortName, StringComparison.OrdinalIgnoreCase); } @@ -123,7 +126,7 @@ public bool MatchesStoredName(string storedName) /// public override string ToString() => FullReference; - private static string NormalizeTag(string tag) + static string NormalizeTag(string tag) { if (string.IsNullOrWhiteSpace(tag)) { @@ -140,10 +143,11 @@ private static string NormalizeTag(string tag) throw new ArgumentException($"Image tag '{tag}' is invalid.", nameof(tag)); } + // Tags are case-sensitive, but we normalize to lowercase for consistency with repository names. return tag; } - private static void ValidateRepository(string reference, string repository) + static void ValidateRepository(string reference, string repository) { if (repository.Length == 0) { diff --git a/src/WslContainers/Images/ImagePullProgress.cs b/src/src/WslContainers/Images/ImagePullProgress.cs similarity index 100% rename from src/WslContainers/Images/ImagePullProgress.cs rename to src/src/WslContainers/Images/ImagePullProgress.cs diff --git a/src/WslContainers/Images/ImageSummary.cs b/src/src/WslContainers/Images/ImageSummary.cs similarity index 100% rename from src/WslContainers/Images/ImageSummary.cs rename to src/src/WslContainers/Images/ImageSummary.cs diff --git a/src/WslContainers/Images/PullPolicy.cs b/src/src/WslContainers/Images/PullPolicy.cs similarity index 89% rename from src/WslContainers/Images/PullPolicy.cs rename to src/src/WslContainers/Images/PullPolicy.cs index 322de58..2c99788 100644 --- a/src/WslContainers/Images/PullPolicy.cs +++ b/src/src/WslContainers/Images/PullPolicy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Images; /// Controls when an image is pulled. public enum PullPolicy diff --git a/src/src/WslContainers/LogBuffer.cs b/src/src/WslContainers/LogBuffer.cs new file mode 100644 index 0000000..ded2dbd --- /dev/null +++ b/src/src/WslContainers/LogBuffer.cs @@ -0,0 +1,149 @@ +using System.Text; +using System.Threading.Channels; +using Microsoft.WSL.Containers; +using Purview.WslContainers.Containers; + +namespace Purview.WslContainers; + +/// +/// Bounded buffer for init-process output. Accumulates decoded lines for diagnostics while publishing +/// every entry to a channel so wait strategies and consumers can tail live output. +/// +sealed class LogBuffer +{ + const int MaxEntries = 4096; + readonly Lock _sync = new(); + readonly List _entries = [with(MaxEntries)]; + readonly Channel _channel = Channel.CreateBounded( + new BoundedChannelOptions(1024) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + } + ); + readonly Decoder _stdoutDecoder = Encoding.UTF8.GetDecoder(); + readonly Decoder _stderrDecoder = Encoding.UTF8.GetDecoder(); + readonly StringBuilder _stdoutPending = new(); + readonly StringBuilder _stderrPending = new(); + int _attached; + + public void Attach(Process process) + { + if (Interlocked.Exchange(ref _attached, 1) == 1) + { + return; + } + + process.OutputReceived += OnStdout; + process.ErrorReceived += OnStderr; + process.Exited += _ => + { + FlushRemaining(); + Complete(); + }; + } + + public IReadOnlyList Snapshot() + { + lock (_sync) + { + return _entries.ToArray(); + } + } + + public string GetLogs(LogOutput? stream) + { + lock (_sync) + { + var source = stream is LogOutput s ? _entries.Where(entry => entry.Stream == s) : _entries; + return string.Join('\n', source.Select(entry => entry.Text)); + } + } + + public ChannelReader Reader => _channel.Reader; + + /// + /// Completes the live stream. Call once no further output can arrive (init process exit or container + /// disposal) so consumers tailing finish instead of waiting forever. + /// + public void Complete() + { + _channel.Writer.TryComplete(); + } + + void OnStdout(byte[] data) + { + Append(LogOutput.Stdout, _stdoutDecoder, _stdoutPending, data); + } + + void OnStderr(byte[] data) + { + Append(LogOutput.Stderr, _stderrDecoder, _stderrPending, data); + } + + void Append(LogOutput stream, Decoder decoder, StringBuilder pending, byte[] data) + { + if (data.Length == 0) + { + return; + } + + var chars = new char[decoder.GetCharCount(data, 0, data.Length, flush: false)]; + var count = decoder.GetChars(data, 0, data.Length, chars, 0, flush: false); + lock (_sync) + { + pending.Append(chars, 0, count); + FlushLines(stream, pending); + } + } + + void FlushLines(LogOutput stream, StringBuilder pending) + { + var text = pending.ToString(); + pending.Clear(); + var start = 0; + for (var i = 0; i < text.Length; i++) + { + if (text[i] == '\n') + { + Add(new ContainerLogEntry(DateTimeOffset.UtcNow, stream, text[start..i].TrimEnd('\r'))); + start = i + 1; + } + } + + if (start < text.Length) + { + pending.Append(text[start..]); + } + } + + void FlushRemaining() + { + lock (_sync) + { + if (_stdoutPending.Length > 0) + { + Add(new ContainerLogEntry(DateTimeOffset.UtcNow, LogOutput.Stdout, _stdoutPending.ToString())); + _stdoutPending.Clear(); + } + + if (_stderrPending.Length > 0) + { + Add(new ContainerLogEntry(DateTimeOffset.UtcNow, LogOutput.Stderr, _stderrPending.ToString())); + _stderrPending.Clear(); + } + } + } + + void Add(ContainerLogEntry entry) + { + _entries.Add(entry); + while (_entries.Count > MaxEntries) + { + _entries.RemoveAt(0); + } + + _channel.Writer.TryWrite(entry); + } +} diff --git a/src/WslContainers/Mounts/BindMount.cs b/src/src/WslContainers/Mounts/BindMount.cs similarity index 81% rename from src/WslContainers/Mounts/BindMount.cs rename to src/src/WslContainers/Mounts/BindMount.cs index ddf19cb..30f4eda 100644 --- a/src/WslContainers/Mounts/BindMount.cs +++ b/src/src/WslContainers/Mounts/BindMount.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.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/WslContainers/Mounts/NamedVolume.cs b/src/src/WslContainers/Mounts/NamedVolume.cs similarity index 83% rename from src/WslContainers/Mounts/NamedVolume.cs rename to src/src/WslContainers/Mounts/NamedVolume.cs index 95e4905..bff93df 100644 --- a/src/WslContainers/Mounts/NamedVolume.cs +++ b/src/src/WslContainers/Mounts/NamedVolume.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.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/WslContainers/Networking/ContainerNetworkingMode.cs b/src/src/WslContainers/Networking/ContainerNetworkingMode.cs similarity index 89% rename from src/WslContainers/Networking/ContainerNetworkingMode.cs rename to src/src/WslContainers/Networking/ContainerNetworkingMode.cs index 3b09068..d139b58 100644 --- a/src/WslContainers/Networking/ContainerNetworkingMode.cs +++ b/src/src/WslContainers/Networking/ContainerNetworkingMode.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Networking; /// Container networking mode. WSLC only supports none and bridged. public enum ContainerNetworkingMode diff --git a/src/WslContainers/Networking/PortBinding.cs b/src/src/WslContainers/Networking/PortBinding.cs similarity index 94% rename from src/WslContainers/Networking/PortBinding.cs rename to src/src/WslContainers/Networking/PortBinding.cs index b69a130..e7dc7f4 100644 --- a/src/WslContainers/Networking/PortBinding.cs +++ b/src/src/WslContainers/Networking/PortBinding.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Networking; /// A host-to-container port mapping. /// The port inside the container. diff --git a/src/WslContainers/Networking/PortProtocol.cs b/src/src/WslContainers/Networking/PortProtocol.cs similarity index 81% rename from src/WslContainers/Networking/PortProtocol.cs rename to src/src/WslContainers/Networking/PortProtocol.cs index a0b18b6..1d1d7aa 100644 --- a/src/WslContainers/Networking/PortProtocol.cs +++ b/src/src/WslContainers/Networking/PortProtocol.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Networking; /// Port transport protocol. public enum PortProtocol diff --git a/src/WslContainers/RegistryCredentials.cs b/src/src/WslContainers/RegistryCredentials.cs similarity index 94% rename from src/WslContainers/RegistryCredentials.cs rename to src/src/WslContainers/RegistryCredentials.cs index d56ba69..6998c9c 100644 --- a/src/WslContainers/RegistryCredentials.cs +++ b/src/src/WslContainers/RegistryCredentials.cs @@ -1,3 +1,5 @@ +using Purview.WslContainers.Diagnostics; + namespace Purview.WslContainers; /// Credentials for authenticating to a private container registry. diff --git a/src/WslContainers/Runtime/WslContainerException.cs b/src/src/WslContainers/Runtime/WslContainerException.cs similarity index 58% rename from src/WslContainers/Runtime/WslContainerException.cs rename to src/src/WslContainers/Runtime/WslContainerException.cs index b2cffd8..5a0ff1c 100644 --- a/src/WslContainers/Runtime/WslContainerException.cs +++ b/src/src/WslContainers/Runtime/WslContainerException.cs @@ -1,4 +1,6 @@ -namespace Purview.WslContainers; +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 @@ -21,36 +23,26 @@ public WslContainerConfigurationException(string message, Exception innerExcepti } /// WSL/WSL Containers prerequisites are missing or incompatible. -public class WslContainerPrerequisiteException : WslContainerException +public class WslContainerPrerequisiteException(string message) : WslContainerException(message) { - public WslContainerPrerequisiteException(string message) - : base(message) { } } /// A requested feature is not supported by the current WSLC API. -public class WslContainerNotSupportedException : WslContainerException +public class WslContainerNotSupportedException(string message) : WslContainerException(message) { - public WslContainerNotSupportedException(string message) - : base(message) { } } /// Starting the container failed (image, OCI runtime, or environment). -public class WslContainerStartupException : WslContainerException +public class WslContainerStartupException(string message, Exception innerException) : WslContainerException(message, innerException) { - public WslContainerStartupException(string message, Exception innerException) - : base(message, innerException) { } } /// An operation did not complete within its configured timeout. -public class WslContainerTimeoutException : WslContainerException +public class WslContainerTimeoutException(string message) : WslContainerException(message) { - public WslContainerTimeoutException(string message) - : base(message) { } } /// The backing WSLC session terminated unexpectedly. -public class WslContainerSessionTerminatedException : WslContainerException +public class WslContainerSessionTerminatedException(string message) : WslContainerException(message) { - public WslContainerSessionTerminatedException(string message) - : base(message) { } } diff --git a/src/WslContainers/StorageMode.cs b/src/src/WslContainers/StorageMode.cs similarity index 99% rename from src/WslContainers/StorageMode.cs rename to src/src/WslContainers/StorageMode.cs index f21b794..9df4d97 100644 --- a/src/WslContainers/StorageMode.cs +++ b/src/src/WslContainers/StorageMode.cs @@ -11,4 +11,4 @@ public enum StorageMode /// Each session gets a unique, isolated storage directory (fresh image store per process). PerSession = 1, -} \ No newline at end of file +} diff --git a/src/WslContainers/Waiting/AllWaitStrategy.cs b/src/src/WslContainers/Waiting/AllWaitStrategy.cs similarity index 76% rename from src/WslContainers/Waiting/AllWaitStrategy.cs rename to src/src/WslContainers/Waiting/AllWaitStrategy.cs index fa4847d..2c8170c 100644 --- a/src/WslContainers/Waiting/AllWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/AllWaitStrategy.cs @@ -1,20 +1,20 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Composes strategies; ready only when all contained strategies are ready on the same check. public sealed class AllWaitStrategy : WaitStrategy { - private readonly IReadOnlyList strategies; + readonly IReadOnlyList _strategies; public AllWaitStrategy(IReadOnlyList strategies) { ArgumentNullException.ThrowIfNull(strategies); - this.strategies = strategies; + _strategies = strategies; } /// public override async Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { - foreach (IWaitStrategy strategy in strategies) + foreach (var strategy in _strategies) { if (!await strategy.UntilAsync(context, cancellationToken).ConfigureAwait(false)) { diff --git a/src/WslContainers/Waiting/AnyWaitStrategy.cs b/src/src/WslContainers/Waiting/AnyWaitStrategy.cs similarity index 76% rename from src/WslContainers/Waiting/AnyWaitStrategy.cs rename to src/src/WslContainers/Waiting/AnyWaitStrategy.cs index d90e38a..1c698bf 100644 --- a/src/WslContainers/Waiting/AnyWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/AnyWaitStrategy.cs @@ -1,20 +1,20 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Composes strategies; ready when any contained strategy is ready on the same check. public sealed class AnyWaitStrategy : WaitStrategy { - private readonly IReadOnlyList strategies; + readonly IReadOnlyList _strategies; public AnyWaitStrategy(IReadOnlyList strategies) { ArgumentNullException.ThrowIfNull(strategies); - this.strategies = strategies; + _strategies = strategies; } /// public override async Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { - foreach (IWaitStrategy strategy in strategies) + foreach (var strategy in _strategies) { if (await strategy.UntilAsync(context, cancellationToken).ConfigureAwait(false)) { diff --git a/src/WslContainers/Waiting/CommandWaitStrategy.cs b/src/src/WslContainers/Waiting/CommandWaitStrategy.cs similarity index 72% rename from src/WslContainers/Waiting/CommandWaitStrategy.cs rename to src/src/WslContainers/Waiting/CommandWaitStrategy.cs index 0432399..ae04827 100644 --- a/src/WslContainers/Waiting/CommandWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/CommandWaitStrategy.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Waits until a command executed inside the container exits with the expected code (default 0). [SuppressMessage( @@ -10,20 +10,20 @@ namespace Purview.WslContainers; )] public sealed class CommandWaitStrategy : WaitStrategy { - private readonly string[] command; - private int expectedExitCode; + readonly string[] _command; + int _expectedExitCode; public CommandWaitStrategy(string[] command, int expectedExitCode = 0) { ArgumentNullException.ThrowIfNull(command); - this.command = command; - this.expectedExitCode = expectedExitCode; + _command = command; + _expectedExitCode = expectedExitCode; } /// Sets the expected exit code. Defaults to 0. public CommandWaitStrategy ForExitCode(int exitCode) { - expectedExitCode = exitCode; + _expectedExitCode = exitCode; return this; } @@ -32,10 +32,10 @@ public override async Task UntilAsync(WaitContext context, CancellationTok { try { - ExecResult result = await context - .Container.ExecAsync(command, options: null, cancellationToken) + var result = await context + .Container.ExecAsync(_command, options: null, cancellationToken) .ConfigureAwait(false); - return result.ExitCode == expectedExitCode; + return result.ExitCode == _expectedExitCode; } catch (OperationCanceledException) { diff --git a/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs b/src/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs similarity index 81% rename from src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs rename to src/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs index 06d8bc1..1841ba2 100644 --- a/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/ContainerRunningWaitStrategy.cs @@ -1,4 +1,6 @@ -namespace Purview.WslContainers; +using Purview.WslContainers.Containers; + +namespace Purview.WslContainers.Waiting; /// Waits until the container init process is running. public sealed class ContainerRunningWaitStrategy : WaitStrategy diff --git a/src/WslContainers/Waiting/CustomWaitStrategy.cs b/src/src/WslContainers/Waiting/CustomWaitStrategy.cs similarity index 67% rename from src/WslContainers/Waiting/CustomWaitStrategy.cs rename to src/src/WslContainers/Waiting/CustomWaitStrategy.cs index 18fd6b6..3479195 100644 --- a/src/WslContainers/Waiting/CustomWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/CustomWaitStrategy.cs @@ -1,19 +1,19 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Waits until a custom predicate returns true. public sealed class CustomWaitStrategy : WaitStrategy { - private readonly Func> predicate; + readonly Func> _predicate; public CustomWaitStrategy(Func> predicate) { ArgumentNullException.ThrowIfNull(predicate); - this.predicate = predicate; + _predicate = predicate; } /// public override Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { - return predicate(context, cancellationToken); + return _predicate(context, cancellationToken); } } diff --git a/src/WslContainers/Waiting/HttpWaitStrategy.cs b/src/src/WslContainers/Waiting/HttpWaitStrategy.cs similarity index 56% rename from src/WslContainers/Waiting/HttpWaitStrategy.cs rename to src/src/WslContainers/Waiting/HttpWaitStrategy.cs index f6e2b71..0cd8326 100644 --- a/src/WslContainers/Waiting/HttpWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/HttpWaitStrategy.cs @@ -1,8 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Waits until an HTTP(S) request against the mapped host port succeeds. [SuppressMessage( @@ -10,56 +9,51 @@ namespace Purview.WslContainers; "CA1031:Do not catch general exception types", Justification = "Transient network failures during startup are treated as not-ready." )] -public sealed class HttpWaitStrategy : WaitStrategy +public sealed class HttpWaitStrategy(string path = "/") : WaitStrategy { - private readonly string path; - private ushort? containerPort; - private string scheme = "http"; - private HttpStatusCode? expectedStatusCode = HttpStatusCode.OK; - private Func? statusPredicate; - private readonly Dictionary headers = new(StringComparer.OrdinalIgnoreCase); - private bool allowInsecureTls; - private TimeSpan requestTimeout = TimeSpan.FromSeconds(2); - - public HttpWaitStrategy(string path = "/") - { - this.path = string.IsNullOrEmpty(path) ? "/" : path; - } + readonly string _path = string.IsNullOrEmpty(path) ? "/" : path; + ushort? _containerPort; + string _scheme = "http"; + HttpStatusCode? _expectedStatusCode = HttpStatusCode.OK; + Func? _statusPredicate; + readonly Dictionary _headers = [with(StringComparer.OrdinalIgnoreCase)]; + bool _allowInsecureTls; + readonly TimeSpan _requestTimeout = TimeSpan.FromSeconds(2); /// The container port to connect to. When unset, the first mapped port is used. public HttpWaitStrategy ForPort(ushort containerPort) { - this.containerPort = containerPort; + _containerPort = containerPort; return this; } /// The URL scheme: http or https. public HttpWaitStrategy ForScheme(string scheme) { - this.scheme = scheme; + _scheme = scheme; return this; } /// The expected HTTP status code. public HttpWaitStrategy ForStatusCode(HttpStatusCode statusCode) { - expectedStatusCode = statusCode; - statusPredicate = null; + _expectedStatusCode = statusCode; + _statusPredicate = null; return this; } /// A predicate over the response status code. public HttpWaitStrategy ForStatusPredicate(Func predicate) { - statusPredicate = predicate; - expectedStatusCode = null; + _statusPredicate = predicate; + _expectedStatusCode = null; return this; } /// Adds a request header. public HttpWaitStrategy ForHeader(string name, string value) { - headers[name] = value; + _headers[name] = value; return this; } @@ -68,14 +62,14 @@ public HttpWaitStrategy ForHeader(string name, string value) /// public HttpWaitStrategy AllowInsecureTls() { - allowInsecureTls = true; + _allowInsecureTls = true; return this; } /// public override async Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { - int? hostPort = containerPort is ushort containerPortValue + var hostPort = _containerPort is ushort containerPortValue ? context.GetHostPort(containerPortValue) : context.FirstHostPort; if (hostPort is null) @@ -83,32 +77,34 @@ public override async Task UntilAsync(WaitContext context, CancellationTok return false; } - string url = $"{scheme}://127.0.0.1:{hostPort}{path}"; + var url = $"{_scheme}://127.0.0.1:{hostPort}{_path}"; try { - using SocketsHttpHandler handler = new SocketsHttpHandler + using SocketsHttpHandler handler = new() { - ConnectTimeout = requestTimeout, + ConnectTimeout = _requestTimeout, PooledConnectionLifetime = TimeSpan.Zero, }; - if (allowInsecureTls) + if (_allowInsecureTls) { +#pragma warning disable CA5359 // Do Not Disable Certificate Validation handler.SslOptions.RemoteCertificateValidationCallback = static (_, _, _, _) => true; +#pragma warning restore CA5359 // Do Not Disable Certificate Validation } - using HttpClient client = new HttpClient(handler) { Timeout = requestTimeout }; - using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); - foreach ((string name, string value) in headers) + using HttpClient client = new(handler) { Timeout = _requestTimeout }; + using HttpRequestMessage request = new(HttpMethod.Get, url); + foreach ((var name, var value) in _headers) { request.Headers.TryAddWithoutValidation(name, value); } - using HttpResponseMessage response = await client + using var response = await client .SendAsync(request, cancellationToken) .ConfigureAwait(false); - int status = (int)response.StatusCode; - return statusPredicate?.Invoke(status) - ?? (expectedStatusCode is HttpStatusCode expected && status == (int)expected); + var status = (int)response.StatusCode; + return _statusPredicate?.Invoke(status) + ?? (_expectedStatusCode is HttpStatusCode expected && status == (int)expected); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { diff --git a/src/WslContainers/Waiting/IWaitStrategy.cs b/src/src/WslContainers/Waiting/IWaitStrategy.cs similarity index 94% rename from src/WslContainers/Waiting/IWaitStrategy.cs rename to src/src/WslContainers/Waiting/IWaitStrategy.cs index 6f6b442..eb638b8 100644 --- a/src/WslContainers/Waiting/IWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/IWaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// A composable readiness check evaluated until the container is ready or the timeout expires. public interface IWaitStrategy diff --git a/src/WslContainers/Waiting/LogMessageWaitStrategy.cs b/src/src/WslContainers/Waiting/LogMessageWaitStrategy.cs similarity index 61% rename from src/WslContainers/Waiting/LogMessageWaitStrategy.cs rename to src/src/WslContainers/Waiting/LogMessageWaitStrategy.cs index 38ed023..d23acb7 100644 --- a/src/WslContainers/Waiting/LogMessageWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/LogMessageWaitStrategy.cs @@ -1,28 +1,28 @@ using System.Text.RegularExpressions; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Waits until the container's accumulated init-process output matches a message or regex. public sealed class LogMessageWaitStrategy : WaitStrategy { - private readonly Regex pattern; + readonly Regex _pattern; public LogMessageWaitStrategy(string message) { ArgumentNullException.ThrowIfNull(message); - pattern = new Regex(Regex.Escape(message), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + _pattern = new Regex(Regex.Escape(message), RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } public LogMessageWaitStrategy(Regex pattern) { ArgumentNullException.ThrowIfNull(pattern); - this.pattern = pattern; + _pattern = pattern; } /// public override async Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { - string logs = await context.Container.GetLogsAsync(stream: null, cancellationToken).ConfigureAwait(false); - return pattern.IsMatch(logs); + var logs = await context.Container.GetLogsAsync(stream: null, cancellationToken).ConfigureAwait(false); + return _pattern.IsMatch(logs); } } diff --git a/src/WslContainers/Waiting/TcpPortWaitStrategy.cs b/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs similarity index 52% rename from src/WslContainers/Waiting/TcpPortWaitStrategy.cs rename to src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs index 19b2725..9f75495 100644 --- a/src/WslContainers/Waiting/TcpPortWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs @@ -1,37 +1,29 @@ using System.Net; using System.Net.Sockets; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Waits until a TCP connection to the mapped host port succeeds (IPv4 loopback). -public sealed class TcpPortWaitStrategy : WaitStrategy +public sealed class TcpPortWaitStrategy(ushort containerPort, TimeSpan connectTimeout) : WaitStrategy { - private readonly ushort containerPort; - private readonly TimeSpan connectTimeout; - - public TcpPortWaitStrategy(ushort containerPort, TimeSpan connectTimeout) - { - this.containerPort = containerPort; - this.connectTimeout = connectTimeout; - } /// public override async Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { - int? hostPort = context.GetHostPort(containerPort); + var hostPort = context.GetHostPort(containerPort); if (hostPort is null) { return false; } - using CancellationTokenSource timeoutSource = new CancellationTokenSource(connectTimeout); - using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource( + using CancellationTokenSource timeoutSource = new(connectTimeout); + using var linked = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, timeoutSource.Token ); try { - using TcpClient client = new TcpClient(); + using TcpClient client = new(); await client.ConnectAsync(IPAddress.Loopback, hostPort.Value, linked.Token).ConfigureAwait(false); return true; } diff --git a/src/WslContainers/Waiting/Wait.cs b/src/src/WslContainers/Waiting/Wait.cs similarity index 98% rename from src/WslContainers/Waiting/Wait.cs rename to src/src/WslContainers/Waiting/Wait.cs index e61c653..1dbe2d8 100644 --- a/src/WslContainers/Waiting/Wait.cs +++ b/src/src/WslContainers/Waiting/Wait.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Factory for composable wait strategies. public static class Wait diff --git a/src/WslContainers/Waiting/WaitContext.cs b/src/src/WslContainers/Waiting/WaitContext.cs similarity index 71% rename from src/WslContainers/Waiting/WaitContext.cs rename to src/src/WslContainers/Waiting/WaitContext.cs index 84c7bdf..abbe0af 100644 --- a/src/WslContainers/Waiting/WaitContext.cs +++ b/src/src/WslContainers/Waiting/WaitContext.cs @@ -1,14 +1,14 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Readiness-check context: the container plus resolved runtime state (ports, IP). public sealed class WaitContext { - private readonly IReadOnlyDictionary portMappings; + readonly IReadOnlyDictionary _portMappings; internal WaitContext(IContainer container, IReadOnlyDictionary portMappings, string? networkIp) { Container = container; - this.portMappings = portMappings; + _portMappings = portMappings; NetworkIp = networkIp; } @@ -21,9 +21,9 @@ internal WaitContext(IContainer container, IReadOnlyDictionary p /// Returns the host port mapped to , or null when not mapped. public int? GetHostPort(ushort containerPort) { - return portMappings.TryGetValue(containerPort, out ushort 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/WslContainers/Waiting/WaitStrategy.cs b/src/src/WslContainers/Waiting/WaitStrategy.cs similarity index 96% rename from src/WslContainers/Waiting/WaitStrategy.cs rename to src/src/WslContainers/Waiting/WaitStrategy.cs index 255a0db..cdadd2a 100644 --- a/src/WslContainers/Waiting/WaitStrategy.cs +++ b/src/src/WslContainers/Waiting/WaitStrategy.cs @@ -1,4 +1,4 @@ -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Base class for wait strategies with shared polling configuration. public abstract class WaitStrategy : IWaitStrategy diff --git a/src/WslContainers/Waiting/WaitStrategyRunner.cs b/src/src/WslContainers/Waiting/WaitStrategyRunner.cs similarity index 67% rename from src/WslContainers/Waiting/WaitStrategyRunner.cs rename to src/src/WslContainers/Waiting/WaitStrategyRunner.cs index d43da59..c316ed2 100644 --- a/src/WslContainers/Waiting/WaitStrategyRunner.cs +++ b/src/src/WslContainers/Waiting/WaitStrategyRunner.cs @@ -1,9 +1,10 @@ -using System.Diagnostics; +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; -namespace Purview.WslContainers; +namespace Purview.WslContainers.Waiting; /// Runs wait strategies with timeout, interval and retries, producing actionable diagnostics on failure. [SuppressMessage( @@ -11,7 +12,7 @@ namespace Purview.WslContainers; "CA1031:Do not catch general exception types", Justification = "Wait checks observe transient errors; diagnostics collection is best-effort." )] -internal static class WaitStrategyRunner +static class WaitStrategyRunner { public static async Task RunAsync( WaitContext context, @@ -20,32 +21,32 @@ public static async Task RunAsync( CancellationToken cancellationToken ) { - foreach (IWaitStrategy strategy in strategies) + foreach (var strategy in strategies) { await RunSingleAsync(context, strategy, defaultTimeout, cancellationToken).ConfigureAwait(false); } } - private static async Task RunSingleAsync( + static async Task RunSingleAsync( WaitContext context, IWaitStrategy strategy, TimeSpan defaultTimeout, CancellationToken cancellationToken ) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.wait"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.wait"); activity?.SetTag("container.name", context.Container.Name); activity?.SetTag("strategy", strategy.GetType().Name); - TimeSpan timeout = strategy.Timeout ?? defaultTimeout; - TimeSpan interval = strategy.Interval > TimeSpan.Zero ? strategy.Interval : TimeSpan.FromMilliseconds(250); - using CancellationTokenSource timeoutSource = CancellationTokenSource.CreateLinkedTokenSource( + var timeout = strategy.Timeout ?? defaultTimeout; + var interval = strategy.Interval > TimeSpan.Zero ? strategy.Interval : TimeSpan.FromMilliseconds(250); + using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken ); timeoutSource.CancelAfter(timeout); - CancellationToken token = timeoutSource.Token; + var token = timeoutSource.Token; Exception? lastError = null; - int failedChecks = 0; + var failedChecks = 0; while (!token.IsCancellationRequested) { bool ready; @@ -82,21 +83,17 @@ CancellationToken cancellationToken } catch (OperationCanceledException) { } } + cancellationToken.ThrowIfCancellationRequested(); - if (cancellationToken.IsCancellationRequested) - { - throw new OperationCanceledException(cancellationToken); - } - - string diagnostics = await BuildDiagnosticsAsync(context, timeout, lastError).ConfigureAwait(false); + var diagnostics = await BuildDiagnosticsAsync(context, timeout, lastError, cancellationToken).ConfigureAwait(false); throw new WslContainerTimeoutException( $"Container '{context.Container.Name}' did not become ready within {timeout} for strategy {strategy.GetType().Name}. {diagnostics}" ); } - private static async Task BuildDiagnosticsAsync(WaitContext context, TimeSpan timeout, Exception? lastError) + static async Task BuildDiagnosticsAsync(WaitContext context, TimeSpan timeout, Exception? lastError, CancellationToken cancellationToken) { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.Append( CultureInfo.InvariantCulture, $"State: {context.Container.State}. Image: {context.Container.Image}. Startup timeout: {timeout}." @@ -104,7 +101,7 @@ private static async Task BuildDiagnosticsAsync(WaitContext context, Tim try { - IReadOnlyDictionary ports = context.Container.GetMappedPublicPorts(); + var ports = context.Container.GetMappedPublicPorts(); if (ports.Count > 0) { builder.Append( @@ -125,10 +122,10 @@ private static async Task BuildDiagnosticsAsync(WaitContext context, Tim try { - string logs = await context - .Container.GetLogsAsync(stream: null, CancellationToken.None) + var logs = await context + .Container.GetLogsAsync(stream: null, cancellationToken) .ConfigureAwait(false); - string tail = logs.Length > 2000 ? logs[^2000..] : logs; + var tail = logs.Length > 2000 ? logs[^2000..] : logs; if (tail.Length > 0) { builder.Append(CultureInfo.InvariantCulture, $" Recent output: {tail}"); diff --git a/src/WslContainers/WslContainer.cs b/src/src/WslContainers/WslContainer.cs similarity index 60% rename from src/WslContainers/WslContainer.cs rename to src/src/WslContainers/WslContainer.cs index 4021484..bd51988 100644 --- a/src/WslContainers/WslContainer.cs +++ b/src/src/WslContainers/WslContainer.cs @@ -1,9 +1,10 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text; -using System.Threading.Channels; using Microsoft.WSL.Containers; -using Purview.WslContainers.Images; +using Purview.WslContainers.Containers; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; using MsContainerState = Microsoft.WSL.Containers.ContainerState; using MsProcess = Microsoft.WSL.Containers.Process; using MsProcessState = Microsoft.WSL.Containers.ProcessState; @@ -19,14 +20,13 @@ namespace Purview.WslContainers; )] public class WslContainer : IContainer { - private readonly IContainerRuntime runtime; - private readonly LogBuffer logBuffer = new(); - private WslContainerSession? session; - private Microsoft.WSL.Containers.Container? handle; - private IReadOnlyDictionary portMappings = new Dictionary(); - private string? networkIp; - private int started; - private int disposed; + readonly LogBuffer _logBuffer = new(); + WslContainerSession? _session; + Container? _handle; + IReadOnlyDictionary _portMappings = new Dictionary(); + string? _networkIp; + int _started; + int _disposed; /// Creates a container bound to a runtime. Prefer building via a . public WslContainer(ContainerConfiguration configuration, IContainerRuntime runtime) @@ -34,7 +34,7 @@ public WslContainer(ContainerConfiguration configuration, IContainerRuntime runt ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(runtime); Configuration = configuration; - this.runtime = runtime; + Runtime = runtime; Name = configuration.Name ?? GenerateName(configuration); } @@ -42,16 +42,16 @@ public WslContainer(ContainerConfiguration configuration, IContainerRuntime runt protected ContainerConfiguration Configuration { get; } /// The runtime this container is bound to. - protected IContainerRuntime Runtime => runtime; + protected IContainerRuntime Runtime { get; } /// public string Name { get; } /// - public string Id => handle?.Id ?? string.Empty; + public string Id => _handle?.Id ?? string.Empty; /// - public ContainerState State { get; private set; } + public Containers.ContainerState State { get; private set; } /// public string Image => Configuration.Image; @@ -59,16 +59,15 @@ public WslContainer(ContainerConfiguration configuration, IContainerRuntime runt /// public virtual async Task StartAsync(CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(Volatile.Read(ref disposed) == 1, this); - if (Volatile.Read(ref started) == 1) + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + if (Volatile.Read(ref _started) == 1) { return; } - WslContainerSession sessionLocal = (WslContainerSession) - await runtime.GetSessionAsync(cancellationToken).ConfigureAwait(false); - session = sessionLocal; - await session + var sessionLocal = (WslContainerSession)await Runtime.GetSessionAsync(cancellationToken).ConfigureAwait(false); + _session = sessionLocal; + await _session .EnsureImageAsync( Configuration.Image, Configuration.PullPolicy, @@ -78,22 +77,20 @@ await session ) .ConfigureAwait(false); - ContainerSettings settings = WslSettingsMapper.ToContainerSettings(Configuration, Name); - Microsoft.WSL.Containers.Container container = await session - .CreateContainerAsync(settings, cancellationToken) - .ConfigureAwait(false); - handle = container; - logBuffer.Attach(container.InitProcess); - await session.StartContainerAsync(container, cancellationToken).ConfigureAwait(false); + var settings = WslSettingsMapper.ToContainerSettings(Configuration, Name); + var container = await _session.CreateContainerAsync(settings, cancellationToken).ConfigureAwait(false); + _handle = container; + _logBuffer.Attach(container.InitProcess); + await _session.StartContainerAsync(container, cancellationToken).ConfigureAwait(false); State = MapState(container.State); - portMappings = WslInspectParser.ParseHostPorts(container.Inspect()); - networkIp = WslInspectParser.TryGetNetworkIp(container.Inspect()); - Volatile.Write(ref started, 1); + _portMappings = WslInspectParser.ParseHostPorts(container.Inspect()); + _networkIp = WslInspectParser.TryGetNetworkIp(container.Inspect()); + Volatile.Write(ref _started, 1); if (Configuration.WaitStrategies.Count > 0) { - WaitContext waitContext = new(this, portMappings, networkIp); + WaitContext waitContext = new(this, _portMappings, _networkIp); await WaitStrategyRunner .RunAsync(waitContext, Configuration.WaitStrategies, Configuration.StartupTimeout, cancellationToken) .ConfigureAwait(false); @@ -104,15 +101,15 @@ await WaitStrategyRunner public virtual async Task StopAsync(CancellationToken cancellationToken = default) { EnsureStarted(); - if (handle is null || session is null) + if (_handle is null || _session is null) { return; } try { - await session - .StopContainerAsync(handle, MsSignal.SIGTERM, TimeSpan.FromSeconds(10), cancellationToken) + await _session + .StopContainerAsync(_handle, MsSignal.SIGTERM, TimeSpan.FromSeconds(10), cancellationToken) .ConfigureAwait(false); } catch (WslContainerSessionTerminatedException) @@ -124,7 +121,7 @@ await session // already stopped; idempotent by design } - State = MapState(handle.State); + State = MapState(_handle.State); } /// @@ -141,14 +138,14 @@ public virtual async Task ExecAsync( } EnsureStarted(); - if (handle is null || session is null) + if (_handle is null || _session is null) { throw new InvalidOperationException("Container is not started."); } - ProcessSettings settings = new ProcessSettings + ProcessSettings settings = new() { - CommandLine = new List(command), + CommandLine = [.. command], OutputMode = ProcessOutputMode.Event, WorkingDirectory = options?.WorkingDirectory, }; @@ -157,8 +154,8 @@ public virtual async Task ExecAsync( settings.EnvironmentVariables = new Dictionary(options.Environment); } - MsProcess process = await session.CreateProcessAsync(handle, settings, cancellationToken).ConfigureAwait(false); - using CancellationTokenRegistration cancelRegistration = cancellationToken.Register( + var process = await _session.CreateProcessAsync(_handle, settings, cancellationToken).ConfigureAwait(false); + using var cancelRegistration = cancellationToken.Register( static state => { if (state is MsProcess processToSignal) @@ -173,23 +170,20 @@ public virtual async Task ExecAsync( process ); - StringBuilder stdout = new StringBuilder(); - StringBuilder stderr = new StringBuilder(); - TaskCompletionSource exitSource = new TaskCompletionSource( - TaskCreationOptions.RunContinuationsAsynchronously - ); + StringBuilder stdout = new(); + StringBuilder stderr = new(); + TaskCompletionSource exitSource = new(TaskCreationOptions.RunContinuationsAsynchronously); process.OutputReceived += data => stdout.Append(Encoding.UTF8.GetString(data)); process.ErrorReceived += data => stderr.Append(Encoding.UTF8.GetString(data)); process.Exited += code => exitSource.TrySetResult(code); - await session.StartProcessAsync(process, cancellationToken).ConfigureAwait(false); + await _session.StartProcessAsync(process, cancellationToken).ConfigureAwait(false); - Task exitTask = exitSource.Task; + var exitTask = exitSource.Task; int exitCode; if (options?.Timeout is TimeSpan timeout) { - Task completed = await Task.WhenAny(exitTask, Task.Delay(timeout, CancellationToken.None)) - .ConfigureAwait(false); + var completed = await Task.WhenAny(exitTask, Task.Delay(timeout, cancellationToken)).ConfigureAwait(false); if (completed == exitTask) { exitCode = await exitTask.ConfigureAwait(false); @@ -217,7 +211,7 @@ public virtual async Task ExecAsync( return new ExecResult(exitCode, stdout.ToString(), stderr.ToString()); } - private static void TryKill(MsProcess process) + static void TryKill(MsProcess process) { try { @@ -227,9 +221,9 @@ private static void TryKill(MsProcess process) } // WSLC does not reliably raise Process.Exited after Signal(SIGKILL); poll the process state instead. - private static async Task PollForExitAsync(MsProcess process, Task exitTask) + static async Task PollForExitAsync(MsProcess process, Task exitTask) { - DateTime deadline = DateTime.UtcNow.AddSeconds(15); + var deadline = DateTime.UtcNow.AddSeconds(15); while (DateTime.UtcNow < deadline) { if (exitTask.IsCompleted) @@ -237,7 +231,7 @@ private static async Task PollForExitAsync(MsProcess process, Task exi return await exitTask.ConfigureAwait(false); } - MsProcessState state = process.State; + var state = process.State; if (state is MsProcessState.Exited or MsProcessState.Signalled) { return process.ExitCode; @@ -253,11 +247,12 @@ private static async Task PollForExitAsync(MsProcess process, Task exi public virtual ushort GetMappedPublicPort(ushort containerPort) { EnsureStarted(); - if (!portMappings.TryGetValue(containerPort, out ushort hostPort)) + if (!_portMappings.TryGetValue(containerPort, out var hostPort)) { throw new WslContainerException($"No host port mapping found for container port {containerPort}."); } + // WSLC does not support dynamic host port assignment; the host port is always the same as the container port. return hostPort; } @@ -265,14 +260,14 @@ public virtual ushort GetMappedPublicPort(ushort containerPort) public virtual IReadOnlyDictionary GetMappedPublicPorts() { EnsureStarted(); - return portMappings; + return _portMappings; } /// public virtual Task GetLogsAsync(LogOutput? stream = null, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(Volatile.Read(ref disposed) == 1, this); - return Task.FromResult(logBuffer.GetLogs(stream)); + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + return Task.FromResult(_logBuffer.GetLogs(stream)); } /// @@ -280,14 +275,14 @@ public virtual async IAsyncEnumerable GetLogsAsync( [EnumeratorCancellation] CancellationToken cancellationToken ) { - ObjectDisposedException.ThrowIf(Volatile.Read(ref disposed) == 1, this); - foreach (ContainerLogEntry entry in logBuffer.Snapshot()) + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + foreach (var entry in _logBuffer.Snapshot()) { yield return entry; } - ChannelReader reader = logBuffer.Reader; - await foreach (ContainerLogEntry entry in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) + var reader = _logBuffer.Reader; + await foreach (var entry in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { yield return entry; } @@ -296,22 +291,22 @@ [EnumeratorCancellation] CancellationToken cancellationToken /// public virtual async ValueTask DisposeAsync() { - if (Interlocked.Exchange(ref disposed, 1) == 1) + if (Interlocked.Exchange(ref _disposed, 1) == 1) { return; } try { - if (handle is not null && session is not null) + if (_handle is not null && _session is not null) { try { - if (handle.State == MsContainerState.Running) + if (_handle.State == MsContainerState.Running) { - await session + await _session .StopContainerAsync( - handle, + _handle, MsSignal.SIGTERM, TimeSpan.FromSeconds(10), CancellationToken.None @@ -323,40 +318,41 @@ await session try { - await session - .DeleteContainerAsync(handle, DeleteContainerOption.Force, CancellationToken.None) + await _session + .DeleteContainerAsync(_handle, DeleteContainerOption.Force, CancellationToken.None) .ConfigureAwait(false); } catch { } try { - handle.Dispose(); + _handle.Dispose(); } catch { } } } finally { - handle = null; + _logBuffer.Complete(); + _handle = null; GC.SuppressFinalize(this); } } - private void EnsureStarted() + void EnsureStarted() { - if (Volatile.Read(ref started) == 0) + if (Volatile.Read(ref _started) == 0) { throw new InvalidOperationException("Container has not been started. Call StartAsync() first."); } } - private static ContainerState MapState(MsContainerState state) + static Containers.ContainerState MapState(MsContainerState state) { - return (ContainerState)state; + return (Containers.ContainerState)state; } - private static string GenerateName(ContainerConfiguration configuration) + static string GenerateName(ContainerConfiguration configuration) { string shortName; try @@ -368,11 +364,11 @@ private static string GenerateName(ContainerConfiguration configuration) shortName = "container"; } - string safe = new string( - shortName - .Select(character => char.IsAsciiLetterOrDigit(character) || character is '-' or '_' ? character : '-') - .ToArray() - ); + string safe = new([ + .. shortName.Select(character => + char.IsAsciiLetterOrDigit(character) || character is '-' or '_' ? character : '-' + ), + ]); return $"{safe}-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..6]}"; } } diff --git a/src/src/WslContainers/WslContainerRuntime.cs b/src/src/WslContainers/WslContainerRuntime.cs new file mode 100644 index 0000000..0415045 --- /dev/null +++ b/src/src/WslContainers/WslContainerRuntime.cs @@ -0,0 +1,246 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.WSL.Containers; + +namespace Purview.WslContainers; + +/// +/// The process-wide WSL Containers runtime. Owns a single lazily started WSLC session. +/// Images are shared by default: sessions use a stable shared storage directory so the image store is +/// pulled once and reused across process runs. When a concurrent process holds that store, the runtime +/// falls back to an isolated per-process store automatically. +/// +[SuppressMessage( + "Design", + "CA2213:Disposable fields should be disposed", + Justification = "The session is released asynchronously in DisposeAsync/TerminateAsync; CA2213 cannot see the async path." +)] +public sealed class WslContainerRuntime : IContainerRuntime +{ + const int ErrorSharingViolation = unchecked((int)0x80070020); + + static readonly Lazy InstanceHolder = new(() => new WslContainerRuntime()); + + readonly WslContainerRuntimeOptions _options; + readonly Lock _sync = new(); + readonly SemaphoreSlim _sessionGate = new(1, 1); + WslContainerSession? _session; + int _disposed; + int _storeVerified; + + /// Creates a runtime with default options. Registers a process-exit cleanup hook. + public WslContainerRuntime() + : this(WslContainerRuntimeOptions.Default) { } + + /// Creates a runtime with the given options. + public WslContainerRuntime(WslContainerRuntimeOptions options) + { + _options = options ?? WslContainerRuntimeOptions.Default; + if (!_options.DisableProcessExitCleanup) + { + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + } + } + + /// The default process-wide runtime. + public static WslContainerRuntime Instance => InstanceHolder.Value; + + /// + public async Task GetInfoAsync(CancellationToken cancellationToken = default) + { + await Task.Yield(); + var version = WslcService.GetVersion(); + var missing = WslcService.GetMissingComponents(); + var compatible = !missing.Contains(Component.SdkNeedsUpdate); + return new WslContainerRuntimeInfo( + $"{version.Major}.{version.Minor}.{version.Revision}", + missing.Select(component => component.ToString()).ToArray(), + missing.Count == 0, + compatible + ); + } + + /// + public async Task GetSessionAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + var local = GetOrCreateSession(); + if (Volatile.Read(ref _storeVerified) == 1) + { + await local.EnsureStartedAsync(cancellationToken).ConfigureAwait(false); + return local; + } + + // A session's image store lives in storage.vhdx and is opened lazily on first access rather than + // at session start, so contention from a concurrent process can surface on the first store read + // instead of during start. Verify the store once, under a gate, and fall back to an isolated + // per-process store when another process holds it. + await _sessionGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) == 1, this); + local = GetOrCreateSession(); + if (Volatile.Read(ref _storeVerified) == 0) + { + local = await EnsureStoreAsync(local, cancellationToken).ConfigureAwait(false); + Volatile.Write(ref _storeVerified, 1); + } + else + { + await local.EnsureStartedAsync(cancellationToken).ConfigureAwait(false); + } + + return local; + } + finally + { + _sessionGate.Release(); + } + } + + async Task EnsureStoreAsync(WslContainerSession session, CancellationToken cancellationToken) + { + try + { + await session.EnsureStartedAsync(cancellationToken).ConfigureAwait(false); + // Opens (and therefore locks) storage.vhdx; throws 0x80070020 when another process holds it. + await session.ListImagesAsync(cancellationToken).ConfigureAwait(false); + return session; + } + catch (Exception ex) when (ShouldFallBackToIsolatedStore(ex, IsUsingDefaultSharedStore())) + { + // A concurrent process holds the shared image-store VHD. Isolate this process instead of failing. + session.DisposeSession(); + var isolated = CreateIsolatedSession(); + _session = isolated; + await isolated.EnsureStartedAsync(cancellationToken).ConfigureAwait(false); + await isolated.ListImagesAsync(cancellationToken).ConfigureAwait(false); + return isolated; + } + } + + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) == 1) + { + return; + } + + var local = _session; + if (local is not null) + { + await local.TerminateAsync().ConfigureAwait(false); + } + + _sessionGate.Dispose(); + GC.SuppressFinalize(this); + } + + WslContainerSession GetOrCreateSession() + { + var local = _session; + if (local is not null) + { + return local; + } + + lock (_sync) + { + local = _session; + if (local is null) + { + local = CreateSession(); + _session = local; + } + + return local; + } + } + + WslContainerSession CreateSession() + { + var name = NextSessionName(); + var storagePath = ResolveStoragePath(name); + return new WslContainerSession(name, storagePath, _options); + } + + WslContainerSession CreateIsolatedSession() + { + var name = NextSessionName(); + var storagePath = Path.Combine(PerSessionRoot, name); + // An isolated fallback store is a throwaway: it is removed when the session terminates so + // contended runs do not accumulate per-process image stores. + return new WslContainerSession(name, storagePath, _options, deleteStorageOnTerminate: true); + } + + string ResolveStoragePath(string sessionName) + { + if (_options.StoragePath is not null) + { + return _options.StoragePath; + } + + var envPath = Environment.GetEnvironmentVariable("WSL_CONTAINERS_STORAGE_PATH"); + if (!string.IsNullOrEmpty(envPath)) + { + return envPath; + } + + if (_options.StorageMode == StorageMode.PerSession) + { + return Path.Combine(PerSessionRoot, sessionName); + } + + // Default to a shared store in the user's local application data directory. + return SharedImagesRoot; + } + + bool IsUsingDefaultSharedStore() + { + return _options.StoragePath is null + && string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WSL_CONTAINERS_STORAGE_PATH")) + && _options.StorageMode == StorageMode.Shared; + } + + static string NextSessionName() + { + return $"wslc-{Environment.ProcessId}-{Guid.NewGuid().ToString("N")[..8]}"; + } + + static string PerSessionRoot => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Purview", + "WslContainers", + "sessions" + ); + + static string SharedImagesRoot => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Purview", + "WslContainers", + "images" + ); + + static bool IsSharingViolation(Exception exception) + { + return exception.HResult == ErrorSharingViolation; + } + + /// + /// True when is a Windows sharing violation (0x80070020) and the + /// runtime is using the default shared image store, meaning a concurrent process holds the store VHD + /// and this process should fall back to an isolated per-process store. + /// + internal static bool ShouldFallBackToIsolatedStore(Exception exception, bool isUsingDefaultSharedStore) + { + return isUsingDefaultSharedStore && IsSharingViolation(exception); + } + + void OnProcessExit(object? sender, EventArgs e) + { + var local = _session; + local?.TerminateBestEffort(); + } +} diff --git a/src/WslContainers/WslContainerRuntimeInfo.cs b/src/src/WslContainers/WslContainerRuntimeInfo.cs similarity index 100% rename from src/WslContainers/WslContainerRuntimeInfo.cs rename to src/src/WslContainers/WslContainerRuntimeInfo.cs diff --git a/src/WslContainers/WslContainerRuntimeOptions.cs b/src/src/WslContainers/WslContainerRuntimeOptions.cs similarity index 95% rename from src/WslContainers/WslContainerRuntimeOptions.cs rename to src/src/WslContainers/WslContainerRuntimeOptions.cs index 0184b7a..896f76b 100644 --- a/src/WslContainers/WslContainerRuntimeOptions.cs +++ b/src/src/WslContainers/WslContainerRuntimeOptions.cs @@ -4,13 +4,13 @@ namespace Purview.WslContainers; public sealed record WslContainerRuntimeOptions { /// CPU count for the session VM. - public uint? CpuCount { get; init; } + public uint? CPUCount { get; init; } /// Memory (MB) for the session VM. public uint? MemorySizeInMB { get; init; } /// Expose GPU devices in the session. - public bool EnableGpu { get; init; } + public bool EnableGPU { get; init; } /// /// Session name. Defaults to wslc-{processId}-{random}. Session names are machine-unique. diff --git a/src/WslContainers/WslContainerSession.cs b/src/src/WslContainers/WslContainerSession.cs similarity index 55% rename from src/WslContainers/WslContainerSession.cs rename to src/src/WslContainers/WslContainerSession.cs index 1676960..64bcbef 100644 --- a/src/WslContainers/WslContainerSession.cs +++ b/src/src/WslContainers/WslContainerSession.cs @@ -1,8 +1,9 @@ using System.Collections.Concurrent; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.WSL.Containers; +using Purview.WslContainers.Diagnostics; using Purview.WslContainers.Images; +using Purview.WslContainers.Runtime; using Windows.Foundation; using Process = Microsoft.WSL.Containers.Process; @@ -13,57 +14,65 @@ namespace Purview.WslContainers; "CA1031:Do not catch general exception types", Justification = "Session termination and pull cleanup are best-effort; WSLC surfaces a wide range of COMException variants." )] -internal sealed class WslContainerSession : IContainerSession +sealed class WslContainerSession : IContainerSession { - private readonly Session session; - private readonly string name; - private readonly SemaphoreSlim lifecycleGate = new(1, 1); - private readonly ConcurrentDictionary activePulls = new(StringComparer.Ordinal); - private int started; - private int terminated; - - public WslContainerSession(string name, string storagePath, WslContainerRuntimeOptions options) + readonly Session _session; + readonly SemaphoreSlim _lifecycleGate = new(1, 1); + readonly ConcurrentDictionary _activePulls = new(StringComparer.Ordinal); + readonly string _storagePath; + readonly bool _deleteStorageOnTerminate; + int _started; + int _terminated; + + public WslContainerSession( + string name, + string storagePath, + WslContainerRuntimeOptions options, + bool deleteStorageOnTerminate = false + ) { - this.name = name; - SessionSettings settings = new SessionSettings(name, storagePath) + Name = name; + _storagePath = storagePath; + _deleteStorageOnTerminate = deleteStorageOnTerminate; + SessionSettings settings = new(name, storagePath) { - CpuCount = options.CpuCount, + CpuCount = options.CPUCount, MemorySizeInMB = options.MemorySizeInMB, - EnableGpu = options.EnableGpu, + EnableGpu = options.EnableGPU, Timeout = options.SessionTimeout, }; - session = new Session(settings); - session.Terminated += reason => Terminated?.Invoke(reason); + _session = new Session(settings); + _session.Terminated += reason => Terminated?.Invoke(reason); } /// Raised when the underlying session terminates. internal event Action? Terminated; - public string Name => name; + public string Name { get; } public async Task EnsureStartedAsync(CancellationToken cancellationToken) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.session.start"); - activity?.SetTag("session.name", name); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.session.start"); + activity?.SetTag("session.name", Name); ThrowIfTerminated(); - if (Volatile.Read(ref started) == 1) + if (Volatile.Read(ref _started) == 1) { return; } - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); - if (Volatile.Read(ref started) == 0) + if (Volatile.Read(ref _started) == 0) { - session.Start(); - Volatile.Write(ref started, 1); + _session.Start(); + Volatile.Write(ref _started, 1); } } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } @@ -76,7 +85,7 @@ CancellationToken cancellationToken ) { await EnsureStartedAsync(cancellationToken).ConfigureAwait(false); - Image reference = Image.Parse(image); + var reference = Image.Parse(image); switch (policy) { case PullPolicy.Never: @@ -105,97 +114,115 @@ CancellationToken cancellationToken public async Task> ListImagesAsync(CancellationToken cancellationToken) { await EnsureStartedAsync(cancellationToken).ConfigureAwait(false); - return session - .GetImages() - .Select(image => new ImageSummary(image.Name, image.Size, image.CreatedTimestamp)) - .ToArray(); + return await ReadImagesAsync(cancellationToken).ConfigureAwait(false); + } + + // GetImages() reads (and locks) the session's storage VHD, so it is gated with the other + // session-mutating operations rather than treated as a side-effect-free read. + async Task> ReadImagesAsync(CancellationToken cancellationToken) + { + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + ThrowIfTerminated(); + return _session + .GetImages() + .Select(image => new ImageSummary(image.Name, image.Size, image.CreatedTimestamp)) + .ToArray(); + } + finally + { + _lifecycleGate.Release(); + } } public async Task TerminateAsync(CancellationToken cancellationToken = default) { - if (Interlocked.Exchange(ref terminated, 1) == 1) + if (Interlocked.Exchange(ref _terminated, 1) == 1) { return; } try { - session.Terminate(); + _session.Terminate(); } catch { } try { - session.Dispose(); + _session.Dispose(); } catch { } - lifecycleGate.Dispose(); + _lifecycleGate.Dispose(); + TryDeleteStorage(); await Task.CompletedTask; } /// Synchronous best-effort terminate for the process-exit hook. internal void TerminateBestEffort() { - if (Interlocked.Exchange(ref terminated, 1) == 1) + if (Interlocked.Exchange(ref _terminated, 1) == 1) { return; } try { - session.Terminate(); + _session.Terminate(); } catch { } try { - session.Dispose(); + _session.Dispose(); } catch { } - lifecycleGate.Dispose(); + _lifecycleGate.Dispose(); + TryDeleteStorage(); } /// Releases a session that failed to start (e.g. a shared-store contention fallback). internal void DisposeSession() { - if (Interlocked.Exchange(ref terminated, 1) == 1) + if (Interlocked.Exchange(ref _terminated, 1) == 1) { return; } try { - session.Dispose(); + _session.Dispose(); } catch { } - lifecycleGate.Dispose(); + _lifecycleGate.Dispose(); } internal async Task CreateContainerAsync(ContainerSettings settings, CancellationToken cancellationToken) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.create"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.create"); activity?.SetTag("container.name", settings.Name); await EnsureStartedAsync(cancellationToken).ConfigureAwait(false); - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); - return session.CreateContainer(settings); + return _session.CreateContainer(settings); } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } internal async Task StartContainerAsync(Container container, CancellationToken cancellationToken) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.start"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.start"); activity?.SetTag("container.id", container.Id); - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); @@ -203,7 +230,7 @@ internal async Task StartContainerAsync(Container container, CancellationToken c } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } @@ -214,9 +241,9 @@ internal async Task StopContainerAsync( CancellationToken cancellationToken ) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.stop"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.stop"); activity?.SetTag("container.id", container.Id); - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); @@ -224,7 +251,7 @@ CancellationToken cancellationToken } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } @@ -234,9 +261,9 @@ internal async Task DeleteContainerAsync( CancellationToken cancellationToken ) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.delete"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.container.delete"); activity?.SetTag("container.id", container.Id); - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); @@ -244,7 +271,7 @@ CancellationToken cancellationToken } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } @@ -254,9 +281,9 @@ internal async Task CreateProcessAsync( CancellationToken cancellationToken ) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.exec.create"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.exec.create"); activity?.SetTag("container.id", container.Id); - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); @@ -264,13 +291,13 @@ CancellationToken cancellationToken } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } internal async Task StartProcessAsync(Process process, CancellationToken cancellationToken) { - await lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); + await _lifecycleGate.WaitAsync(cancellationToken).ConfigureAwait(false); try { ThrowIfTerminated(); @@ -278,7 +305,7 @@ internal async Task StartProcessAsync(Process process, CancellationToken cancell } finally { - lifecycleGate.Release(); + _lifecycleGate.Release(); } } @@ -287,20 +314,21 @@ public async ValueTask DisposeAsync() await TerminateAsync().ConfigureAwait(false); } - private async Task ContainsImageAsync(Image reference, CancellationToken cancellationToken) + async Task ContainsImageAsync(Image reference, CancellationToken cancellationToken) { await EnsureStartedAsync(cancellationToken).ConfigureAwait(false); - return session.GetImages().Any(image => reference.MatchesStoredName(image.Name)); + var images = await ReadImagesAsync(cancellationToken).ConfigureAwait(false); + return images.Any(image => reference.MatchesStoredName(image.Name)); } - private Task PullDeduplicatedAsync( + Task PullDeduplicatedAsync( Image reference, IProgress? progress, RegistryCredentials? credentials, CancellationToken cancellationToken ) { - return activePulls.GetOrAdd( + return _activePulls.GetOrAdd( reference.FullReference, static (_, state) => state.Session.PullCoreAsync( @@ -319,19 +347,19 @@ CancellationToken cancellationToken ); } - private async Task PullCoreAsync( + async Task PullCoreAsync( Image reference, IProgress? progress, RegistryCredentials? credentials, CancellationToken cancellationToken ) { - using Activity? activity = WslContainersActivity.Source.StartActivity("wslcontainer.image.pull"); + using var activity = WslContainersActivity.Source.StartActivity("wslcontainer.image.pull"); activity?.SetTag("image", reference.FullReference); - PullImageOptions options = new PullImageOptions(reference.FullReference); + PullImageOptions options = new(reference.FullReference); if (credentials is not null) { - AuthenticateResult authentication = session.Authenticate( + var authentication = _session.Authenticate( credentials.ServerAddress, credentials.Username, credentials.Password.Value @@ -339,8 +367,8 @@ CancellationToken cancellationToken options.RegistryAuth = authentication.IdentityToken; } - IAsyncActionWithProgress operation = session.PullImageAsync(options); - using CancellationTokenRegistration registration = cancellationToken.Register( + var operation = _session.PullImageAsync(options); + using var registration = cancellationToken.Register( static state => { if (state is IAsyncActionWithProgress op) @@ -366,15 +394,39 @@ CancellationToken cancellationToken } finally { - activePulls.TryRemove(reference.FullReference, out _); + _activePulls.TryRemove(reference.FullReference, out _); + } + } + + void TryDeleteStorage() + { + if (!_deleteStorageOnTerminate) + { + return; + } + + try + { + if (Directory.Exists(_storagePath)) + { + Directory.Delete(_storagePath, recursive: true); + } + } + catch (IOException) + { + // The session manager may still hold the store VHD briefly; the store is transient. + } + catch (UnauthorizedAccessException) + { + // Same. } } - private void ThrowIfTerminated() + void ThrowIfTerminated() { - if (Volatile.Read(ref terminated) == 1) + if (Volatile.Read(ref _terminated) == 1) { - throw new WslContainerSessionTerminatedException($"Session '{name}' has been terminated."); + throw new WslContainerSessionTerminatedException($"Session '{Name}' has been terminated."); } } } diff --git a/src/WslContainers/WslContainers.csproj b/src/src/WslContainers/WslContainers.csproj similarity index 100% rename from src/WslContainers/WslContainers.csproj rename to src/src/WslContainers/WslContainers.csproj diff --git a/src/src/WslContainers/WslInspectParser.cs b/src/src/WslContainers/WslInspectParser.cs new file mode 100644 index 0000000..41b1498 --- /dev/null +++ b/src/src/WslContainers/WslInspectParser.cs @@ -0,0 +1,79 @@ +using System.Text.Json; + +namespace Purview.WslContainers; + +/// Parses the Docker-compatible JSON returned by Container.Inspect(). +static class WslInspectParser +{ + public static IReadOnlyDictionary ParseHostPorts(string inspectJson) + { + Dictionary result = []; + try + { + using var document = JsonDocument.Parse(inspectJson); + if ( + !document.RootElement.TryGetProperty("Ports", out var ports) + || ports.ValueKind != JsonValueKind.Object + ) + { + return result; + } + + foreach (var property in ports.EnumerateObject()) + { + // Key format: "8080/tcp" + var key = property.Name; + var slash = key.IndexOf('/', StringComparison.Ordinal); + if ( + slash <= 0 + || !ushort.TryParse(key.AsSpan(0, slash), out var containerPort) + || property.Value.ValueKind != JsonValueKind.Array + ) + { + continue; + } + + foreach (var binding in property.Value.EnumerateArray()) + { + if ( + binding.TryGetProperty("HostPort", out var hostPort) + && hostPort.ValueKind == JsonValueKind.String + && ushort.TryParse(hostPort.GetString(), out var host) + ) + { + result[containerPort] = host; + break; + } + } + } + } + catch (JsonException) { } + + return result; + } + + public static string? TryGetNetworkIp(string inspectJson, string network = "bridge") + { + try + { + using var document = JsonDocument.Parse(inspectJson); + if ( + document.RootElement.TryGetProperty("NetworkSettings", out var networkSettings) + && networkSettings.TryGetProperty("Networks", out var networks) + && networks.TryGetProperty(network, out var net) + && net.TryGetProperty("IPAddress", out var ip) + && ip.ValueKind == JsonValueKind.String + ) + { + var value = ip.GetString() ?? string.Empty; + return string.IsNullOrEmpty(value) ? null : value; + } + + return null; + } + catch (JsonException) + { + return null; + } + } +} diff --git a/src/WslContainers/WslSettingsMapper.cs b/src/src/WslContainers/WslSettingsMapper.cs similarity index 64% rename from src/WslContainers/WslSettingsMapper.cs rename to src/src/WslContainers/WslSettingsMapper.cs index 6c071c6..cd14133 100644 --- a/src/WslContainers/WslSettingsMapper.cs +++ b/src/src/WslContainers/WslSettingsMapper.cs @@ -1,16 +1,18 @@ using Microsoft.WSL.Containers; +using Purview.WslContainers.Mounts; +using Purview.WslContainers.Networking; using Windows.Networking; namespace Purview.WslContainers; /// Maps the public configuration model onto the WSLC settings types. -internal static class WslSettingsMapper +static class WslSettingsMapper { public static ContainerSettings ToContainerSettings(IContainerConfiguration configuration, string containerName) { - ProcessSettings initProcess = new ProcessSettings + ProcessSettings initProcess = new() { - CommandLine = new List(configuration.Command), + CommandLine = [.. configuration.Command], OutputMode = ProcessOutputMode.Event, WorkingDirectory = configuration.WorkingDirectory, }; @@ -19,7 +21,7 @@ public static ContainerSettings ToContainerSettings(IContainerConfiguration conf initProcess.EnvironmentVariables = new Dictionary(configuration.Environment); } - ContainerSettings settings = new ContainerSettings(configuration.Image) + ContainerSettings settings = new(configuration.Image) { Name = containerName, HostName = configuration.Hostname, @@ -27,31 +29,31 @@ public static ContainerSettings ToContainerSettings(IContainerConfiguration conf InitProcess = initProcess, NetworkingMode = (Microsoft.WSL.Containers.ContainerNetworkingMode)configuration.NetworkingMode, Privileged = configuration.Privileged, - EnableGpu = configuration.EnableGpu, + EnableGpu = configuration.EnableGPU, EnableAutoRemove = configuration.EnableAutoRemove, }; if (configuration.PortBindings.Count > 0) { - settings.PortMappings = new List(configuration.PortBindings.Select(ToPortMapping)); + settings.PortMappings = [.. configuration.PortBindings.Select(ToPortMapping)]; } if (configuration.BindMounts.Count > 0) { - settings.Volumes = new List(configuration.BindMounts.Select(ToVolume)); + settings.Volumes = [.. configuration.BindMounts.Select(ToVolume)]; } if (configuration.NamedVolumes.Count > 0) { - settings.NamedVolumes = new List(configuration.NamedVolumes.Select(ToNamedVolume)); + settings.NamedVolumes = [.. configuration.NamedVolumes.Select(ToNamedVolume)]; } return settings; } - private static ContainerPortMapping ToPortMapping(PortBinding binding) + static ContainerPortMapping ToPortMapping(PortBinding binding) { - ContainerPortMapping mapping = new ContainerPortMapping( + ContainerPortMapping mapping = new( binding.HostPort ?? 0, binding.ContainerPort, (Microsoft.WSL.Containers.PortProtocol)binding.Protocol @@ -64,12 +66,12 @@ private static ContainerPortMapping ToPortMapping(PortBinding binding) return mapping; } - private static ContainerVolume ToVolume(BindMount bindMount) + static ContainerVolume ToVolume(BindMount bindMount) { return new ContainerVolume(bindMount.HostPath, bindMount.ContainerPath, bindMount.ReadOnly); } - private static ContainerNamedVolume ToNamedVolume(NamedVolume namedVolume) + static ContainerNamedVolume ToNamedVolume(NamedVolume namedVolume) { return new ContainerNamedVolume(namedVolume.Name, namedVolume.ContainerPath, namedVolume.ReadOnly); } diff --git a/tests/WslContainers.Azurite.IntegrationTests/WslContainers.Azurite.IntegrationTests.csproj b/src/tests/Azurite.IntegrationTests/Azurite.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.Azurite.IntegrationTests/WslContainers.Azurite.IntegrationTests.csproj rename to src/tests/Azurite.IntegrationTests/Azurite.IntegrationTests.csproj diff --git a/tests/WslContainers.Azurite.IntegrationTests/AzuriteIntegrationTests.cs b/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs similarity index 70% rename from tests/WslContainers.Azurite.IntegrationTests/AzuriteIntegrationTests.cs rename to src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs index 27d38a2..e532cc3 100644 --- a/tests/WslContainers.Azurite.IntegrationTests/AzuriteIntegrationTests.cs +++ b/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs @@ -1,18 +1,13 @@ -using Purview.WslContainers.Azurite; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +namespace Purview.WslContainers.Azurite; -namespace WslContainers.Azurite.IntegrationTests; - -public class AzuriteIntegrationTests +class AzuriteIntegrationTests { [Test] public async Task Azurite_StartsAndExposesEndpoints() { await WslcTest.SkipIfUnavailableAsync(); - await using AzuriteContainer azurite = new AzuriteBuilder().Build(); + await using var azurite = new AzuriteBuilder().Build(); await azurite.StartAsync(); @@ -26,14 +21,14 @@ public async Task ConnectionString_UsesMappedPortsAndAccount() { await WslcTest.SkipIfUnavailableAsync(); - await using AzuriteContainer azurite = new AzuriteBuilder().Build(); + await using var azurite = new AzuriteBuilder().Build(); await azurite.StartAsync(); - ushort blobPort = azurite.GetMappedPublicPort(AzuriteBuilder.BlobPort); - ushort queuePort = azurite.GetMappedPublicPort(AzuriteBuilder.QueuePort); - ushort tablePort = azurite.GetMappedPublicPort(AzuriteBuilder.TablePort); - string connectionString = azurite.GetConnectionString(); + var blobPort = azurite.GetMappedPublicPort(AzuriteBuilder.BlobPort); + var queuePort = azurite.GetMappedPublicPort(AzuriteBuilder.QueuePort); + var tablePort = azurite.GetMappedPublicPort(AzuriteBuilder.TablePort); + var connectionString = azurite.GetConnectionString(); await Assert.That(connectionString).Contains("DefaultEndpointsProtocol=http"); await Assert.That(connectionString).Contains("AccountName=devstoreaccount1"); diff --git a/tests/WslContainers.Azurite.UnitTests/WslContainers.Azurite.UnitTests.csproj b/src/tests/Azurite.UnitTests/Azurite.UnitTests.csproj similarity index 100% rename from tests/WslContainers.Azurite.UnitTests/WslContainers.Azurite.UnitTests.csproj rename to src/tests/Azurite.UnitTests/Azurite.UnitTests.csproj diff --git a/tests/WslContainers.Azurite.UnitTests/AzuriteBuilderTests.cs b/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs similarity index 56% rename from tests/WslContainers.Azurite.UnitTests/AzuriteBuilderTests.cs rename to src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs index 539cdec..542ecd7 100644 --- a/tests/WslContainers.Azurite.UnitTests/AzuriteBuilderTests.cs +++ b/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs @@ -1,35 +1,34 @@ -using Purview.WslContainers; -using Purview.WslContainers.Azurite; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Waiting; -namespace WslContainers.Azurite.UnitTests; +namespace Purview.WslContainers.Azurite; -public class AzuriteBuilderTests +class AzuriteBuilderTests { + static readonly string[] Expected = ["azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0"]; + [Test] public async Task BuildConfig_AppliesDefaults() { - AzuriteConfiguration configuration = new AzuriteBuilder().BuildConfigurationForTesting(); + var configuration = new AzuriteBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("mcr.microsoft.com/azure-storage/azurite:latest"); await Assert .That(configuration.Command) .IsEquivalentTo( - new[] { "azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0" } + Expected ); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(3); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(3); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 10000 && p.AssignRandomHostPort); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 10001 && p.AssignRandomHostPort); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 10002 && p.AssignRandomHostPort); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); } [Test] public async Task BuildConfig_SupportsCustomImage() { - AzuriteConfiguration configuration = new AzuriteBuilder("custom/azurite:1").BuildConfigurationForTesting(); + var configuration = new AzuriteBuilder("custom/azurite:1").BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/custom/azurite:1"); } diff --git a/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj b/src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj rename to src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj diff --git a/tests/WslContainers.MinIo.IntegrationTests/MinIoIntegrationTests.cs b/src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs similarity index 53% rename from tests/WslContainers.MinIo.IntegrationTests/MinIoIntegrationTests.cs rename to src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs index bac00e4..44ddb1e 100644 --- a/tests/WslContainers.MinIo.IntegrationTests/MinIoIntegrationTests.cs +++ b/src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs @@ -1,46 +1,42 @@ -using Purview.WslContainers.MinIo; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; using TUnit.Core.Exceptions; -namespace WslContainers.MinIo.IntegrationTests; +namespace Purview.WslContainers.MinIO; -public class MinIoIntegrationTests +class MinIOIntegrationTests { [Test] - public async Task MinIo_ExposesApiAndConsole() + public async Task MinIO_ExposesAPIAndConsole() { - await using MinIoContainer minio = await StartOrSkipAsync(); + await using var minio = await StartOrSkipAsync(); - await WslcTest.WaitForTcpAsync(minio.GetMappedPublicPort(MinIoBuilder.ApiPort), TimeSpan.FromSeconds(30)); - await WslcTest.WaitForTcpAsync(minio.GetMappedPublicPort(MinIoBuilder.ConsolePort), TimeSpan.FromSeconds(30)); + await WslcTest.WaitForTcpAsync(minio.GetMappedPublicPort(MinIOBuilder.APIPort), TimeSpan.FromSeconds(30)); + await WslcTest.WaitForTcpAsync(minio.GetMappedPublicPort(MinIOBuilder.ConsolePort), TimeSpan.FromSeconds(30)); - using HttpClient client = new HttpClient(); - using HttpResponseMessage response = await client.GetAsync(minio.GetConsoleEndpoint()); + using HttpClient client = new(); + using var response = await client.GetAsync(minio.GetConsoleEndpoint()); await Assert.That((int)response.StatusCode).IsEqualTo(200); } [Test] public async Task Endpoints_UseMappedRandomPorts() { - await using MinIoContainer minio = await StartOrSkipAsync(); + await using var minio = await StartOrSkipAsync(); - ushort apiPort = minio.GetMappedPublicPort(MinIoBuilder.ApiPort); - ushort consolePort = minio.GetMappedPublicPort(MinIoBuilder.ConsolePort); + var apiPort = minio.GetMappedPublicPort(MinIOBuilder.APIPort); + var consolePort = minio.GetMappedPublicPort(MinIOBuilder.ConsolePort); - await Assert.That(minio.GetApiEndpoint().ToString()).IsEqualTo($"http://127.0.0.1:{apiPort}"); + await Assert.That(minio.GetAPIEndpoint().ToString()).IsEqualTo($"http://127.0.0.1:{apiPort}"); await Assert.That(minio.GetConsoleEndpoint().ToString()).IsEqualTo($"http://127.0.0.1:{consolePort}"); await Assert.That(minio.GetAccessKey()).IsEqualTo("minioadmin"); } // MinIO's official image lives on quay.io, which some networks/WSL sessions cannot reach // (e.g. IPv6-only resolution with no route). Skip gracefully rather than fail the suite. - private static async Task StartOrSkipAsync() + static async Task StartOrSkipAsync() { await WslcTest.SkipIfUnavailableAsync(); - MinIoContainer minio = new MinIoBuilder().Build(); + var minio = new MinIOBuilder().Build(); try { await minio.StartAsync(); @@ -49,7 +45,7 @@ private static async Task StartOrSkipAsync() catch (Exception ex) { await minio.DisposeAsync(); - string message = ex.ToString(); + var message = ex.ToString(); if ( message.Contains("network is unreachable", StringComparison.OrdinalIgnoreCase) || message.Contains("unreachable", StringComparison.OrdinalIgnoreCase) diff --git a/tests/WslContainers.MinIo.IntegrationTests/WslContainers.MinIo.IntegrationTests.csproj b/src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj similarity index 100% rename from tests/WslContainers.MinIo.IntegrationTests/WslContainers.MinIo.IntegrationTests.csproj rename to src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj diff --git a/tests/WslContainers.MinIo.UnitTests/MinIoBuilderTests.cs b/src/tests/MinIo.UnitTests/MinIOBuilderTests.cs similarity index 64% rename from tests/WslContainers.MinIo.UnitTests/MinIoBuilderTests.cs rename to src/tests/MinIo.UnitTests/MinIOBuilderTests.cs index c6132cc..6dcab62 100644 --- a/tests/WslContainers.MinIo.UnitTests/MinIoBuilderTests.cs +++ b/src/tests/MinIo.UnitTests/MinIOBuilderTests.cs @@ -1,16 +1,15 @@ -using Purview.WslContainers; -using Purview.WslContainers.MinIo; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Runtime; -namespace WslContainers.MinIo.UnitTests; +namespace Purview.WslContainers.MinIO; -public class MinIoBuilderTests +class MinIOBuilderTests { + static readonly string[] Expected = ["minio", "server", "/data", "--console-address", ":9001"]; + [Test] public async Task BuildConfig_AppliesDefaults() { - MinIoConfiguration configuration = new MinIoBuilder().BuildConfigurationForTesting(); + var configuration = new MinIOBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("quay.io/minio/minio:latest"); await Assert.That(configuration.AccessKey).IsEqualTo("minioadmin"); @@ -19,8 +18,8 @@ public async Task BuildConfig_AppliesDefaults() await Assert.That(configuration.Environment["MINIO_ROOT_PASSWORD"]).IsEqualTo("minioadmin"); await Assert .That(configuration.Command) - .IsEquivalentTo(new[] { "minio", "server", "/data", "--console-address", ":9001" }); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(2); + .IsEquivalentTo(Expected); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(2); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 9000 && p.AssignRandomHostPort); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 9001 && p.AssignRandomHostPort); } @@ -28,7 +27,7 @@ await Assert [Test] public async Task BuildConfig_EmptySecret_Throws() { - MinIoBuilder builder = new MinIoBuilder().WithSecretKey(string.Empty); + var builder = new MinIOBuilder().WithSecretKey(string.Empty); await Assert.That(() => builder.Build()).Throws(); } diff --git a/tests/WslContainers.MinIo.UnitTests/WslContainers.MinIo.UnitTests.csproj b/src/tests/MsSql.IntegrationTests/MsSql.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.MinIo.UnitTests/WslContainers.MinIo.UnitTests.csproj rename to src/tests/MsSql.IntegrationTests/MsSql.IntegrationTests.csproj diff --git a/tests/WslContainers.MsSql.IntegrationTests/MsSqlIntegrationTests.cs b/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs similarity index 52% rename from tests/WslContainers.MsSql.IntegrationTests/MsSqlIntegrationTests.cs rename to src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs index 5d24396..92a1fa5 100644 --- a/tests/WslContainers.MsSql.IntegrationTests/MsSqlIntegrationTests.cs +++ b/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs @@ -1,29 +1,25 @@ using Microsoft.Data.SqlClient; -using Purview.WslContainers.MsSql; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -namespace WslContainers.MsSql.IntegrationTests; +namespace Purview.WslContainers.MsSql; -public class MsSqlIntegrationTests +class MsSqlIntegrationTests { [Test] public async Task MsSql_ConnectsAndRunsQuery() { await WslcTest.SkipIfUnavailableAsync(); - await using MsSqlContainer sqlServer = new MsSqlBuilder() + await using var sqlServer = new MsSqlBuilder() .WithPassword("SomeStrong!Password1") .AcceptLicense() .Build(); await sqlServer.StartAsync(); - await using SqlConnection connection = new SqlConnection(sqlServer.GetConnectionString()); + await using SqlConnection connection = new(sqlServer.GetConnectionString()); await connection.OpenAsync(); - await using SqlCommand command = new SqlCommand("SELECT 1", connection); - object result = await command.ExecuteScalarAsync(); + await using SqlCommand command = new("SELECT 1", connection); + var result = await command.ExecuteScalarAsync(); await Assert.That(Convert.ToInt32(result, System.Globalization.CultureInfo.InvariantCulture)).IsEqualTo(1); } @@ -33,15 +29,15 @@ public async Task ConnectionString_UsesMappedRandomPort() { await WslcTest.SkipIfUnavailableAsync(); - await using MsSqlContainer sqlServer = new MsSqlBuilder() + await using var sqlServer = new MsSqlBuilder() .WithPassword("SomeStrong!Password1") .AcceptLicense() .Build(); await sqlServer.StartAsync(); - SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(sqlServer.GetConnectionString()); - ushort port = sqlServer.GetMappedPublicPort(MsSqlBuilder.MsSqlPort); + SqlConnectionStringBuilder builder = [with(sqlServer.GetConnectionString())]; + var port = sqlServer.GetMappedPublicPort(MsSqlBuilder.MsSqlPort); await Assert.That(builder.DataSource).IsEqualTo($"127.0.0.1,{port}"); await Assert.That(port).IsGreaterThan((ushort)0); diff --git a/tests/WslContainers.MsSql.IntegrationTests/WslContainers.MsSql.IntegrationTests.csproj b/src/tests/MsSql.UnitTests/MsSql.UnitTests.csproj similarity index 100% rename from tests/WslContainers.MsSql.IntegrationTests/WslContainers.MsSql.IntegrationTests.csproj rename to src/tests/MsSql.UnitTests/MsSql.UnitTests.csproj diff --git a/tests/WslContainers.MsSql.UnitTests/MsSqlBuilderTests.cs b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs similarity index 67% rename from tests/WslContainers.MsSql.UnitTests/MsSqlBuilderTests.cs rename to src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs index cfb2646..4915107 100644 --- a/tests/WslContainers.MsSql.UnitTests/MsSqlBuilderTests.cs +++ b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs @@ -1,16 +1,13 @@ -using Purview.WslContainers; -using Purview.WslContainers.MsSql; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Runtime; -namespace WslContainers.MsSql.UnitTests; +namespace Purview.WslContainers.MsSql; -public class MsSqlBuilderTests +class MsSqlBuilderTests { [Test] public async Task Build_WithoutAcceptLicense_Throws() { - MsSqlBuilder builder = new MsSqlBuilder().WithPassword("SomeStrong!Password1"); + var builder = new MsSqlBuilder().WithPassword("SomeStrong!Password1"); await Assert.That(() => builder.Build()).Throws(); } @@ -18,7 +15,7 @@ public async Task Build_WithoutAcceptLicense_Throws() [Test] public async Task Build_WithWeakPassword_Throws() { - MsSqlBuilder builder = new MsSqlBuilder().WithPassword("short").AcceptLicense(); + var builder = new MsSqlBuilder().WithPassword("short").AcceptLicense(); await Assert.That(() => builder.Build()).Throws(); } @@ -26,21 +23,21 @@ public async Task Build_WithWeakPassword_Throws() [Test] public async Task BuildConfig_AppliesDefaults() { - MsSqlConfiguration configuration = new MsSqlBuilder().BuildConfigurationForTesting(); + var configuration = new MsSqlBuilder().BuildConfigurationForTesting(); 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.AcceptLicense).IsFalse(); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(1); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(1); await Assert.That(configuration.PortBindings[0].ContainerPort).IsEqualTo((ushort)1433); await Assert.That(configuration.PortBindings[0].AssignRandomHostPort).IsTrue(); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); } [Test] public async Task BuildConfig_HonoursModuleSetters() { - MsSqlConfiguration configuration = new MsSqlBuilder() + var configuration = new MsSqlBuilder() .WithPassword("SomeStrong!Password1") .AcceptLicense() .BuildConfigurationForTesting(); diff --git a/tests/WslContainers.MySql.IntegrationTests/WslContainers.MySql.IntegrationTests.csproj b/src/tests/MySql.IntegrationTests/MySql.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.MySql.IntegrationTests/WslContainers.MySql.IntegrationTests.csproj rename to src/tests/MySql.IntegrationTests/MySql.IntegrationTests.csproj diff --git a/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs b/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs new file mode 100644 index 0000000..6ddae54 --- /dev/null +++ b/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs @@ -0,0 +1,44 @@ +using MySqlConnector; + +namespace Purview.WslContainers.MySql; + +class MySqlIntegrationTests +{ + [Test] + public async Task MySql_ConnectsAndRunsQuery() + { + await WslcTest.SkipIfUnavailableAsync(); + + await using var mySql = new MySqlBuilder() + .WithDatabase("integration") + .WithUsername("test") + .WithPassword("test") + .Build(); + + await mySql.StartAsync(); + + await using MySqlConnection connection = new(mySql.GetConnectionString()); + await connection.OpenAsync(); + await using MySqlCommand command = new("SELECT 1", connection); + var result = await command.ExecuteScalarAsync(); + + await Assert.That(Convert.ToInt32(result, System.Globalization.CultureInfo.InvariantCulture)).IsEqualTo(1); + } + + [Test] + public async Task ConnectionString_UsesMappedRandomPort() + { + await WslcTest.SkipIfUnavailableAsync(); + + await using var mySql = new MySqlBuilder().Build(); + + await mySql.StartAsync(); + + MySqlConnectionStringBuilder builder = [with(mySql.GetConnectionString())]; + var port = mySql.GetMappedPublicPort(MySqlBuilder.MySqlPort); + + await Assert.That(builder.Server).IsEqualTo("127.0.0.1"); + await Assert.That(builder.Port).IsEqualTo(port); + await Assert.That(builder.Database).IsEqualTo("test"); + } +} diff --git a/tests/WslContainers.MsSql.UnitTests/WslContainers.MsSql.UnitTests.csproj b/src/tests/MySql.UnitTests/MySql.UnitTests.csproj similarity index 100% rename from tests/WslContainers.MsSql.UnitTests/WslContainers.MsSql.UnitTests.csproj rename to src/tests/MySql.UnitTests/MySql.UnitTests.csproj diff --git a/tests/WslContainers.MySql.UnitTests/MySqlBuilderTests.cs b/src/tests/MySql.UnitTests/MySqlBuilderTests.cs similarity index 62% rename from tests/WslContainers.MySql.UnitTests/MySqlBuilderTests.cs rename to src/tests/MySql.UnitTests/MySqlBuilderTests.cs index fb4ab81..c978e85 100644 --- a/tests/WslContainers.MySql.UnitTests/MySqlBuilderTests.cs +++ b/src/tests/MySql.UnitTests/MySqlBuilderTests.cs @@ -1,16 +1,13 @@ -using Purview.WslContainers; -using Purview.WslContainers.MySql; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Runtime; -namespace WslContainers.MySql.UnitTests; +namespace Purview.WslContainers.MySql; -public class MySqlBuilderTests +class MySqlBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() { - MySqlConfiguration configuration = new MySqlBuilder().BuildConfigurationForTesting(); + var configuration = new MySqlBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/library/mysql:8"); await Assert.That(configuration.Database).IsEqualTo("test"); @@ -19,15 +16,15 @@ public async Task BuildConfig_AppliesDefaults() await Assert.That(configuration.Environment["MYSQL_DATABASE"]).IsEqualTo("test"); await Assert.That(configuration.Environment["MYSQL_USER"]).IsEqualTo("test"); await Assert.That(configuration.Environment["MYSQL_PASSWORD"]).IsEqualTo("test"); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(1); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(1); await Assert.That(configuration.PortBindings[0].ContainerPort).IsEqualTo((ushort)3306); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); } [Test] public async Task BuildConfig_EmptyPassword_Throws() { - MySqlBuilder builder = new MySqlBuilder().WithPassword(string.Empty); + var builder = new MySqlBuilder().WithPassword(string.Empty); await Assert.That(() => builder.Build()).Throws(); } diff --git a/tests/WslContainers.Nats.IntegrationTests/WslContainers.Nats.IntegrationTests.csproj b/src/tests/Nats.IntegrationTests/Nats.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.Nats.IntegrationTests/WslContainers.Nats.IntegrationTests.csproj rename to src/tests/Nats.IntegrationTests/Nats.IntegrationTests.csproj diff --git a/tests/WslContainers.Nats.IntegrationTests/NatsIntegrationTests.cs b/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs similarity index 54% rename from tests/WslContainers.Nats.IntegrationTests/NatsIntegrationTests.cs rename to src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs index d48f53c..6bb1982 100644 --- a/tests/WslContainers.Nats.IntegrationTests/NatsIntegrationTests.cs +++ b/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs @@ -1,25 +1,21 @@ using NATS.Client.Core; -using Purview.WslContainers.Nats; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -namespace WslContainers.Nats.IntegrationTests; +namespace Purview.WslContainers.Nats; -public class NatsIntegrationTests +class NatsIntegrationTests { [Test] public async Task Nats_RespondsToPing() { await WslcTest.SkipIfUnavailableAsync(); - await using NatsContainer nats = new NatsBuilder().Build(); + await using var nats = new NatsBuilder().Build(); await nats.StartAsync(); await WslcTest.WaitForTcpAsync(nats.GetMappedPublicPort(NatsBuilder.ClientPort), TimeSpan.FromSeconds(30)); - await using NatsConnection client = new NatsConnection(new NatsOpts { Url = nats.GetConnectionString() }); - TimeSpan roundTrip = await client.PingAsync(); + await using NatsConnection client = new(new NatsOpts { Url = nats.GetConnectionString() }); + var roundTrip = await client.PingAsync(); await Assert.That(roundTrip).IsGreaterThan(TimeSpan.Zero); } @@ -29,12 +25,12 @@ public async Task Endpoints_UseMappedRandomPorts() { await WslcTest.SkipIfUnavailableAsync(); - await using NatsContainer nats = new NatsBuilder().Build(); + await using var nats = new NatsBuilder().Build(); await nats.StartAsync(); - ushort clientPort = nats.GetMappedPublicPort(NatsBuilder.ClientPort); - ushort monitoringPort = nats.GetMappedPublicPort(NatsBuilder.MonitoringPort); + var clientPort = nats.GetMappedPublicPort(NatsBuilder.ClientPort); + var monitoringPort = nats.GetMappedPublicPort(NatsBuilder.MonitoringPort); await Assert.That(nats.GetClientEndpoint().Host).IsEqualTo("127.0.0.1"); await Assert.That(nats.GetClientEndpoint().Port).IsEqualTo(clientPort); diff --git a/tests/WslContainers.MySql.UnitTests/WslContainers.MySql.UnitTests.csproj b/src/tests/Nats.UnitTests/Nats.UnitTests.csproj similarity index 100% rename from tests/WslContainers.MySql.UnitTests/WslContainers.MySql.UnitTests.csproj rename to src/tests/Nats.UnitTests/Nats.UnitTests.csproj diff --git a/tests/WslContainers.Nats.UnitTests/NatsBuilderTests.cs b/src/tests/Nats.UnitTests/NatsBuilderTests.cs similarity index 52% rename from tests/WslContainers.Nats.UnitTests/NatsBuilderTests.cs rename to src/tests/Nats.UnitTests/NatsBuilderTests.cs index 722a2be..50f2884 100644 --- a/tests/WslContainers.Nats.UnitTests/NatsBuilderTests.cs +++ b/src/tests/Nats.UnitTests/NatsBuilderTests.cs @@ -1,22 +1,19 @@ -using Purview.WslContainers; -using Purview.WslContainers.Nats; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Waiting; -namespace WslContainers.Nats.UnitTests; +namespace Purview.WslContainers.Nats; -public class NatsBuilderTests +class NatsBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() { - NatsConfiguration configuration = new NatsBuilder().BuildConfigurationForTesting(); + var configuration = new NatsBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/library/nats:2"); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(2); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(2); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 4222 && p.AssignRandomHostPort); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 8222 && p.AssignRandomHostPort); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); } } diff --git a/tests/WslContainers.Nats.UnitTests/WslContainers.Nats.UnitTests.csproj b/src/tests/PostgreSql.IntegrationTests/PostgreSql.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.Nats.UnitTests/WslContainers.Nats.UnitTests.csproj rename to src/tests/PostgreSql.IntegrationTests/PostgreSql.IntegrationTests.csproj diff --git a/tests/WslContainers.PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs b/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs similarity index 55% rename from tests/WslContainers.PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs rename to src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs index 456a13e..f12c7de 100644 --- a/tests/WslContainers.PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs +++ b/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs @@ -1,20 +1,16 @@ using Npgsql; -using Purview.WslContainers; -using Purview.WslContainers.PostgreSql; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using PurviewContainerState = Purview.WslContainers.Containers.ContainerState; -namespace WslContainers.PostgreSql.IntegrationTests; +namespace Purview.WslContainers.PostgreSql; -public class PostgreSqlIntegrationTests +class PostgreSqlIntegrationTests { [Test] public async Task PostgreSql_ConnectsAndRunsQuery() { await WslcTest.SkipIfUnavailableAsync(); - await using PostgreSqlContainer postgres = new PostgreSqlBuilder() + await using var postgres = new PostgreSqlBuilder() .WithDatabase("integration") .WithUsername("postgres") .WithPassword("postgres") @@ -22,10 +18,10 @@ public async Task PostgreSql_ConnectsAndRunsQuery() await postgres.StartAsync(); - await using NpgsqlConnection connection = new NpgsqlConnection(postgres.GetConnectionString()); + await using NpgsqlConnection connection = new(postgres.GetConnectionString()); await connection.OpenAsync(); - await using NpgsqlCommand command = new NpgsqlCommand("SELECT 1", connection); - object result = await command.ExecuteScalarAsync(); + await using NpgsqlCommand command = new("SELECT 1", connection); + var result = await command.ExecuteScalarAsync(); await Assert.That(Convert.ToInt32(result, System.Globalization.CultureInfo.InvariantCulture)).IsEqualTo(1); } @@ -35,7 +31,7 @@ public async Task ConnectionString_UsesMappedRandomPort() { await WslcTest.SkipIfUnavailableAsync(); - await using PostgreSqlContainer postgres = new PostgreSqlBuilder() + await using var postgres = new PostgreSqlBuilder() .WithDatabase("integration") .WithUsername("postgres") .WithPassword("postgres") @@ -43,7 +39,7 @@ public async Task ConnectionString_UsesMappedRandomPort() await postgres.StartAsync(); - NpgsqlConnectionStringBuilder builder = new NpgsqlConnectionStringBuilder(postgres.GetConnectionString()); + NpgsqlConnectionStringBuilder builder = [with(postgres.GetConnectionString())]; await Assert.That(builder.Host).IsEqualTo("localhost"); await Assert.That(builder.Database).IsEqualTo("integration"); @@ -57,10 +53,10 @@ public async Task StartAsync_WaitsForPostgresReadiness() { await WslcTest.SkipIfUnavailableAsync(); - await using PostgreSqlContainer postgres = new PostgreSqlBuilder().Build(); + await using var postgres = new PostgreSqlBuilder().Build(); await postgres.StartAsync(); - await Assert.That(postgres.State).IsEqualTo(ContainerState.Running); + await Assert.That(postgres.State).IsEqualTo(PurviewContainerState.Running); } } diff --git a/tests/WslContainers.PostgreSql.IntegrationTests/WslContainers.PostgreSql.IntegrationTests.csproj b/src/tests/PostgreSql.UnitTests/PostgreSql.UnitTests.csproj similarity index 100% rename from tests/WslContainers.PostgreSql.IntegrationTests/WslContainers.PostgreSql.IntegrationTests.csproj rename to src/tests/PostgreSql.UnitTests/PostgreSql.UnitTests.csproj diff --git a/tests/WslContainers.PostgreSql.UnitTests/PostgreSqlBuilderTests.cs b/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs similarity index 74% rename from tests/WslContainers.PostgreSql.UnitTests/PostgreSqlBuilderTests.cs rename to src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs index 313d1cc..36befbf 100644 --- a/tests/WslContainers.PostgreSql.UnitTests/PostgreSqlBuilderTests.cs +++ b/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs @@ -1,16 +1,15 @@ -using Purview.WslContainers; -using Purview.WslContainers.PostgreSql; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; -namespace WslContainers.PostgreSql.UnitTests; +namespace Purview.WslContainers.PostgreSql; -public class PostgreSqlBuilderTests +class PostgreSqlBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() { - PostgreSqlConfiguration configuration = new PostgreSqlBuilder().BuildConfigurationForTesting(); + var configuration = new PostgreSqlBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/library/postgres:17"); await Assert.That(configuration.Database).IsEqualTo("postgres"); @@ -19,17 +18,17 @@ public async Task BuildConfig_AppliesDefaults() await Assert.That(configuration.Environment["POSTGRES_DB"]).IsEqualTo("postgres"); await Assert.That(configuration.Environment["POSTGRES_USER"]).IsEqualTo("postgres"); await Assert.That(configuration.Environment["POSTGRES_PASSWORD"]).IsEqualTo("postgres"); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(1); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(1); await Assert.That(configuration.PortBindings[0].ContainerPort).IsEqualTo((ushort)5432); await Assert.That(configuration.PortBindings[0].AssignRandomHostPort).IsTrue(); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); } [Test] public async Task BuildConfig_HonoursModuleSetters() { - PostgreSqlConfiguration configuration = new PostgreSqlBuilder() + var configuration = new PostgreSqlBuilder() .WithDatabase("integration") .WithUsername("app") .WithPassword("s3cret") @@ -46,7 +45,7 @@ public async Task BuildConfig_HonoursModuleSetters() [Test] public async Task BuildConfig_EmptyPassword_Throws() { - PostgreSqlBuilder builder = new PostgreSqlBuilder().WithPassword(string.Empty); + var builder = new PostgreSqlBuilder().WithPassword(string.Empty); await Assert.That(() => builder.Build()).Throws(); } @@ -54,7 +53,7 @@ public async Task BuildConfig_EmptyPassword_Throws() [Test] public async Task ConfigurationToString_RedactsPasswordAndEnvironment() { - PostgreSqlConfiguration configuration = new PostgreSqlConfiguration + PostgreSqlConfiguration configuration = new() { Image = "postgres:17", Environment = new Dictionary @@ -65,7 +64,7 @@ public async Task ConfigurationToString_RedactsPasswordAndEnvironment() Password = Secret.From("s3cret-password"), }; - string rendered = configuration.ToString(); + var rendered = configuration.ToString(); await Assert.That(rendered).DoesNotContain("s3cret-password"); await Assert.That(rendered).Contains("postgres"); diff --git a/tests/WslContainers.RabbitMq.IntegrationTests/WslContainers.RabbitMq.IntegrationTests.csproj b/src/tests/RabbitMq.IntegrationTests/RabbitMq.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.RabbitMq.IntegrationTests/WslContainers.RabbitMq.IntegrationTests.csproj rename to src/tests/RabbitMq.IntegrationTests/RabbitMq.IntegrationTests.csproj diff --git a/tests/WslContainers.RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs b/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs similarity index 57% rename from tests/WslContainers.RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs rename to src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs index bb638bb..1df8262 100644 --- a/tests/WslContainers.RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs +++ b/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs @@ -1,29 +1,26 @@ using System.Text; -using Purview.WslContainers.RabbitMq; -using Purview.WslContainers.Testing; using RabbitMQ.Client; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -namespace WslContainers.RabbitMq.IntegrationTests; +namespace Purview.WslContainers.RabbitMq; -public class RabbitMqIntegrationTests +class RabbitMqIntegrationTests { [Test] public async Task RabbitMq_PublishesAndConsumes() { await WslcTest.SkipIfUnavailableAsync(); - await using RabbitMqContainer rabbit = new RabbitMqBuilder() + await using var rabbit = new RabbitMqBuilder() .WithUsername("guest") .WithPassword("guest") .Build(); await rabbit.StartAsync(); - ConnectionFactory factory = new ConnectionFactory { Uri = rabbit.GetAmqpEndpoint() }; - using IConnection connection = await factory.CreateConnectionAsync(); - using IChannel channel = await connection.CreateChannelAsync(); + ConnectionFactory factory = new() + { Uri = rabbit.GetAmqpEndpoint() }; + using var connection = await factory.CreateConnectionAsync(); + using var channel = await connection.CreateChannelAsync(); const string queue = "wslc-test-queue"; await channel.QueueDeclareAsync(queue, durable: false, exclusive: false, autoDelete: true); @@ -33,7 +30,7 @@ await channel.BasicPublishAsync( body: Encoding.UTF8.GetBytes("hello-rabbitmq") ); - BasicGetResult? result = await channel.BasicGetAsync(queue, autoAck: true); + var result = await channel.BasicGetAsync(queue, autoAck: true); await Assert.That(result).IsNotNull(); await Assert.That(Encoding.UTF8.GetString(result!.Body.ToArray())).IsEqualTo("hello-rabbitmq"); @@ -44,12 +41,12 @@ public async Task Endpoints_UseMappedRandomPorts() { await WslcTest.SkipIfUnavailableAsync(); - await using RabbitMqContainer rabbit = new RabbitMqBuilder().Build(); + await using var rabbit = new RabbitMqBuilder().Build(); await rabbit.StartAsync(); - ushort amqpPort = rabbit.GetMappedPublicPort(RabbitMqBuilder.AmqpPort); - ushort managementPort = rabbit.GetMappedPublicPort(RabbitMqBuilder.ManagementPort); + var amqpPort = rabbit.GetMappedPublicPort(RabbitMqBuilder.AmqpPort); + var managementPort = rabbit.GetMappedPublicPort(RabbitMqBuilder.ManagementPort); await Assert.That(rabbit.GetAmqpEndpoint().ToString()).IsEqualTo($"amqp://guest:guest@localhost:{amqpPort}/"); await Assert.That(rabbit.GetManagementEndpoint().ToString()).IsEqualTo($"http://localhost:{managementPort}/"); diff --git a/tests/WslContainers.PostgreSql.UnitTests/WslContainers.PostgreSql.UnitTests.csproj b/src/tests/RabbitMq.UnitTests/RabbitMq.UnitTests.csproj similarity index 100% rename from tests/WslContainers.PostgreSql.UnitTests/WslContainers.PostgreSql.UnitTests.csproj rename to src/tests/RabbitMq.UnitTests/RabbitMq.UnitTests.csproj diff --git a/tests/WslContainers.RabbitMq.UnitTests/RabbitMqBuilderTests.cs b/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs similarity index 71% rename from tests/WslContainers.RabbitMq.UnitTests/RabbitMqBuilderTests.cs rename to src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs index 2f98668..e90e2e0 100644 --- a/tests/WslContainers.RabbitMq.UnitTests/RabbitMqBuilderTests.cs +++ b/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs @@ -1,32 +1,30 @@ -using Purview.WslContainers; -using Purview.WslContainers.RabbitMq; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; -namespace WslContainers.RabbitMq.UnitTests; +namespace Purview.WslContainers.RabbitMq; -public class RabbitMqBuilderTests +class RabbitMqBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() { - RabbitMqConfiguration configuration = new RabbitMqBuilder().BuildConfigurationForTesting(); + var configuration = new RabbitMqBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/library/rabbitmq:3-management"); await Assert.That(configuration.Username).IsEqualTo("guest"); await Assert.That(configuration.Password.Value).IsEqualTo("guest"); await Assert.That(configuration.VirtualHost).IsEqualTo("/"); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(2); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(2); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 5672); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 15672); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); } [Test] public async Task BuildConfig_HonoursModuleSetters() { - RabbitMqConfiguration configuration = new RabbitMqBuilder() + var configuration = new RabbitMqBuilder() .WithUsername("app") .WithPassword("s3cret") .WithVirtualHost("myvhost") @@ -43,7 +41,7 @@ public async Task BuildConfig_HonoursModuleSetters() [Test] public async Task BuildConfig_EmptyPassword_Throws() { - RabbitMqBuilder builder = new RabbitMqBuilder().WithPassword(string.Empty); + var builder = new RabbitMqBuilder().WithPassword(string.Empty); await Assert.That(() => builder.Build()).Throws(); } diff --git a/tests/WslContainers.Redis.IntegrationTests/WslContainers.Redis.IntegrationTests.csproj b/src/tests/Redis.IntegrationTests/Redis.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.Redis.IntegrationTests/WslContainers.Redis.IntegrationTests.csproj rename to src/tests/Redis.IntegrationTests/Redis.IntegrationTests.csproj diff --git a/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs b/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs new file mode 100644 index 0000000..344cdb3 --- /dev/null +++ b/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs @@ -0,0 +1,40 @@ +using StackExchange.Redis; + +namespace Purview.WslContainers.Redis; + +class RedisIntegrationTests +{ + [Test] + public async Task Redis_RespondsToPing() + { + await WslcTest.SkipIfUnavailableAsync(); + + await using var redis = new RedisBuilder().Build(); + + await redis.StartAsync(); + + await using var connection = await ConnectionMultiplexer.ConnectAsync( + redis.GetConnectionString() + ); + var db = connection.GetDatabase(); + var pong = await db.ExecuteAsync("PING"); + + await Assert.That(pong.ToString()).IsEqualTo("PONG"); + } + + [Test] + public async Task ConnectionString_UsesMappedRandomPort() + { + await WslcTest.SkipIfUnavailableAsync(); + + await using var redis = new RedisBuilder().Build(); + + await redis.StartAsync(); + + var connectionString = redis.GetConnectionString(); + var port = redis.GetMappedPublicPort(RedisBuilder.RedisPort); + + await Assert.That(connectionString).IsEqualTo($"localhost:{port}"); + await Assert.That(port).IsNotEqualTo(RedisBuilder.RedisPort); + } +} diff --git a/tests/WslContainers.RabbitMq.UnitTests/WslContainers.RabbitMq.UnitTests.csproj b/src/tests/Redis.UnitTests/Redis.UnitTests.csproj similarity index 100% rename from tests/WslContainers.RabbitMq.UnitTests/WslContainers.RabbitMq.UnitTests.csproj rename to src/tests/Redis.UnitTests/Redis.UnitTests.csproj diff --git a/tests/WslContainers.Redis.UnitTests/RedisBuilderTests.cs b/src/tests/Redis.UnitTests/RedisBuilderTests.cs similarity index 54% rename from tests/WslContainers.Redis.UnitTests/RedisBuilderTests.cs rename to src/tests/Redis.UnitTests/RedisBuilderTests.cs index fa14446..e962d00 100644 --- a/tests/WslContainers.Redis.UnitTests/RedisBuilderTests.cs +++ b/src/tests/Redis.UnitTests/RedisBuilderTests.cs @@ -1,29 +1,26 @@ -using Purview.WslContainers; -using Purview.WslContainers.Redis; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Waiting; -namespace WslContainers.Redis.UnitTests; +namespace Purview.WslContainers.Redis; -public class RedisBuilderTests +class RedisBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() { - RedisConfiguration configuration = new RedisBuilder().BuildConfigurationForTesting(); + var configuration = new RedisBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/library/redis:7"); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(1); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(1); await Assert.That(configuration.PortBindings[0].ContainerPort).IsEqualTo((ushort)6379); await Assert.That(configuration.PortBindings[0].AssignRandomHostPort).IsTrue(); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); } [Test] public async Task BuildConfig_SupportsCompatibleImages() { - RedisConfiguration configuration = new RedisBuilder("valkey/valkey:7").BuildConfigurationForTesting(); + var configuration = new RedisBuilder("valkey/valkey:7").BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("docker.io/valkey/valkey:7"); } @@ -31,11 +28,11 @@ public async Task BuildConfig_SupportsCompatibleImages() [Test] public async Task BuildConfig_PreservesCustomWaitStrategy() { - RedisConfiguration configuration = new RedisBuilder() + var configuration = new RedisBuilder() .WithWaitStrategy(Wait.ForTcpPort(6379)) .BuildConfigurationForTesting(); - await Assert.That(configuration.WaitStrategies).HasCount().EqualTo(1); + await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); } } diff --git a/tests/SharedTestingFramework/SharedTestingFramework.csproj b/src/tests/SharedTestingFramework/SharedTestingFramework.csproj similarity index 100% rename from tests/SharedTestingFramework/SharedTestingFramework.csproj rename to src/tests/SharedTestingFramework/SharedTestingFramework.csproj diff --git a/tests/SharedTestingFramework/WslcTest.cs b/src/tests/SharedTestingFramework/WslcTest.cs similarity index 68% rename from tests/SharedTestingFramework/WslcTest.cs rename to src/tests/SharedTestingFramework/WslcTest.cs index b7e40df..f7f8e7d 100644 --- a/tests/SharedTestingFramework/WslcTest.cs +++ b/src/tests/SharedTestingFramework/WslcTest.cs @@ -1,7 +1,6 @@ -using Purview.WslContainers; using TUnit.Core.Exceptions; -namespace Purview.WslContainers.Testing; +namespace Purview.WslContainers; /// Shared helpers for integration tests that exercise real WSL Containers. public static class WslcTest @@ -9,7 +8,7 @@ public static class WslcTest /// Skips the current test when WSL Containers prerequisites are missing. public static async Task SkipIfUnavailableAsync() { - WslContainerRuntimeInfo info = await WslContainerRuntime.Instance.GetInfoAsync(); + var info = await WslContainerRuntime.Instance.GetInfoAsync(); if (!info.IsAvailable || !info.IsCompatible) { throw new SkipTestException( @@ -21,8 +20,8 @@ public static async Task SkipIfUnavailableAsync() /// Connects to a host TCP port, throwing when the timeout elapses. public static async Task WaitForTcpAsync(int port, TimeSpan timeout) { - using System.Net.Sockets.TcpClient client = new System.Net.Sockets.TcpClient(); - using System.Threading.CancellationTokenSource timeoutSource = new System.Threading.CancellationTokenSource( + using System.Net.Sockets.TcpClient client = new(); + using CancellationTokenSource timeoutSource = new( timeout ); await client.ConnectAsync(System.Net.IPAddress.Loopback, port, timeoutSource.Token); diff --git a/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs b/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs similarity index 74% rename from tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs rename to src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs index abb7630..7859e65 100644 --- a/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs +++ b/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs @@ -1,18 +1,15 @@ -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Containers; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class AlpineLifecycleTests +class AlpineLifecycleTests { [Test] public async Task EchoContainer_ProducesOutputAndCleansUp() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/echo", "hello-from-alpine") .Build(); @@ -20,7 +17,7 @@ public async Task EchoContainer_ProducesOutputAndCleansUp() await container.StartAsync(); await Assert.That(container.State).IsNotEqualTo(ContainerState.Invalid); - string logs = await container.GetLogsAsync(); + var logs = await container.GetLogsAsync(); await Assert.That(logs).Contains("hello-from-alpine"); } @@ -29,7 +26,7 @@ public async Task EnvironmentVariable_IsPassedToInitProcess() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithEnvironment("GREETING", "hello-env") .WithCommand("/bin/sh", "-c", "echo $GREETING") @@ -37,7 +34,7 @@ public async Task EnvironmentVariable_IsPassedToInitProcess() await container.StartAsync(); - string logs = await container.GetLogsAsync(); + var logs = await container.GetLogsAsync(); await Assert.That(logs).Contains("hello-env"); } @@ -46,7 +43,7 @@ public async Task StopAsync_ThenDisposeAsync_IsIdempotent() { await WslcTest.SkipIfUnavailableAsync(); - WslContainer container = new ContainerBuilder() + var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .Build(); @@ -67,7 +64,7 @@ public async Task GetMappedPublicPort_BeforeStart_Throws() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/echo", "hi") .Build(); diff --git a/tests/WslContainers.IntegrationTests/CleanupTests.cs b/src/tests/WslContainers.IntegrationTests/CleanupTests.cs similarity index 77% rename from tests/WslContainers.IntegrationTests/CleanupTests.cs rename to src/tests/WslContainers.IntegrationTests/CleanupTests.cs index 983b36b..57a5678 100644 --- a/tests/WslContainers.IntegrationTests/CleanupTests.cs +++ b/src/tests/WslContainers.IntegrationTests/CleanupTests.cs @@ -1,18 +1,16 @@ -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class CleanupTests +class CleanupTests { [Test] public async Task Dispose_AfterWaitTimeout_IsIdempotent() { await WslcTest.SkipIfUnavailableAsync(); - WslContainer container = new ContainerBuilder() + var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sh", "-c", "echo booting; sleep 300") .WithWaitStrategy(Wait.ForLogMessage("this never appears").WithTimeout(TimeSpan.FromSeconds(3))) @@ -28,7 +26,7 @@ public async Task Dispose_AfterInvalidImage_DoesNotThrow() { await WslcTest.SkipIfUnavailableAsync(); - WslContainer container = new ContainerBuilder() + var container = new ContainerBuilder() .WithImage("docker.io/library/definitely-not-a-real-image-xyz:latest") .Build(); @@ -41,7 +39,7 @@ public async Task Stop_ThenDispose_IsIdempotent() { await WslcTest.SkipIfUnavailableAsync(); - WslContainer container = new ContainerBuilder() + var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .Build(); @@ -57,7 +55,7 @@ public async Task ImmediateExit_ThenDispose_Works() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/echo", "bye") .Build(); diff --git a/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs b/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs similarity index 71% rename from tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs rename to src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs index 51dcb1b..bf632f1 100644 --- a/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs +++ b/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs @@ -1,11 +1,8 @@ -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Waiting; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class ConcurrencyStressTests +class ConcurrencyStressTests { [Test] public async Task ConcurrentContainers_GetDistinctUsablePorts() @@ -13,10 +10,10 @@ public async Task ConcurrentContainers_GetDistinctUsablePorts() await WslcTest.SkipIfUnavailableAsync(); const int count = 5; - List containers = new List(count); + List containers = [with(count)]; try { - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { containers.Add( new ContainerBuilder() @@ -39,7 +36,7 @@ public async Task ConcurrentContainers_GetDistinctUsablePorts() } finally { - foreach (WslContainer container in containers) + foreach (var container in containers) { await container.DisposeAsync(); } diff --git a/tests/WslContainers.IntegrationTests/ExecTests.cs b/src/tests/WslContainers.IntegrationTests/ExecTests.cs similarity index 66% rename from tests/WslContainers.IntegrationTests/ExecTests.cs rename to src/tests/WslContainers.IntegrationTests/ExecTests.cs index 458cb4b..b388d07 100644 --- a/tests/WslContainers.IntegrationTests/ExecTests.cs +++ b/src/tests/WslContainers.IntegrationTests/ExecTests.cs @@ -1,24 +1,21 @@ -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Containers; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class ExecTests +class ExecTests { [Test] public async Task Exec_ReturnsStdoutAndExitCode() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .Build(); await container.StartAsync(); - ExecResult result = await container.ExecAsync(new[] { "/bin/echo", "hello-exec" }); + var result = await container.ExecAsync(["/bin/echo", "hello-exec"]); await Assert.That(result.ExitCode).IsEqualTo(0); await Assert.That(result.Stdout.Trim()).IsEqualTo("hello-exec"); @@ -30,14 +27,14 @@ public async Task Exec_PropagatesEnvironmentAndWorkingDirectory() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .Build(); await container.StartAsync(); - ExecResult result = await container.ExecAsync( - new[] { "/bin/sh", "-c", "echo $FOO; pwd" }, + var result = await container.ExecAsync( + ["/bin/sh", "-c", "echo $FOO; pwd"], new ExecOptions { Environment = new Dictionary { ["FOO"] = "bar" }, @@ -54,13 +51,13 @@ public async Task Exec_ReturnsNonZeroExitCodeAndStderr() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .Build(); await container.StartAsync(); - ExecResult result = await container.ExecAsync(new[] { "/bin/sh", "-c", "echo boom >&2; exit 9" }); + var result = await container.ExecAsync(["/bin/sh", "-c", "echo boom >&2; exit 9"]); await Assert.That(result.ExitCode).IsEqualTo(9); await Assert.That(result.Stderr.Trim()).IsEqualTo("boom"); @@ -71,14 +68,14 @@ public async Task Exec_TimesOut_AndKillsProcess() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .Build(); await container.StartAsync(); - ExecResult result = await container.ExecAsync( - new[] { "/bin/sleep", "60" }, + var result = await container.ExecAsync( + ["/bin/sleep", "60"], new ExecOptions { Timeout = TimeSpan.FromSeconds(2) } ); diff --git a/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs b/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs new file mode 100644 index 0000000..ac77ee7 --- /dev/null +++ b/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; + +namespace Purview.WslContainers; + +class PerformanceTests +{ + [Test] + [NotInParallel] + public async Task WarmContainerStart_IsFast(CancellationToken cancellationToken) + { + await WslcTest.SkipIfUnavailableAsync(); + + // Warm the image into the shared session store. + await using ( + var warm = new ContainerBuilder() + .WithImage("alpine:latest") + .WithCommand("/bin/echo", "warm") + .Build() + ) + { + await warm.StartAsync(cancellationToken); + } + + var stopwatch = Stopwatch.StartNew(); + await using var container = new ContainerBuilder() + .WithImage("alpine:latest") + .WithCommand("/bin/echo", "hi") + .Build(); + await container.StartAsync(cancellationToken); + stopwatch.Stop(); + + await TestContext.Current!.OutputWriter.WriteLineAsync($"warm container start: {stopwatch.Elapsed}", cancellationToken); + await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromSeconds(60)); + } +} diff --git a/tests/WslContainers.IntegrationTests/PortMappingTests.cs b/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs similarity index 68% rename from tests/WslContainers.IntegrationTests/PortMappingTests.cs rename to src/tests/WslContainers.IntegrationTests/PortMappingTests.cs index 3aadd8e..3b53d77 100644 --- a/tests/WslContainers.IntegrationTests/PortMappingTests.cs +++ b/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs @@ -1,21 +1,17 @@ using System.Net; -using System.Net.Http; using System.Net.Sockets; -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Containers; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class PortMappingTests +class PortMappingTests { [Test] public async Task RandomHostPort_IsAssignedAndReachable() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, assignRandomHostPort: true) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") @@ -24,7 +20,7 @@ public async Task RandomHostPort_IsAssignedAndReachable() await container.StartAsync(); await Assert.That(container.State).IsEqualTo(ContainerState.Running); - ushort hostPort = container.GetMappedPublicPort(8080); + var hostPort = container.GetMappedPublicPort(8080); await Assert.That(hostPort).IsGreaterThan((ushort)0); await WslcTest.WaitForTcpAsync(hostPort, TimeSpan.FromSeconds(30)); @@ -35,8 +31,8 @@ public async Task ExplicitHostPort_IsUsed() { await WslcTest.SkipIfUnavailableAsync(); - ushort hostPort = GetFreeTcpPort(); - await using WslContainer container = new ContainerBuilder() + var hostPort = GetFreeTcpPort(); + await using var container = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, hostPort) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") @@ -53,12 +49,12 @@ public async Task MultipleContainers_ReceiveDistinctRandomPorts() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer first = new ContainerBuilder() + await using var first = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, assignRandomHostPort: true) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") .Build(); - await using WslContainer second = new ContainerBuilder() + await using var second = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, assignRandomHostPort: true) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") @@ -66,14 +62,14 @@ public async Task MultipleContainers_ReceiveDistinctRandomPorts() await Task.WhenAll(first.StartAsync(), second.StartAsync()); - ushort firstPort = first.GetMappedPublicPort(8080); - ushort secondPort = second.GetMappedPublicPort(8080); + var firstPort = first.GetMappedPublicPort(8080); + var secondPort = second.GetMappedPublicPort(8080); await Assert.That(firstPort).IsNotEqualTo(secondPort); } - private static ushort GetFreeTcpPort() + static ushort GetFreeTcpPort() { - using TcpListener listener = new TcpListener(IPAddress.Loopback, 0); + using TcpListener listener = new(IPAddress.Loopback, 0); listener.Start(); return (ushort)((IPEndPoint)listener.LocalEndpoint).Port; } diff --git a/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs b/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs new file mode 100644 index 0000000..5147ee6 --- /dev/null +++ b/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs @@ -0,0 +1,16 @@ +namespace Purview.WslContainers; + +class RuntimeInfoTests +{ + [Test] + public async Task GetInfoAsync_ReportsAvailability() + { + await WslcTest.SkipIfUnavailableAsync(); + + var info = await WslContainerRuntime.Instance.GetInfoAsync(); + + await Assert.That(info.IsAvailable).IsTrue(); + await Assert.That(info.IsCompatible).IsTrue(); + await Assert.That(info.Version).IsNotEmpty(); + } +} diff --git a/tests/WslContainers.IntegrationTests/SharedStorageTests.cs b/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs similarity index 62% rename from tests/WslContainers.IntegrationTests/SharedStorageTests.cs rename to src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs index 39da654..21bee0e 100644 --- a/tests/WslContainers.IntegrationTests/SharedStorageTests.cs +++ b/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs @@ -1,46 +1,42 @@ -using Purview.WslContainers; using Purview.WslContainers.Images; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class SharedStorageTests +class SharedStorageTests { [Test] public async Task SharedStore_IsReusedAcrossSequentialRuntimes() { await WslcTest.SkipIfUnavailableAsync(); - string path = Path.Combine(Path.GetTempPath(), "wslc-shared-" + Guid.NewGuid().ToString("N")[..6]); + var path = Path.Combine(Path.GetTempPath(), "wslc-shared-" + Guid.NewGuid().ToString("N")[..6]); Directory.CreateDirectory(path); try { IReadOnlyList firstRun; - await using (WslContainerRuntime runtime1 = new WslContainerRuntime(new WslContainerRuntimeOptions + await using (var runtime1 = new WslContainerRuntime(new WslContainerRuntimeOptions { StoragePath = path, DisableProcessExitCleanup = true, })) { - await using WslContainer container = new ContainerBuilder(runtime1) + await using var container = new ContainerBuilder(runtime1) .WithImage("alpine:latest") .WithCommand("/bin/echo", "one") .Build(); await container.StartAsync(); - IContainerSession session1 = await runtime1.GetSessionAsync(); + var session1 = await runtime1.GetSessionAsync(); firstRun = await session1.ListImagesAsync(); } IReadOnlyList secondRun; - await using (WslContainerRuntime runtime2 = new WslContainerRuntime(new WslContainerRuntimeOptions + await using (var runtime2 = new WslContainerRuntime(new WslContainerRuntimeOptions { StoragePath = path, DisableProcessExitCleanup = true, })) { - IContainerSession session2 = await runtime2.GetSessionAsync(); + var session2 = await runtime2.GetSessionAsync(); secondRun = await session2.ListImagesAsync(); } @@ -63,4 +59,4 @@ public async Task SharedStore_IsReusedAcrossSequentialRuntimes() } } } -} \ No newline at end of file +} diff --git a/src/tests/WslContainers.IntegrationTests/VolumeTests.cs b/src/tests/WslContainers.IntegrationTests/VolumeTests.cs new file mode 100644 index 0000000..c877fe1 --- /dev/null +++ b/src/tests/WslContainers.IntegrationTests/VolumeTests.cs @@ -0,0 +1,74 @@ +namespace Purview.WslContainers; + +class VolumeTests +{ + [Test] + public async Task BindMount_ReadsHostFile(CancellationToken cancellationToken) + { + await WslcTest.SkipIfUnavailableAsync(); + + var dir = Path.Combine(Path.GetTempPath(), "wslc-bind-" + Guid.NewGuid().ToString("N")[..6]); + Directory.CreateDirectory(dir); + await File.WriteAllTextAsync(Path.Combine(dir, "hello.txt"), "hello-bind\n", cancellationToken); + try + { + await using var container = new ContainerBuilder() + .WithImage("alpine:latest") + .WithBindMount(dir, "/mnt/data") + .WithCommand("/bin/cat", "/mnt/data/hello.txt") + .Build(); + + await container.StartAsync(cancellationToken); + + // The live log stream completes when the init process exits; bound it so a regression fails + // instead of hanging the suite. + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromSeconds(60)); + List logs = []; + await foreach (var log in container.GetLogsAsync(timeout.Token)) + { + logs.Add(log.Text); + } + + await Assert.That(logs.Any(text => text.Contains("hello-bind", StringComparison.Ordinal))).IsTrue(); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Test] + public async Task NamedVolume_IsSharedBetweenContainers(CancellationToken cancellationToken) + { + await WslcTest.SkipIfUnavailableAsync(); + + var volume = "wslc-vol-" + Guid.NewGuid().ToString("N")[..6]; + await using var writer = new ContainerBuilder() + .WithImage("alpine:latest") + .WithVolumeMount(volume, "/data") + .WithCommand("/bin/sh", "-c", "echo shared-data > /data/file.txt; sleep 300") + .Build(); + await writer.StartAsync(cancellationToken); + await Task.Delay(1500, cancellationToken); + + await using var reader = new ContainerBuilder() + .WithImage("alpine:latest") + .WithVolumeMount(volume, "/data") + .WithCommand("/bin/cat", "/data/file.txt") + .Build(); + await reader.StartAsync(cancellationToken); + + // The live log stream completes when the init process exits; bound it so a regression fails + // instead of hanging the suite. + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromSeconds(60)); + List logs = []; + await foreach (var log in reader.GetLogsAsync(timeout.Token)) + { + logs.Add(log.Text); + } + + await Assert.That(logs.Any(text => text.Contains("shared-data", StringComparison.Ordinal))).IsTrue(); + } +} diff --git a/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs b/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs similarity index 81% rename from tests/WslContainers.IntegrationTests/WaitStrategyTests.cs rename to src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs index 5fb1ff1..e1debf0 100644 --- a/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs +++ b/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs @@ -1,19 +1,18 @@ using System.Net; -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Containers; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; -namespace WslContainers.IntegrationTests; +namespace Purview.WslContainers; -public class WaitStrategyTests +class WaitStrategyTests { [Test] public async Task LogMessage_WaitsForReadiness() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sh", "-c", "echo ready; sleep 300") .WithWaitStrategy(Wait.ForLogMessage("ready")) @@ -29,7 +28,7 @@ public async Task TcpPort_WaitsForListener() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, assignRandomHostPort: true) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") @@ -38,7 +37,7 @@ public async Task TcpPort_WaitsForListener() await container.StartAsync(); - ushort hostPort = container.GetMappedPublicPort(8080); + var hostPort = container.GetMappedPublicPort(8080); await Assert.That(hostPort).IsGreaterThan((ushort)0); } @@ -47,7 +46,7 @@ public async Task Http_WaitsForExpectedStatus() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, assignRandomHostPort: true) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") @@ -62,7 +61,7 @@ public async Task Command_WaitsForExitCodeZero() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sleep", "300") .WithWaitStrategy(Wait.ForCommand("/bin/true")) @@ -78,7 +77,7 @@ public async Task Composition_AllStrategiesReady() { await WslcTest.SkipIfUnavailableAsync(); - await using WslContainer container = new ContainerBuilder() + await using var container = new ContainerBuilder() .WithImage("python:3-alpine") .WithPortBinding(8080, assignRandomHostPort: true) .WithCommand("/bin/sh", "-c", "python3 -m http.server 8080") @@ -95,7 +94,7 @@ public async Task Timeout_ThrowsWithDiagnosticsAndIsDisposable() { await WslcTest.SkipIfUnavailableAsync(); - WslContainer container = new ContainerBuilder() + var container = new ContainerBuilder() .WithImage("alpine:latest") .WithCommand("/bin/sh", "-c", "echo booting; sleep 300") .WithWaitStrategy( diff --git a/tests/WslContainers.Redis.UnitTests/WslContainers.Redis.UnitTests.csproj b/src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj similarity index 100% rename from tests/WslContainers.Redis.UnitTests/WslContainers.Redis.UnitTests.csproj rename to src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj diff --git a/tests/WslContainers.UnitTests/ContainerBuilderTests.cs b/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs similarity index 72% rename from tests/WslContainers.UnitTests/ContainerBuilderTests.cs rename to src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs index 578f65e..58c970f 100644 --- a/tests/WslContainers.UnitTests/ContainerBuilderTests.cs +++ b/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs @@ -1,13 +1,12 @@ -using Purview.WslContainers; using Purview.WslContainers.Images; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Networking; +using Purview.WslContainers.Runtime; -namespace WslContainers.UnitTests; +namespace Purview.WslContainers; -public class ContainerBuilderTests +class ContainerBuilderTests { - private sealed class TestBuilder : ContainerBuilder + sealed class TestBuilder : ContainerBuilder { public ContainerConfiguration BuildConfig() => BuildConfiguration(); @@ -19,10 +18,12 @@ protected override WslContainer CreateContainer(ContainerConfiguration configura } } + static readonly string[] Expected = ["sleep", "10"]; + [Test] public async Task BuildConfiguration_PopulatesAllCommonFields() { - ContainerConfiguration configuration = new TestBuilder() + var configuration = new TestBuilder() .WithImage("alpine:3.19") .WithName("unit-test") .WithHostname("unit") @@ -41,13 +42,13 @@ public async Task BuildConfiguration_PopulatesAllCommonFields() await Assert.That(configuration.Hostname).IsEqualTo("unit"); await Assert.That(configuration.Environment["A"]).IsEqualTo("1"); await Assert.That(configuration.Environment["B"]).IsEqualTo("2"); - await Assert.That(configuration.Command).IsEquivalentTo(new[] { "sleep", "10" }); + await Assert.That(configuration.Command).IsEquivalentTo(Expected); await Assert.That(configuration.WorkingDirectory).IsEqualTo("/tmp"); - await Assert.That(configuration.PortBindings).HasCount().EqualTo(2); + await Assert.That(configuration.PortBindings.Count).IsEqualTo(2); await Assert.That(configuration.PortBindings[0].AssignRandomHostPort).IsTrue(); await Assert.That(configuration.PortBindings[1].HostPort).IsEqualTo((ushort)9091); - await Assert.That(configuration.BindMounts).HasCount().EqualTo(1); - await Assert.That(configuration.NamedVolumes).HasCount().EqualTo(1); + await Assert.That(configuration.BindMounts.Count).IsEqualTo(1); + await Assert.That(configuration.NamedVolumes.Count).IsEqualTo(1); await Assert.That(configuration.NetworkingMode).IsEqualTo(ContainerNetworkingMode.Bridged); await Assert.That(configuration.PullPolicy).IsEqualTo(PullPolicy.Missing); } @@ -61,43 +62,43 @@ public async Task Build_WithoutImage_Throws() [Test] public async Task Build_WithUdpPort_ThrowsNotSupported() { - TestBuilder builder = new TestBuilder().WithImage("alpine").WithPortBinding(53, 53, PortProtocol.Udp); + var builder = new TestBuilder().WithImage("alpine").WithPortBinding(53, 53, PortProtocol.Udp); await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithContainerPortZero_Throws() { - TestBuilder builder = new TestBuilder().WithImage("alpine").WithPortBinding(0, assignRandomHostPort: true); + var builder = new TestBuilder().WithImage("alpine").WithPortBinding(0, assignRandomHostPort: true); await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithHostPortZero_Throws() { - TestBuilder builder = new TestBuilder().WithImage("alpine").WithPortBinding(80, 0); + var builder = new TestBuilder().WithImage("alpine").WithPortBinding(80, 0); await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithEmptyBindMount_Throws() { - TestBuilder builder = new TestBuilder().WithImage("alpine").WithBindMount(string.Empty, "/data"); + var builder = new TestBuilder().WithImage("alpine").WithBindMount(string.Empty, "/data"); await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task Build_WithZeroStartupTimeout_Throws() { - TestBuilder builder = new TestBuilder().WithImage("alpine").WithStartupTimeout(TimeSpan.Zero); + var builder = new TestBuilder().WithImage("alpine").WithStartupTimeout(TimeSpan.Zero); await Assert.That(() => builder.Build()).Throws(); } [Test] public async Task GenericContainerBuilder_GeneratesUniqueName() { - WslContainer first = new ContainerBuilder().WithImage("alpine").Build(); - WslContainer second = new ContainerBuilder().WithImage("alpine").Build(); + var first = new ContainerBuilder().WithImage("alpine").Build(); + var second = new ContainerBuilder().WithImage("alpine").Build(); await Assert.That(first.Name).IsNotEqualTo(second.Name); await Assert.That(first.Image).IsEqualTo("docker.io/library/alpine:latest"); @@ -117,7 +118,7 @@ await Assert [Test] public async Task WithTag_AppliesTagToConfiguredImage() { - ContainerConfiguration configuration = new TestBuilder().WithImage("redis").WithTag("7.4").BuildConfig(); + var configuration = new TestBuilder().WithImage("redis").WithTag("7.4").BuildConfig(); await Assert.That(configuration.Image).IsEqualTo("docker.io/library/redis:7.4"); } diff --git a/tests/WslContainers.UnitTests/FakeContainer.cs b/src/tests/WslContainers.UnitTests/FakeContainer.cs similarity index 88% rename from tests/WslContainers.UnitTests/FakeContainer.cs rename to src/tests/WslContainers.UnitTests/FakeContainer.cs index bfa6b1f..3eec705 100644 --- a/tests/WslContainers.UnitTests/FakeContainer.cs +++ b/src/tests/WslContainers.UnitTests/FakeContainer.cs @@ -1,8 +1,8 @@ -using Purview.WslContainers; +using Purview.WslContainers.Containers; -namespace WslContainers.UnitTests; +namespace Purview.WslContainers; -internal sealed class FakeContainer : IContainer +sealed class FakeContainer : IContainer { public string Id { get; init; } = "fake-id"; @@ -33,11 +33,12 @@ public Task ExecAsync( public ushort GetMappedPublicPort(ushort containerPort) { - if (!PortMappings.TryGetValue(containerPort, out ushort hostPort)) + if (!PortMappings.TryGetValue(containerPort, out var hostPort)) { throw new InvalidOperationException($"No mapping for {containerPort}."); } + // Return the mapped host port return hostPort; } diff --git a/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs b/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs new file mode 100644 index 0000000..1af5d81 --- /dev/null +++ b/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs @@ -0,0 +1,122 @@ +using Purview.WslContainers.Waiting; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Purview.WslContainers; + +class HttpWaitStrategyTests +{ + [Test] + public async Task Http_ReturnsTrueOnExpectedStatus(CancellationToken cancellationToken) + { + (var port, var listener, var _) = StartServer(HttpStatusCode.OK, cancellationToken); + try + { + await using FakeContainer container = new() + { + PortMappings = new Dictionary { [80] = (ushort)port }, + }; + WaitContext context = new(container, container.PortMappings, networkIp: null); + var strategy = Wait.ForHttp("/health").ForPort(80).ForStatusCode(HttpStatusCode.OK); + + await Assert.That(await strategy.UntilAsync(context, cancellationToken)).IsTrue(); + } + finally + { + listener.Stop(); + } + } + + [Test] + public async Task Http_ReturnsFalseOnUnexpectedStatus(CancellationToken cancellationToken) + { + (var port, var listener, var _) = StartServer(HttpStatusCode.InternalServerError, cancellationToken); + try + { + await using FakeContainer container = new() + { + PortMappings = new Dictionary { [80] = (ushort)port }, + }; + WaitContext context = new(container, container.PortMappings, networkIp: null); + var strategy = Wait.ForHttp("/health").ForPort(80).ForStatusCode(HttpStatusCode.OK); + + await Assert.That(await strategy.UntilAsync(context, cancellationToken)).IsFalse(); + } + finally + { + listener.Stop(); + } + } + + [Test] + public async Task Http_StatusPredicate_IsUsed(CancellationToken cancellationToken) + { + (var port, var listener, var _) = StartServer(HttpStatusCode.Accepted, cancellationToken); + try + { + await using FakeContainer container = new() + { + PortMappings = new Dictionary { [80] = (ushort)port }, + }; + WaitContext context = new(container, container.PortMappings, networkIp: null); + var strategy = Wait.ForHttp("/") + .ForPort(80) + .ForStatusPredicate(status => status is >= 200 and < 300); + + await Assert.That(await strategy.UntilAsync(context, cancellationToken)).IsTrue(); + } + finally + { + listener.Stop(); + } + } + + [Test] + public async Task Http_ReturnsFalseWhenPortNotMapped(CancellationToken cancellationToken) + { + await using FakeContainer container = new(); + WaitContext context = new(container, container.PortMappings, networkIp: null); + var strategy = Wait.ForHttp("/").ForPort(80); + + await Assert.That(await strategy.UntilAsync(context, cancellationToken)).IsFalse(); + } + + static (int Port, HttpListener Listener, Task Handler) StartServer(HttpStatusCode status, CancellationToken cancellationToken) + { + using TcpListener probe = new(IPAddress.Loopback, 0); + probe.Start(); + var port = ((IPEndPoint)probe.LocalEndpoint).Port; + probe.Stop(); + + HttpListener listener = new(); + listener.Prefixes.Add($"http://127.0.0.1:{port}/"); + listener.Start(); + + var handler = Task.Run(async () => + { + while (listener.IsListening && !cancellationToken.IsCancellationRequested) + { + try + { + var ctx = await listener.GetContextAsync(); + ctx.Response.StatusCode = (int)status; + var body = Encoding.UTF8.GetBytes("ok"); + ctx.Response.ContentLength64 = body.Length; + await ctx.Response.OutputStream.WriteAsync(body, cancellationToken); + ctx.Response.Close(); + } + catch (HttpListenerException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } + } + }, cancellationToken); + + return (port, listener, handler); + } +} diff --git a/tests/WslContainers.UnitTests/ImageTests.cs b/src/tests/WslContainers.UnitTests/ImageTests.cs similarity index 81% rename from tests/WslContainers.UnitTests/ImageTests.cs rename to src/tests/WslContainers.UnitTests/ImageTests.cs index 4c4da77..11234ea 100644 --- a/tests/WslContainers.UnitTests/ImageTests.cs +++ b/src/tests/WslContainers.UnitTests/ImageTests.cs @@ -1,15 +1,13 @@ using Purview.WslContainers.Images; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -namespace WslContainers.UnitTests; +namespace Purview.WslContainers; -public class ImageTests +class ImageTests { [Test] public async Task Parse_ShortName_DefaultsToDockerHubLibrary() { - Image reference = Image.Parse("alpine"); + var reference = Image.Parse("alpine"); await Assert.That(reference.Registry).IsEqualTo("docker.io"); await Assert.That(reference.Repository).IsEqualTo("library/alpine"); @@ -20,7 +18,7 @@ public async Task Parse_ShortName_DefaultsToDockerHubLibrary() [Test] public async Task Parse_ExplicitTag_IsPreserved() { - Image reference = Image.Parse("redis:7"); + var reference = Image.Parse("redis:7"); await Assert.That(reference.Registry).IsEqualTo("docker.io"); await Assert.That(reference.Repository).IsEqualTo("library/redis"); @@ -30,7 +28,7 @@ public async Task Parse_ExplicitTag_IsPreserved() [Test] public async Task Parse_FullyQualified_IsNormalized() { - Image reference = Image.Parse("docker.io/library/alpine:latest"); + var reference = Image.Parse("docker.io/library/alpine:latest"); await Assert.That(reference.Registry).IsEqualTo("docker.io"); await Assert.That(reference.Repository).IsEqualTo("library/alpine"); @@ -40,7 +38,7 @@ public async Task Parse_FullyQualified_IsNormalized() [Test] public async Task Parse_PrivateRegistry_IsDetected() { - Image reference = Image.Parse("ghcr.io/acme/app:v1"); + var reference = Image.Parse("ghcr.io/acme/app:v1"); await Assert.That(reference.Registry).IsEqualTo("ghcr.io"); await Assert.That(reference.Repository).IsEqualTo("acme/app"); @@ -50,7 +48,7 @@ public async Task Parse_PrivateRegistry_IsDetected() [Test] public async Task MatchesStoredName_ShortVsQualified_Matches() { - Image reference = Image.Parse("docker.io/library/alpine:latest"); + var reference = Image.Parse("docker.io/library/alpine:latest"); await Assert.That(reference.MatchesStoredName("alpine:latest")).IsTrue(); await Assert.That(reference.MatchesStoredName("redis:latest")).IsFalse(); @@ -60,7 +58,7 @@ public async Task MatchesStoredName_ShortVsQualified_Matches() [Test] public async Task WithTag_ReturnsNewImage() { - Image reference = Image.Parse("redis").WithTag("7.4"); + var reference = Image.Parse("redis").WithTag("7.4"); await Assert.That(reference.Tag).IsEqualTo("7.4"); await Assert.That(reference.FullReference).IsEqualTo("docker.io/library/redis:7.4"); diff --git a/tests/WslContainers.UnitTests/SecretTests.cs b/src/tests/WslContainers.UnitTests/SecretTests.cs similarity index 63% rename from tests/WslContainers.UnitTests/SecretTests.cs rename to src/tests/WslContainers.UnitTests/SecretTests.cs index 7856020..e3b032f 100644 --- a/tests/WslContainers.UnitTests/SecretTests.cs +++ b/src/tests/WslContainers.UnitTests/SecretTests.cs @@ -1,15 +1,13 @@ -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Diagnostics; -namespace WslContainers.UnitTests; +namespace Purview.WslContainers; -public class SecretTests +class SecretTests { [Test] public async Task ToString_RedactsValue() { - Secret secret = Secret.From("super-secret-password"); + var secret = Secret.From("super-secret-password"); await Assert.That(secret.ToString()).IsEqualTo(""); await Assert.That(secret.ToString()).DoesNotContain("super"); @@ -18,7 +16,7 @@ public async Task ToString_RedactsValue() [Test] public async Task Value_ReturnsUnderlying() { - Secret secret = Secret.From("super-secret-password"); + var secret = Secret.From("super-secret-password"); await Assert.That(secret.Value).IsEqualTo("super-secret-password"); } diff --git a/tests/WslContainers.UnitTests/StorageModeTests.cs b/src/tests/WslContainers.UnitTests/StorageModeTests.cs similarity index 75% rename from tests/WslContainers.UnitTests/StorageModeTests.cs rename to src/tests/WslContainers.UnitTests/StorageModeTests.cs index 0d7c214..7f422c8 100644 --- a/tests/WslContainers.UnitTests/StorageModeTests.cs +++ b/src/tests/WslContainers.UnitTests/StorageModeTests.cs @@ -1,15 +1,11 @@ -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +namespace Purview.WslContainers; -namespace WslContainers.UnitTests; - -public class StorageModeTests +class StorageModeTests { [Test] public async Task DefaultOptions_UseSharedStorage() { - WslContainerRuntimeOptions options = WslContainerRuntimeOptions.Default; + var options = WslContainerRuntimeOptions.Default; await Assert.That(options.StorageMode).IsEqualTo(StorageMode.Shared); await Assert.That(options.StoragePath).IsNull(); @@ -31,4 +27,4 @@ public async Task ExplicitStoragePath_OverridesSharedDefault() await Assert.That(options.StoragePath).IsEqualTo(@"C:\temp\store"); await Assert.That(options.StorageMode).IsEqualTo(StorageMode.Shared); } -} \ No newline at end of file +} diff --git a/src/tests/WslContainers.UnitTests/WaitContextTests.cs b/src/tests/WslContainers.UnitTests/WaitContextTests.cs new file mode 100644 index 0000000..68c22b0 --- /dev/null +++ b/src/tests/WslContainers.UnitTests/WaitContextTests.cs @@ -0,0 +1,46 @@ +using Purview.WslContainers.Waiting; + +namespace Purview.WslContainers; + +class WaitContextTests +{ + static (WaitContext, IAsyncDisposable) Create(IReadOnlyDictionary mappings) + { + FakeContainer container = new() + { PortMappings = mappings }; + return (new WaitContext(container, mappings, "172.17.0.9"), container); + } + + [Test] + public async Task GetHostPort_ReturnsMappedPort() + { + var (context, container) = Create(new Dictionary { [8080] = 54321 }); + + await using (container) + { + await Assert.That(context.GetHostPort(8080)).IsEqualTo(54321); + await Assert.That(context.GetHostPort(80)).IsNull(); + } + } + + [Test] + public async Task FirstHostPort_ReturnsFirstMapping() + { + var (context, container) = Create(new Dictionary { [8080] = 54321, [8081] = 54322 }); + + await using (container) + await Assert.That(context.FirstHostPort).IsEqualTo(54321); + } + + [Test] + public async Task FirstHostPort_EmptyMappings_IsNull() + { + var (context, container) = Create(new Dictionary()); + + await using (container) + { + await Assert.That(context.FirstHostPort).IsNull(); + await Assert.That(context.NetworkIp).IsEqualTo("172.17.0.9"); + } + } +} diff --git a/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs b/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs similarity index 62% rename from tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs rename to src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs index 0c33b8f..c372fdc 100644 --- a/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs +++ b/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs @@ -1,35 +1,35 @@ -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +using Purview.WslContainers.Runtime; +using Purview.WslContainers.Waiting; -namespace WslContainers.UnitTests; +namespace Purview.WslContainers; -public class WaitStrategyRunnerTests +class WaitStrategyRunnerTests { [Test] - public async Task RunAsync_SucceedsWhenConditionEventuallyTrue() + public async Task RunAsync_SucceedsWhenConditionEventuallyTrue(CancellationToken cancellationToken) { - FakeContainer container = new FakeContainer { Name = "runner-test" }; - WaitContext context = new WaitContext(container, container.PortMappings, networkIp: null); - int calls = 0; + await using FakeContainer container = new() + { Name = "runner-test" }; + WaitContext context = new(container, container.PortMappings, networkIp: null); + var calls = 0; IWaitStrategy strategy = Wait.ForCustom((_, _) => Task.FromResult(++calls >= 3)) .WithInterval(TimeSpan.FromMilliseconds(10)); - await WaitStrategyRunner.RunAsync(context, new[] { strategy }, TimeSpan.FromSeconds(5), CancellationToken.None); + await WaitStrategyRunner.RunAsync(context, new[] { strategy }, TimeSpan.FromSeconds(5), cancellationToken); await Assert.That(calls).IsGreaterThanOrEqualTo(3); } [Test] - public async Task RunAsync_TimesOut_ThrowsWithDiagnostics() + public async Task RunAsync_TimesOut_ThrowsWithDiagnostics(CancellationToken cancellationToken) { - FakeContainer container = new FakeContainer + await using FakeContainer container = new() { Name = "runner-test", Logs = "some service output\n", PortMappings = new Dictionary { [80] = 1234 }, }; - WaitContext context = new WaitContext(container, container.PortMappings, networkIp: null); + WaitContext context = new(container, container.PortMappings, networkIp: null); IWaitStrategy strategy = Wait.ForCustom((_, _) => Task.FromResult(false)) .WithInterval(TimeSpan.FromMilliseconds(10)) .WithTimeout(TimeSpan.FromMilliseconds(300)); @@ -41,7 +41,7 @@ await WaitStrategyRunner.RunAsync( context, new[] { strategy }, TimeSpan.FromSeconds(5), - CancellationToken.None + cancellationToken ); } catch (WslContainerTimeoutException ex) diff --git a/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs b/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs new file mode 100644 index 0000000..3eadce3 --- /dev/null +++ b/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs @@ -0,0 +1,108 @@ +using Purview.WslContainers.Containers; +using Purview.WslContainers.Waiting; +using System.Text.RegularExpressions; + +namespace Purview.WslContainers; + +class WaitStrategyTests +{ + static WaitContext Context(FakeContainer container) + { + return new WaitContext(container, container.PortMappings, networkIp: null); + } + + [Test] + public async Task LogMessage_MatchesAccumulatedOutput(CancellationToken cancellationToken) + { + await using FakeContainer container = new() + { + Logs = "2026-01-01 database system is ready to accept connections\n", + }; + var strategy = Wait.ForLogMessage("database system is ready"); + + await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsTrue(); + } + + [Test] + public async Task LogMessage_DoesNotMatchAbsentMessage(CancellationToken cancellationToken) + { + await using FakeContainer container = new() + { Logs = "starting up\n" }; +#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. + var strategy = Wait.ForLogMessage(new Regex("ready to accept")); +#pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. + + await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsFalse(); + } + + [Test] + public async Task Command_ReturnsTrueOnExitCodeZero(CancellationToken cancellationToken) + { + await using FakeContainer container = new() + { Exec = _ => new ExecResult(0, "ok", string.Empty) }; + var strategy = Wait.ForCommand("/bin/true"); + + await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsTrue(); + } + + [Test] + public async Task Command_ReturnsFalseOnNonZeroExit(CancellationToken cancellationToken) + { + await using FakeContainer container = new() + { Exec = _ => new ExecResult(1, string.Empty, "boom") }; + var strategy = Wait.ForCommand("/bin/false"); + + await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsFalse(); + } + + [Test] + public async Task Command_ReturnsFalseWhenExecThrows(CancellationToken cancellationToken) + { + await using FakeContainer container = new() + { + Exec = _ => throw new InvalidOperationException("container not ready"), + }; + var strategy = Wait.ForCommand("/bin/true"); + + await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsFalse(); + } + + [Test] + public async Task ForAll_RequiresEveryStrategy(CancellationToken cancellationToken) + { + await using FakeContainer container = new(); + var all = Wait.ForAll( + Wait.ForCustom((_, _) => Task.FromResult(true)), + Wait.ForCustom((_, _) => Task.FromResult(false)) + ); + var any = Wait.ForAny( + Wait.ForCustom((_, _) => Task.FromResult(true)), + Wait.ForCustom((_, _) => Task.FromResult(false)) + ); + + await Assert.That(await all.UntilAsync(Context(container), cancellationToken)).IsFalse(); + await Assert.That(await any.UntilAsync(Context(container), cancellationToken)).IsTrue(); + } + + [Test] + public async Task ContainerRunning_ReflectsState(CancellationToken cancellationToken) + { + await using FakeContainer running = new() + { State = ContainerState.Running }; + await using FakeContainer exited = new() + { State = ContainerState.Exited }; + var strategy = Wait.ForContainerRunning(); + + await Assert.That(await strategy.UntilAsync(Context(running), cancellationToken)).IsTrue(); + await Assert.That(await strategy.UntilAsync(Context(exited), cancellationToken)).IsFalse(); + } + + [Test] + public async Task TcpPort_ReturnsFalseWhenNoMapping(CancellationToken cancellationToken) + { + await using FakeContainer container = new(); + var strategy = Wait.ForTcpPort(8080); + + await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsFalse(); + } +} diff --git a/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs b/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs new file mode 100644 index 0000000..2d9fdd3 --- /dev/null +++ b/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs @@ -0,0 +1,53 @@ +using System.Runtime.InteropServices; + +namespace Purview.WslContainers; + +class WslContainerRuntimeTests +{ + const int ErrorSharingViolation = unchecked((int)0x80070020); + + [Test] + public async Task SharingViolation_OnDefaultSharedStore_FallsBackToIsolatedStore() + { + COMException exception = new( + "The process cannot access the file because it is being used by another process.", + ErrorSharingViolation + ); + + var shouldFallBack = WslContainerRuntime.ShouldFallBackToIsolatedStore( + exception, + isUsingDefaultSharedStore: true + ); + + await Assert.That(shouldFallBack).IsTrue(); + } + + [Test] + public async Task SharingViolation_OnExplicitStore_DoesNotFallBack() + { + COMException exception = new( + "The process cannot access the file because it is being used by another process.", + ErrorSharingViolation + ); + + var shouldFallBack = WslContainerRuntime.ShouldFallBackToIsolatedStore( + exception, + isUsingDefaultSharedStore: false + ); + + await Assert.That(shouldFallBack).IsFalse(); + } + + [Test] + public async Task NonSharingViolation_DoesNotFallBack() + { + COMException exception = new("Unexpected failure", unchecked((int)0x8000FFFF)); + + var shouldFallBack = WslContainerRuntime.ShouldFallBackToIsolatedStore( + exception, + isUsingDefaultSharedStore: true + ); + + await Assert.That(shouldFallBack).IsFalse(); + } +} diff --git a/tests/WslContainers.UnitTests/WslContainers.UnitTests.csproj b/src/tests/WslContainers.UnitTests/WslContainers.UnitTests.csproj similarity index 100% rename from tests/WslContainers.UnitTests/WslContainers.UnitTests.csproj rename to src/tests/WslContainers.UnitTests/WslContainers.UnitTests.csproj diff --git a/tests/WslContainers.UnitTests/WslInspectParserTests.cs b/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs similarity index 62% rename from tests/WslContainers.UnitTests/WslInspectParserTests.cs rename to src/tests/WslContainers.UnitTests/WslInspectParserTests.cs index 7f015a4..12ded9f 100644 --- a/tests/WslContainers.UnitTests/WslInspectParserTests.cs +++ b/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs @@ -1,12 +1,8 @@ -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; +namespace Purview.WslContainers; -namespace WslContainers.UnitTests; - -public class WslInspectParserTests +class WslInspectParserTests { - private const string SampleInspect = """ + const string SampleInspect = /*lang=json,strict*/ """ { "HostConfig": { "NetworkMode": "bridge" }, "NetworkSettings": { @@ -23,7 +19,7 @@ public class WslInspectParserTests [Test] public async Task ParseHostPorts_ReadsMappedPorts() { - IReadOnlyDictionary ports = WslInspectParser.ParseHostPorts(SampleInspect); + var ports = WslInspectParser.ParseHostPorts(SampleInspect); await Assert.That(ports.Count).IsEqualTo(1); await Assert.That(ports[8080]).IsEqualTo((ushort)61332); @@ -32,7 +28,7 @@ public async Task ParseHostPorts_ReadsMappedPorts() [Test] public async Task ParseHostPorts_EmptyJson_ReturnsEmpty() { - IReadOnlyDictionary ports = WslInspectParser.ParseHostPorts("{}"); + var ports = WslInspectParser.ParseHostPorts("{}"); await Assert.That(ports.Count).IsEqualTo(0); } @@ -40,7 +36,7 @@ public async Task ParseHostPorts_EmptyJson_ReturnsEmpty() [Test] public async Task TryGetNetworkIp_ReadsBridgeIp() { - string? ip = WslInspectParser.TryGetNetworkIp(SampleInspect); + var ip = WslInspectParser.TryGetNetworkIp(SampleInspect); await Assert.That(ip).IsEqualTo("172.17.0.3"); } @@ -48,7 +44,7 @@ public async Task TryGetNetworkIp_ReadsBridgeIp() [Test] public async Task TryGetNetworkIp_MissingNetwork_ReturnsNull() { - string? ip = WslInspectParser.TryGetNetworkIp("{}"); + var ip = WslInspectParser.TryGetNetworkIp("{}"); await Assert.That(ip).IsNull(); } diff --git a/tests/WslContainers.IntegrationTests/PerformanceTests.cs b/tests/WslContainers.IntegrationTests/PerformanceTests.cs deleted file mode 100644 index ef1fd9c..0000000 --- a/tests/WslContainers.IntegrationTests/PerformanceTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Diagnostics; -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.IntegrationTests; - -public class PerformanceTests -{ - [Test] - [NotInParallel] - public async Task WarmContainerStart_IsFast() - { - await WslcTest.SkipIfUnavailableAsync(); - - // Warm the image into the shared session store. - await using ( - WslContainer warm = new ContainerBuilder() - .WithImage("alpine:latest") - .WithCommand("/bin/echo", "warm") - .Build() - ) - { - await warm.StartAsync(); - } - - Stopwatch stopwatch = Stopwatch.StartNew(); - await using WslContainer container = new ContainerBuilder() - .WithImage("alpine:latest") - .WithCommand("/bin/echo", "hi") - .Build(); - await container.StartAsync(); - stopwatch.Stop(); - - TestContext.Current!.OutputWriter.WriteLine($"warm container start: {stopwatch.Elapsed}"); - await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromSeconds(60)); - } -} diff --git a/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs b/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs deleted file mode 100644 index 7bd6652..0000000 --- a/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.IntegrationTests; - -public class RuntimeInfoTests -{ - [Test] - public async Task GetInfoAsync_ReportsAvailability() - { - await WslcTest.SkipIfUnavailableAsync(); - - WslContainerRuntimeInfo info = await WslContainerRuntime.Instance.GetInfoAsync(); - - await Assert.That(info.IsAvailable).IsTrue(); - await Assert.That(info.IsCompatible).IsTrue(); - await Assert.That(info.Version).IsNotEmpty(); - } -} diff --git a/tests/WslContainers.IntegrationTests/VolumeTests.cs b/tests/WslContainers.IntegrationTests/VolumeTests.cs deleted file mode 100644 index 76a0dde..0000000 --- a/tests/WslContainers.IntegrationTests/VolumeTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Purview.WslContainers; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.IntegrationTests; - -public class VolumeTests -{ - [Test] - public async Task BindMount_ReadsHostFile() - { - await WslcTest.SkipIfUnavailableAsync(); - - string dir = Path.Combine(Path.GetTempPath(), "wslc-bind-" + Guid.NewGuid().ToString("N")[..6]); - Directory.CreateDirectory(dir); - File.WriteAllText(Path.Combine(dir, "hello.txt"), "hello-bind\n"); - try - { - await using WslContainer container = new ContainerBuilder() - .WithImage("alpine:latest") - .WithBindMount(dir, "/mnt/data") - .WithCommand("/bin/cat", "/mnt/data/hello.txt") - .Build(); - - await container.StartAsync(); - string logs = await container.GetLogsAsync(); - await Assert.That(logs).Contains("hello-bind"); - } - finally - { - Directory.Delete(dir, recursive: true); - } - } - - [Test] - public async Task NamedVolume_IsSharedBetweenContainers() - { - await WslcTest.SkipIfUnavailableAsync(); - - string volume = "wslc-vol-" + Guid.NewGuid().ToString("N")[..6]; - await using WslContainer writer = new ContainerBuilder() - .WithImage("alpine:latest") - .WithVolumeMount(volume, "/data") - .WithCommand("/bin/sh", "-c", "echo shared-data > /data/file.txt; sleep 300") - .Build(); - await writer.StartAsync(); - await Task.Delay(1500); - - await using WslContainer reader = new ContainerBuilder() - .WithImage("alpine:latest") - .WithVolumeMount(volume, "/data") - .WithCommand("/bin/cat", "/data/file.txt") - .Build(); - await reader.StartAsync(); - string logs = await reader.GetLogsAsync(); - await Assert.That(logs).Contains("shared-data"); - } -} diff --git a/tests/WslContainers.MySql.IntegrationTests/MySqlIntegrationTests.cs b/tests/WslContainers.MySql.IntegrationTests/MySqlIntegrationTests.cs deleted file mode 100644 index 55d9589..0000000 --- a/tests/WslContainers.MySql.IntegrationTests/MySqlIntegrationTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using MySqlConnector; -using Purview.WslContainers.MySql; -using Purview.WslContainers.Testing; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.MySql.IntegrationTests; - -public class MySqlIntegrationTests -{ - [Test] - public async Task MySql_ConnectsAndRunsQuery() - { - await WslcTest.SkipIfUnavailableAsync(); - - await using MySqlContainer mySql = new MySqlBuilder() - .WithDatabase("integration") - .WithUsername("test") - .WithPassword("test") - .Build(); - - await mySql.StartAsync(); - - await using MySqlConnection connection = new MySqlConnection(mySql.GetConnectionString()); - await connection.OpenAsync(); - await using MySqlCommand command = new MySqlCommand("SELECT 1", connection); - object result = await command.ExecuteScalarAsync(); - - await Assert.That(Convert.ToInt32(result, System.Globalization.CultureInfo.InvariantCulture)).IsEqualTo(1); - } - - [Test] - public async Task ConnectionString_UsesMappedRandomPort() - { - await WslcTest.SkipIfUnavailableAsync(); - - await using MySqlContainer mySql = new MySqlBuilder().Build(); - - await mySql.StartAsync(); - - MySqlConnectionStringBuilder builder = new MySqlConnectionStringBuilder(mySql.GetConnectionString()); - ushort port = mySql.GetMappedPublicPort(MySqlBuilder.MySqlPort); - - await Assert.That(builder.Server).IsEqualTo("127.0.0.1"); - await Assert.That(builder.Port).IsEqualTo(port); - await Assert.That(builder.Database).IsEqualTo("test"); - } -} diff --git a/tests/WslContainers.Redis.IntegrationTests/RedisIntegrationTests.cs b/tests/WslContainers.Redis.IntegrationTests/RedisIntegrationTests.cs deleted file mode 100644 index 77b4d92..0000000 --- a/tests/WslContainers.Redis.IntegrationTests/RedisIntegrationTests.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Purview.WslContainers.Redis; -using StackExchange.Redis; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.Redis.IntegrationTests; - -public class RedisIntegrationTests -{ - [Test] - public async Task Redis_RespondsToPing() - { - await Purview.WslContainers.Testing.WslcTest.SkipIfUnavailableAsync(); - - await using RedisContainer redis = new RedisBuilder().Build(); - - await redis.StartAsync(); - - await using ConnectionMultiplexer connection = await ConnectionMultiplexer.ConnectAsync( - redis.GetConnectionString() - ); - IDatabase db = connection.GetDatabase(); - RedisResult pong = await db.ExecuteAsync("PING"); - - await Assert.That(pong.ToString()).IsEqualTo("PONG"); - } - - [Test] - public async Task ConnectionString_UsesMappedRandomPort() - { - await Purview.WslContainers.Testing.WslcTest.SkipIfUnavailableAsync(); - - await using RedisContainer redis = new RedisBuilder().Build(); - - await redis.StartAsync(); - - string connectionString = redis.GetConnectionString(); - ushort port = redis.GetMappedPublicPort(RedisBuilder.RedisPort); - - await Assert.That(connectionString).IsEqualTo($"localhost:{port}"); - await Assert.That(port).IsNotEqualTo(RedisBuilder.RedisPort); - } -} diff --git a/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs b/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs deleted file mode 100644 index cba8feb..0000000 --- a/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using System.Text; -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.UnitTests; - -public class HttpWaitStrategyTests -{ - [Test] - public async Task Http_ReturnsTrueOnExpectedStatus() - { - (int port, HttpListener listener, Task _) = StartServer(HttpStatusCode.OK); - try - { - FakeContainer container = new FakeContainer - { - PortMappings = new Dictionary { [80] = (ushort)port }, - }; - WaitContext context = new WaitContext(container, container.PortMappings, networkIp: null); - HttpWaitStrategy strategy = Wait.ForHttp("/health").ForPort(80).ForStatusCode(HttpStatusCode.OK); - - await Assert.That(await strategy.UntilAsync(context, CancellationToken.None)).IsTrue(); - } - finally - { - listener.Stop(); - } - } - - [Test] - public async Task Http_ReturnsFalseOnUnexpectedStatus() - { - (int port, HttpListener listener, Task _) = StartServer(HttpStatusCode.InternalServerError); - try - { - FakeContainer container = new FakeContainer - { - PortMappings = new Dictionary { [80] = (ushort)port }, - }; - WaitContext context = new WaitContext(container, container.PortMappings, networkIp: null); - HttpWaitStrategy strategy = Wait.ForHttp("/health").ForPort(80).ForStatusCode(HttpStatusCode.OK); - - await Assert.That(await strategy.UntilAsync(context, CancellationToken.None)).IsFalse(); - } - finally - { - listener.Stop(); - } - } - - [Test] - public async Task Http_StatusPredicate_IsUsed() - { - (int port, HttpListener listener, Task _) = StartServer(HttpStatusCode.Accepted); - try - { - FakeContainer container = new FakeContainer - { - PortMappings = new Dictionary { [80] = (ushort)port }, - }; - WaitContext context = new WaitContext(container, container.PortMappings, networkIp: null); - HttpWaitStrategy strategy = Wait.ForHttp("/") - .ForPort(80) - .ForStatusPredicate(status => status >= 200 && status < 300); - - await Assert.That(await strategy.UntilAsync(context, CancellationToken.None)).IsTrue(); - } - finally - { - listener.Stop(); - } - } - - [Test] - public async Task Http_ReturnsFalseWhenPortNotMapped() - { - FakeContainer container = new FakeContainer(); - WaitContext context = new WaitContext(container, container.PortMappings, networkIp: null); - HttpWaitStrategy strategy = Wait.ForHttp("/").ForPort(80); - - await Assert.That(await strategy.UntilAsync(context, CancellationToken.None)).IsFalse(); - } - - private static (int Port, HttpListener Listener, Task Handler) StartServer(HttpStatusCode status) - { - using TcpListener probe = new TcpListener(IPAddress.Loopback, 0); - probe.Start(); - int port = ((IPEndPoint)probe.LocalEndpoint).Port; - probe.Stop(); - - HttpListener listener = new HttpListener(); - listener.Prefixes.Add($"http://127.0.0.1:{port}/"); - listener.Start(); - - Task handler = Task.Run(async () => - { - while (listener.IsListening) - { - try - { - HttpListenerContext ctx = await listener.GetContextAsync(); - ctx.Response.StatusCode = (int)status; - byte[] body = Encoding.UTF8.GetBytes("ok"); - ctx.Response.ContentLength64 = body.Length; - await ctx.Response.OutputStream.WriteAsync(body); - ctx.Response.Close(); - } - catch (HttpListenerException) - { - break; - } - catch (ObjectDisposedException) - { - break; - } - } - }); - - return (port, listener, handler); - } -} diff --git a/tests/WslContainers.UnitTests/WaitContextTests.cs b/tests/WslContainers.UnitTests/WaitContextTests.cs deleted file mode 100644 index 788742f..0000000 --- a/tests/WslContainers.UnitTests/WaitContextTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.UnitTests; - -public class WaitContextTests -{ - private static WaitContext Create(IReadOnlyDictionary mappings) - { - FakeContainer container = new FakeContainer { PortMappings = mappings }; - return new WaitContext(container, mappings, "172.17.0.9"); - } - - [Test] - public async Task GetHostPort_ReturnsMappedPort() - { - WaitContext context = Create(new Dictionary { [8080] = 54321 }); - - await Assert.That(context.GetHostPort(8080)).IsEqualTo(54321); - await Assert.That(context.GetHostPort(80)).IsNull(); - } - - [Test] - public async Task FirstHostPort_ReturnsFirstMapping() - { - WaitContext context = Create(new Dictionary { [8080] = 54321, [8081] = 54322 }); - - await Assert.That(context.FirstHostPort).IsEqualTo(54321); - } - - [Test] - public async Task FirstHostPort_EmptyMappings_IsNull() - { - WaitContext context = Create(new Dictionary()); - - await Assert.That(context.FirstHostPort).IsNull(); - await Assert.That(context.NetworkIp).IsEqualTo("172.17.0.9"); - } -} diff --git a/tests/WslContainers.UnitTests/WaitStrategyTests.cs b/tests/WslContainers.UnitTests/WaitStrategyTests.cs deleted file mode 100644 index d15595d..0000000 --- a/tests/WslContainers.UnitTests/WaitStrategyTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Text.RegularExpressions; -using Purview.WslContainers; -using TUnit.Assertions; -using TUnit.Assertions.Extensions; - -namespace WslContainers.UnitTests; - -public class WaitStrategyTests -{ - private static WaitContext Context(FakeContainer container) - { - return new WaitContext(container, container.PortMappings, networkIp: null); - } - - [Test] - public async Task LogMessage_MatchesAccumulatedOutput() - { - FakeContainer container = new FakeContainer - { - Logs = "2026-01-01 database system is ready to accept connections\n", - }; - IWaitStrategy strategy = Wait.ForLogMessage("database system is ready"); - - await Assert.That(await strategy.UntilAsync(Context(container), CancellationToken.None)).IsTrue(); - } - - [Test] - public async Task LogMessage_DoesNotMatchAbsentMessage() - { - FakeContainer container = new FakeContainer { Logs = "starting up\n" }; - IWaitStrategy strategy = Wait.ForLogMessage(new Regex("ready to accept")); - - await Assert.That(await strategy.UntilAsync(Context(container), CancellationToken.None)).IsFalse(); - } - - [Test] - public async Task Command_ReturnsTrueOnExitCodeZero() - { - FakeContainer container = new FakeContainer { Exec = _ => new ExecResult(0, "ok", string.Empty) }; - IWaitStrategy strategy = Wait.ForCommand("/bin/true"); - - await Assert.That(await strategy.UntilAsync(Context(container), CancellationToken.None)).IsTrue(); - } - - [Test] - public async Task Command_ReturnsFalseOnNonZeroExit() - { - FakeContainer container = new FakeContainer { Exec = _ => new ExecResult(1, string.Empty, "boom") }; - IWaitStrategy strategy = Wait.ForCommand("/bin/false"); - - await Assert.That(await strategy.UntilAsync(Context(container), CancellationToken.None)).IsFalse(); - } - - [Test] - public async Task Command_ReturnsFalseWhenExecThrows() - { - FakeContainer container = new FakeContainer - { - Exec = _ => throw new InvalidOperationException("container not ready"), - }; - IWaitStrategy strategy = Wait.ForCommand("/bin/true"); - - await Assert.That(await strategy.UntilAsync(Context(container), CancellationToken.None)).IsFalse(); - } - - [Test] - public async Task ForAll_RequiresEveryStrategy() - { - FakeContainer container = new FakeContainer(); - IWaitStrategy all = Wait.ForAll( - Wait.ForCustom((_, _) => Task.FromResult(true)), - Wait.ForCustom((_, _) => Task.FromResult(false)) - ); - IWaitStrategy any = Wait.ForAny( - Wait.ForCustom((_, _) => Task.FromResult(true)), - Wait.ForCustom((_, _) => Task.FromResult(false)) - ); - - await Assert.That(await all.UntilAsync(Context(container), CancellationToken.None)).IsFalse(); - await Assert.That(await any.UntilAsync(Context(container), CancellationToken.None)).IsTrue(); - } - - [Test] - public async Task ContainerRunning_ReflectsState() - { - FakeContainer running = new FakeContainer { State = ContainerState.Running }; - FakeContainer exited = new FakeContainer { State = ContainerState.Exited }; - IWaitStrategy strategy = Wait.ForContainerRunning(); - - await Assert.That(await strategy.UntilAsync(Context(running), CancellationToken.None)).IsTrue(); - await Assert.That(await strategy.UntilAsync(Context(exited), CancellationToken.None)).IsFalse(); - } - - [Test] - public async Task TcpPort_ReturnsFalseWhenNoMapping() - { - FakeContainer container = new FakeContainer(); - IWaitStrategy strategy = Wait.ForTcpPort(8080); - - await Assert.That(await strategy.UntilAsync(Context(container), CancellationToken.None)).IsFalse(); - } -} From 43552bc02a5238e939db98ebda45131dda2eed17 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 30 Sep 2026 16:29:01 +0100 Subject: [PATCH 2/4] chore: restored .editorconfig --- .editorconfig | 2698 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2698 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..120d509 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2698 @@ +root = true + +# Base Configuration - General Settings, File Formatting, and Naming Conventions + +# All Files +[*] +charset = utf-8 +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +tab_width = 4 +trim_trailing_whitespace = true +max_line_length = 120 + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +# Nullability settings +dotnet_build_property.Nullable = enable + +# Per-rule severity is authoritative in this file. The SDK sets AnalysisMode/AnalysisLevel as +# MSBuild properties, and the .NET SDK ignores bulk 'dotnet_analyzer_diagnostic.*' severity +# configuration whenever those properties are present. Do not re-add a bulk entry here: it +# silently does nothing and hides which severities are actually enforced. + +# Visual Studio XML Project Files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML Configuration Files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,xml,stylecop}] +indent_size = 2 + +# JSON Files +[*.{json,json5,webmanifest}] +indent_size = 2 + +# YAML Files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown Files +[*.{md,mdx}] +trim_trailing_whitespace = false + +# Bash Files +[*.sh] +end_of_line = lf + +# Batch Files +[*.{cmd,bat}] +end_of_line = crlf + +# Web Files +[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}] +indent_size = 2 +insert_final_newline = true + +# Makefiles +[Makefile] +indent_style = tab + +# .NET Style Rules - Naming Conventions +# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/language-rules#net-style-rules + +[*.{cs,csx,cake,vb,vbx}] + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case +# NOTE: the private-field rules that used to sit here referenced a style ('camel_case_underscore') +# and a symbol group ('private_fields') that were never defined, so they never applied. The real +# private-field rules are declared in the "StyleCop Field Naming Rules" section below. +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +# Constants are PascalCase (field constants; local constants are locals and stay camelCase) +dotnet_naming_rule.constants_should_be_pascal_case.severity = warning +dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_style.constant_style.capitalization = pascal_case +dotnet_naming_symbols.constants.applicable_kinds = field +dotnet_naming_symbols.constants.required_modifiers = const + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style +dotnet_naming_rule.locals_should_be_camel_case.severity = warning + +# camel_case_style - Define the camelCase style +dotnet_naming_style.camel_case_style.capitalization = camel_case +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +# first_upper_style - The first character must start with an upper-case character +dotnet_naming_style.first_upper_style.capitalization = first_word_upper + +# prefix_interface_with_i_style - Interfaces must be PascalCase and first character must be 'I' +dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I +dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case + +# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and start with 'T' +dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case +dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T + +# disallowed_style - Anything that has this style applied is marked as disallowed +dotnet_naming_style.disallowed_style.capitalization = pascal_case +dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ +dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ + +# internal_error_style - This style should never occur +dotnet_naming_style.internal_error_style.capitalization = pascal_case +dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ +dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ + +# All public/protected/protected_internal constant fields must be PascalCase +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field +dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const + +# All public/protected/protected_internal static readonly fields must be PascalCase +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly + +# No other public/protected/protected_internal fields are allowed +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style +dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group +dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field + +# StyleCop Field Naming Rules + +# All non-private constant fields must be PascalCase. Private constants are owned by +# 'private_static_fields_group' below - a field must be covered by exactly one rule, otherwise the +# same violation is reported once per matching rule. +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const + +# All non-private static readonly fields must be PascalCase. Private static readonly fields are +# owned by 'private_static_fields_group' below (see the note on the constant rule above). +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly + +# No non-private instance fields are allowed +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field + +# Private static fields are PascalCase: they are type-level state and are never qualified with 'this.' +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.severity = warning +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.style = non_private_static_field_style +dotnet_naming_rule.private_static_fields_must_be_pascal_case_rule.symbols = private_static_fields_group +dotnet_naming_symbols.private_static_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields_group.applicable_kinds = field +dotnet_naming_symbols.private_static_fields_group.required_modifiers = static + +# Private instance fields must be camelCase with a leading underscore: '_name', never 'name'. The +# prefix keeps field access unambiguous (so the noisy 'this.' qualifier is never needed) and keeps +# fields distinguishable from locals and parameters. +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.severity = warning +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.style = camel_case_underscore_style +dotnet_naming_rule.private_instance_fields_must_be_camel_case_with_underscore_prefix.symbols = private_instance_fields_group +dotnet_naming_symbols.private_instance_fields_group.applicable_accessibilities = private +dotnet_naming_symbols.private_instance_fields_group.applicable_kinds = field + +# Local variables must be camelCase +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local + +# Sanity check - uncovered field case +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style +dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * +dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field + +# All elements (namespaces, classes, enums, etc.) must be PascalCase +dotnet_naming_rule.element_rule.severity = warning +dotnet_naming_rule.element_rule.style = non_private_static_field_style +dotnet_naming_rule.element_rule.symbols = element_group +dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property + +# Interfaces use PascalCase and are prefixed with uppercase 'I' +dotnet_naming_rule.interface_rule.severity = warning +dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style +dotnet_naming_rule.interface_rule.symbols = interface_group +dotnet_naming_symbols.interface_group.applicable_kinds = interface + +# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' +dotnet_naming_rule.type_parameter_rule.severity = warning +dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style +dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group +dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter + +# Function parameters use camelCase +dotnet_naming_rule.parameters_rule.severity = warning +dotnet_naming_rule.parameters_rule.style = camel_case_style +dotnet_naming_rule.parameters_rule.symbols = parameters_group +dotnet_naming_symbols.parameters_group.applicable_kinds = parameter + +# Type Parameters +dotnet_naming_rule.type_parameter_naming.severity = warning +dotnet_naming_rule.type_parameter_naming.style = type_parameter_style +dotnet_naming_rule.type_parameter_naming.symbols = type_parameter_symbol +dotnet_naming_style.type_parameter_style.capitalization = pascal_case +dotnet_naming_style.type_parameter_style.required_prefix = T +dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * +dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter + +# Naming style shared by the private-field rules: camelCase with a required '_' prefix. +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = warning +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_style.local_function_style.capitalization = pascal_case +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +# "this." and "Me." qualifiers +dotnet_style_qualification_for_event = false:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_property = false:silent + +# Undocumented +dotnet_style_operator_placement_when_wrapping = end_of_line + +# Naming styles +# NOTE: a 'types_should_be_pascal_case' rule (symbol group 'types') and an +# 'interface_should_be_begins_with_i' rule (symbol group 'interface') used to sit here, but neither +# symbol group was ever defined, so neither rule applied. Types are covered by 'element_rule' and +# interfaces by 'interface_rule' above. + +# By default, name items with PascalCase +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = non_private_static_field_style +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members + +# pascal_case_style - Define the PascalCase style +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Symbol specifications +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.required_modifiers = * + +# Naming styles +dotnet_naming_style.begins_with_i.capitalization = pascal_case +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = + +# Simplify interpolation +dotnet_diagnostic.IDE0071.severity = warning + +# Accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# Allow multiple blank lines +dotnet_diagnostic.IDE2000.severity = silent + +# Code Quality Settings +dotnet_code_quality.enable_nullable_reference_types = true +dotnet_code_quality.enableNETAnalyzers = true +dotnet_code_quality.interpolated_string_composite_format = true +dotnet_code_quality.prefer_auto_properties = false +dotnet_code_quality.prefer_const = true +dotnet_code_quality.prefer_inferred_anonymous_type_member_names = true +dotnet_code_quality.prefer_inferred_tuple_names = true +dotnet_code_quality.prefer_readonly = true +# The real modifier policy is 'dotnet_style_require_accessibility_modifiers' (declared under +# "Modifier preferences" below). The 'require_accessibility_modifiers' and +# 'require_explicit_visibility' entries that used to live here are not .NET analyzer options: +# declaring them only pretended to enforce a policy that was never applied, so they were removed. +dotnet_code_quality.require_explicit_type_arguments = true +dotnet_code_quality.require_variable_declaration_for_explicit_type = false +dotnet_code_quality_unused_parameters = all:warning +dotnet_enable_roslyn_analyzers = true +dotnet_remove_unnecessary_suppression_exclusions = none + +# RS0016: Add public types and members to the declared API +dotnet_public_api_analyzer.require_api_files = true + +# Do not use generic CodeAction.Create to create CodeAction +dotnet_diagnostic.RS0005.severity = none + +# C# Specific Settings + +[*.{cs,csx,cake}] + +# Newline options +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# C# Unnecessary code rules +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent + +# Set the C# language version +csharp_language_version = latest + +# Prefer var for inferred expressions such as method calls. +# PDS0003 supplies the narrower explicit-type rule for object creation. +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +dotnet_style_readonly_field = true:warning +dotnet_style_require_accessibility_modifiers = omit_if_default:warning + +# Code-block preferences +csharp_prefer_braces = when_multiline:suggestion +csharp_prefer_simple_using_statement = false:silent +csharp_prefer_system_threading_lock = true +dotnet_style_prefer_collection_expression = true:suggestion + +# Enable foreach explicit cast preference +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:warning + +# Prefer System.Threading.Lock +csharp_prefer_system_threading_lock = true:warning + +# Prefer parameter null checking +csharp_style_prefer_parameter_null_checking = true:suggestion + +# Prefer extended property pattern +csharp_style_prefer_extended_property_pattern = true:suggestion + +# Prefer UTF-8 string literals +csharp_style_prefer_utf8_string_literals = true:suggestion + +# Prefer tuple swap +csharp_style_prefer_tuple_swap = true:suggestion + +# Prefer local over anonymous function +csharp_style_prefer_local_over_anonymous_function = true:suggestion + +# Prefer unbound generic type in nameof +csharp_style_prefer_unbound_generic_type_in_nameof = true:warning + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:warning +csharp_style_deconstructed_variable_declaration = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_style_throw_expression = true:warning + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:warning + +# New line preferences +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent + +# Expression-Bodied members +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +csharp_style_expression_bodied_methods = true:suggestion +csharp_style_expression_bodied_operators = true:warning +csharp_style_expression_bodied_properties = true:warning + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_switch_expression = true:warning +csharp_style_prefer_explicit_this = true:warning + +# "Null" checking preferences +csharp_style_conditional_delegate_call = true:warning + +# Spacing options +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrap options +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Namespace options +csharp_style_namespace_declarations = file_scoped:warning + +# Visual Basic Specific Settings + +[*.{vb}] + +visual_basic_preferred_modifier_order = Partial, Default, Private, Protected, Public, Friend, NotOverridable, Overridable, MustOverride, Overloads, Overrides, MustInherit, NotInheritable, Static, Shared, Shadows, ReadOnly, WriteOnly, Dim, Const, WithEvents, Widening, Narrowing, Custom, Async:warning +visual_basic_style_prefer_simplified_object_creation = all : suggestion +visual_basic_style_prefer_isnot_expression = true : suggestion + +# .NET Style Preferences + +[*.{cs,csx,cake,vb,vbx}] + +# Language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning + +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_simplified_boolean_expressions = false:silent +dotnet_style_prefer_simplified_interpolation = true:warning + +# Expression-level preferences +dotnet_prefer_system_hash_code = true + +# Null-checking preferences +dotnet_style_coalesce_expression = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning + +[*.g.cs] +# Generated source-generator output: any diagnostics are worthless, so treat as generated code. +generated_code = true +# Missing XML comment for publicly visible type or member 'Type_or_Member' +dotnet_diagnostic.CS1591.severity = none + +# Design Rules (CA1000-CA1070) +# Design rules support adherence to the .NET Framework design guidelines + + +[*.{cs,vb}] + +# Do not declare static members on generic types +dotnet_diagnostic.CA1000.severity = suggestion + +# Types that own disposable fields should be disposable +dotnet_diagnostic.CA1001.severity = error + +# Do not expose generic lists +dotnet_diagnostic.CA1002.severity = error + +# Use generic event handler instances +dotnet_diagnostic.CA1003.severity = warning + +# Avoid excessive parameters on generic types +dotnet_diagnostic.CA1005.severity = warning + +# Enums should have zero value +dotnet_diagnostic.CA1008.severity = error + +# Collections should implement generic interface +dotnet_diagnostic.CA1010.severity = warning + +# Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = error + +# Mark assemblies with CLSCompliantAttribute +dotnet_diagnostic.CA1014.severity = silent + +# Mark assemblies with AssemblyVersionAttribute +dotnet_diagnostic.CA1016.severity = error + +# Mark assemblies with ComVisibleAttribute +dotnet_diagnostic.CA1017.severity = silent + +# Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = suggestion + +# Define accessors for attribute arguments +dotnet_diagnostic.CA1019.severity = suggestion + +# Avoid out parameters +dotnet_diagnostic.CA1021.severity = suggestion + +# Use properties where appropriate +dotnet_diagnostic.CA1024.severity = suggestion + +# Mark enums with FlagsAttribute +dotnet_diagnostic.CA1027.severity = error + +# Enum storage should be Int32 +dotnet_diagnostic.CA1028.severity = error + +# Use events where appropriate +dotnet_diagnostic.CA1030.severity = suggestion + +# Do not catch general exception types +dotnet_diagnostic.CA1031.severity = error + +# Implement standard exception constructors +# Raised to warning: the public API surface of exceptions is part of the accessibility policy and +# must not be invisible to command-line builds. +dotnet_diagnostic.CA1032.severity = warning + +# Interface methods should be callable by child types +# Raised to warning: explicit interface implementations that hide the member from derived types are +# an accessibility problem, not a style preference. +dotnet_diagnostic.CA1033.severity = warning + +# Nested types should not be visible +dotnet_diagnostic.CA1034.severity = error + +# Override methods on comparable types +dotnet_diagnostic.CA1036.severity = suggestion + +# Avoid empty interfaces +# Raised to warning: empty interfaces are unenforceable contracts and were previously invisible to +# command-line builds. +dotnet_diagnostic.CA1040.severity = warning + +# Provide ObsoleteAttribute message +dotnet_diagnostic.CA1041.severity = warning + +# Use integral or string argument for indexers +dotnet_diagnostic.CA1043.severity = suggestion + +# Properties should not be write only +dotnet_diagnostic.CA1044.severity = error + +# Do not pass types by reference +dotnet_diagnostic.CA1045.severity = warning + +# Do not overload operator equals on reference types +dotnet_diagnostic.CA1046.severity = warning + +# Do not declare protected members in sealed types +dotnet_diagnostic.CA1047.severity = error + +# Declare types in namespaces +dotnet_diagnostic.CA1050.severity = error + +# Do not declare visible instance fields +dotnet_diagnostic.CA1051.severity = error + +# Static holder types should be Static or NotInheritable +dotnet_diagnostic.CA1052.severity = warning + +# Static holder types should not have default constructors +dotnet_diagnostic.CA1053.severity = warning + +# URI parameters should not be strings +dotnet_diagnostic.CA1054.severity = warning + +# URI return values should not be strings +dotnet_diagnostic.CA1055.severity = warning + +# URI properties should not be strings +dotnet_diagnostic.CA1056.severity = warning + +# Types should not extend certain base types +dotnet_diagnostic.CA1058.severity = warning + +# Move P/Invokes to NativeMethods class +dotnet_diagnostic.CA1060.severity = warning + +# Do not hide base class methods +dotnet_diagnostic.CA1061.severity = warning + +# Validate arguments of public methods +dotnet_code_quality.CA1062.api_surface = public, protected +dotnet_diagnostic.CA1062.severity = error + +# Implement IDisposable correctly +dotnet_diagnostic.CA1063.severity = error + +# Exceptions should be public +dotnet_diagnostic.CA1064.severity = warning + +# Do not raise exceptions in unexpected locations +dotnet_diagnostic.CA1065.severity = warning + +# Implement IEquatable when overriding Equals +dotnet_diagnostic.CA1066.severity = warning + +# Override Equals when implementing IEquatable +dotnet_diagnostic.CA1067.severity = warning + +# CancellationToken parameters must come last +dotnet_diagnostic.CA1068.severity = warning + +# Enums should not have duplicate values +dotnet_diagnostic.CA1069.severity = error + +# Do not declare event fields as virtual +dotnet_diagnostic.CA1070.severity = warning + +# Documentation Rules (CA1200) +# Documentation rules support writing well-documented libraries + + +[*.{cs,vb}] + +# Avoid using cref tags with a prefix +dotnet_diagnostic.CA1200.severity = suggestion + +# Globalization Rules (CA1303-CA1311 and CA2101) +# Globalization rules support world-ready libraries and applications + + +[*.{cs,vb}] + +# Do not pass literals as localized parameters +dotnet_code_quality.CA1303.use_naming_heuristic = true +dotnet_diagnostic.CA1303.severity = none + +# Specify CultureInfo +dotnet_diagnostic.CA1304.severity = error + +# Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = error + +# Specify StringComparison for clarity +dotnet_diagnostic.CA1307.severity = error + +# Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = warning + +# Use ordinal StringComparison +dotnet_diagnostic.CA1309.severity = warning + +# Specify StringComparison for correctness +dotnet_diagnostic.CA1310.severity = warning + +# Specify a culture or use an invariant version +dotnet_diagnostic.CA1311.severity = error + +# Specify marshaling for P/Invoke string arguments +dotnet_diagnostic.CA2101.severity = warning + +# Interoperability Rules (CA1400-CA1422) +# Portability rules support portability across different platforms +# Interoperability rules support interaction with COM clients + + +[*.{cs,vb}] + +# P/Invokes should not be visible +dotnet_diagnostic.CA1401.severity = error + +# Validate platform compatibility +dotnet_diagnostic.CA1416.severity = suggestion + +# Do not use OutAttribute on string parameters for P/Invokes +dotnet_diagnostic.CA1417.severity = suggestion + +# Validate platform compatibility +dotnet_diagnostic.CA1418.severity = suggestion + +# Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' +dotnet_diagnostic.CA1419.severity = suggestion + +# Property, type, or attribute requires runtime marshalling +dotnet_diagnostic.CA1420.severity = suggestion + +# Method uses runtime marshalling when DisableRuntimeMarshallingAttribute is applied +dotnet_diagnostic.CA1421.severity = warning + +# Validate platform compatibility - obsoleted APIs +dotnet_diagnostic.CA1422.severity = warning + +# Maintainability Rules (CA1501-CA1516) +# Maintainability rules support library and application maintenance + + +[*.{cs,vb}] + +# Avoid excessive inheritance +dotnet_diagnostic.CA1501.severity = warning + +# Avoid excessive complexity +dotnet_diagnostic.CA1502.severity = error + +# Avoid unmaintainable code +dotnet_diagnostic.CA1505.severity = error + +# Avoid excessive class coupling +dotnet_diagnostic.CA1506.severity = error + +# Use nameof in place of string +dotnet_diagnostic.CA1507.severity = warning + +# Avoid dead conditional code +dotnet_diagnostic.CA1508.severity = error + +# Invalid entry in code metrics configuration file +dotnet_diagnostic.CA1509.severity = error + +# Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = warning + +# Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = warning + +# Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = warning + +# Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = warning + +# Avoid redundant length argument +dotnet_diagnostic.CA1514.severity = warning + +# Consider making public types internal +# Reported as a real build warning (not a suggestion) so accessibility hygiene cannot be ignored: +# a public type in an application or test assembly should be made internal rather than suppressed. +dotnet_diagnostic.CA1515.severity = warning + +# Naming Rules (CA1700-CA1727 and IDE0130) +# Naming rules support adherence to the naming conventions of the .NET design guidelines + + +[*.{cs,vb}] + +# Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = warning + +# Do not name enum values 'Reserved' +dotnet_code_quality.CA1700.api_surface = public, protected +dotnet_diagnostic.CA1700.severity = error + +# Identifiers should not contain underscores +dotnet_code_quality.CA1707.api_surface = public, protected +dotnet_diagnostic.CA1707.severity = error + +# Identifiers should differ by more than case +dotnet_diagnostic.CA1708.severity = error + +# Identifiers should have correct suffix +dotnet_diagnostic.CA1710.severity = error + +# Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1711.severity = error + +# Do not prefix enum values with type name +dotnet_diagnostic.CA1712.severity = error + +# Events should not have Before or After prefix +dotnet_diagnostic.CA1713.severity = error + +# Flags enums should have plural names +dotnet_diagnostic.CA1714.severity = error + +# Identifiers should have correct prefix +dotnet_diagnostic.CA1715.severity = error + +# Identifiers should not match keywords +dotnet_diagnostic.CA1716.severity = error + +# Only FlagsAttribute enums should have plural names +dotnet_diagnostic.CA1717.severity = error + +# Identifiers should not contain type names +dotnet_diagnostic.CA1720.severity = warning + +# Property names should not match get methods +dotnet_diagnostic.CA1721.severity = error + +# Type names should not match namespaces +dotnet_diagnostic.CA1724.severity = warning + +# Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = warning + +# Use PascalCase for named placeholders +dotnet_diagnostic.CA1727.severity = error + +# Performance Rules (CA1802-CA1877) +# Performance rules support high-performance libraries and applications + + + +[*.{cs,vb}] + +# Use Literals Where Appropriate +dotnet_diagnostic.CA1802.severity = suggestion + +# Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = error + +# Do not ignore method results +dotnet_diagnostic.CA1806.severity = error + +# Initialize reference type static fields inline +dotnet_diagnostic.CA1810.severity = suggestion + +# Avoid uninstantiated internal classes +dotnet_diagnostic.CA1812.severity = suggestion + +# Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = suggestion + +# Prefer jagged arrays over multidimensional +dotnet_diagnostic.CA1814.severity = warning + +# Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning + +# Properties should not return arrays +dotnet_diagnostic.CA1819.severity = warning +dotnet_code_quality.CA1819.api_surface = public, protected + +# Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = error + +# Remove empty finalizers +dotnet_diagnostic.CA1821.severity = error + +# Mark members as static +dotnet_diagnostic.CA1822.severity = suggestion + +# Avoid unused private fields +dotnet_diagnostic.CA1823.severity = error + +# Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_diagnostic.CA1824.severity = suggestion + +# Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = suggestion + +# Use property instead of Linq Enumerable method +dotnet_diagnostic.CA1826.severity = error + +# Do not use Count/LongCount when Any can be used +dotnet_diagnostic.CA1827.severity = silent + +# Do not use CountAsync/LongCountAsync when AnyAsync can be used +dotnet_diagnostic.CA1828.severity = silent + +# Use Length/Count property instead of Enumerable.Count() method +dotnet_diagnostic.CA1829.severity = error + +# Prefer strongly-typed Append and Insert method overloads on StringBuilder +dotnet_diagnostic.CA1830.severity = warning + +# Use AsSpan instead of Range-based indexers for string when appropriate +dotnet_diagnostic.CA1831.severity = error + +# Use AsSpan or AsMemory instead of Range-based indexers for getting ReadOnlySpan or ReadOnlyMemory portion of an array * +dotnet_diagnostic.CA1832.severity = error + +# Use AsSpan or AsMemory instead of Range-based indexers for getting Span or Memory portion of an array +dotnet_diagnostic.CA1833.severity = error + +# Use StringBuilder.Append(char) for single character strings +dotnet_diagnostic.CA1834.severity = suggestion + +# Prefer the memory-based overloads of ReadAsync/WriteAsync methods in stream-based classes +dotnet_diagnostic.CA1835.severity = warning + +# Prefer IsEmpty over Count when available +dotnet_diagnostic.CA1836.severity = error + +# Use Environment.ProcessId instead of Process.GetCurrentProcess().Id +dotnet_diagnostic.CA1837.severity = suggestion + +# Avoid StringBuilder parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = warning + +# Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName +dotnet_diagnostic.CA1839.severity = error + +# Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId +dotnet_diagnostic.CA1840.severity = silent + +# Prefer Dictionary Contains methods +dotnet_diagnostic.CA1841.severity = silent + +# Do not use 'WhenAll' with a single task + * +dotnet_diagnostic.CA1842.severity = error + +# Do not use 'WaitAll' with a single task + * +dotnet_diagnostic.CA1843.severity = error + +# Provide memory-based overrides of async methods when subclassing 'Stream' * +dotnet_diagnostic.CA1844.severity = warning + +# Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = error + +# Prefer AsSpan over Substring +dotnet_diagnostic.CA1846.severity = error + +# CA1852 (seal internal types) is intentionally not suppressed for internal types that are exposed +# to other assemblies through InternalsVisibleTo: those types must still be sealed. The blanket +# suppression that used to sit here relied on a malformed key, so it never even applied. + +# Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2352.severity = error + +# Unsafe DataSet or DataTable in serializable type +dotnet_diagnostic.CA2353.severity = error + +# Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attack +dotnet_diagnostic.CA2354.severity = error + +# Unsafe DataSet or DataTable in deserialized object graph +dotnet_diagnostic.CA2355.severity = error + +# Unsafe DataSet or DataTable type in web deserialized object graph +dotnet_diagnostic.CA2356.severity = error + +# Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data +dotnet_diagnostic.CA2361.severity = error + +# Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2362.severity = error + +# Review code for SQL injection vulnerabilities +dotnet_diagnostic.CA3001.severity = warning + +# Review code for XSS vulnerabilities +dotnet_diagnostic.CA3002.severity = warning + +# Review code for file path injection vulnerabilities +dotnet_diagnostic.CA3003.severity = warning + +# Review code for information disclosure vulnerabilities +dotnet_diagnostic.CA3004.severity = warning + +# Review code for LDAP injection vulnerabilities +dotnet_diagnostic.CA3005.severity = error + +# Review code for process command injection vulnerabilities +dotnet_diagnostic.CA3006.severity = error + +# Review code for open redirect vulnerabilities +dotnet_diagnostic.CA3007.severity = warning + +# Review code for XPath injection vulnerabilities +dotnet_diagnostic.CA3008.severity = error + +# Review code for XML injection vulnerabilities +dotnet_diagnostic.CA3009.severity = error + +# Review code for XAML injection vulnerabilities +dotnet_diagnostic.CA3010.severity = error + +# Review code for DLL injection vulnerabilities +dotnet_diagnostic.CA3011.severity = warning + +# Review code for regex injection vulnerabilities +dotnet_diagnostic.CA3012.severity = warning + +# Do Not Add Schema By URL +dotnet_diagnostic.CA3061.severity = warning + +# Insecure DTD processing in XML +dotnet_diagnostic.CA3075.severity = warning + +# Insecure XSLT script processing +dotnet_diagnostic.CA3076.severity = warning + +# Insecure Processing in API Design, XmlDocument and XmlTextReader +dotnet_diagnostic.CA3077.severity = warning + +# Mark Verb Handlers With Validate Antiforgery Token +dotnet_diagnostic.CA3147.severity = warning + +# Do Not Use Weak Cryptographic Algorithms +dotnet_diagnostic.CA5350.severity = warning + +# Do Not Use Broken Cryptographic Algorithms +dotnet_diagnostic.CA5351.severity = error + +# Do Not Use Unsafe Cipher Modes +dotnet_diagnostic.CA5358.severity = warning + +# Do Not Disable Certificate Validation +dotnet_diagnostic.CA5359.severity = warning + +# Do Not Call Dangerous Methods In Deserialization +dotnet_diagnostic.CA5360.severity = warning + +# Do Not Disable SChannel Use of Strong Crypto +dotnet_diagnostic.CA5361.severity = warning + +# Do Not Refer Self In Serializable Class +dotnet_diagnostic.CA5362.severity = error + +# Do Not Disable Request Validation +dotnet_diagnostic.CA5363.severity = warning + +# Do Not Use Deprecated Security Protocols +dotnet_diagnostic.CA5364.severity = error + +# Do Not Disable HTTP Header Checking +dotnet_diagnostic.CA5365.severity = warning + +# Use XmlReader For DataSet Read Xml +dotnet_diagnostic.CA5366.severity = warning + +# Do Not Serialize Types With Pointer Fields +dotnet_diagnostic.CA5367.severity = warning + +# Set ViewStateUserKey For Classes Derived From Page +dotnet_diagnostic.CA5368.severity = warning + +# Use XmlReader For Deserialize +dotnet_diagnostic.CA5369.severity = warning + +# Use XmlReader For Validating Reader +dotnet_diagnostic.CA5370.severity = warning + +# Use XmlReader For Schema Read +dotnet_diagnostic.CA5371.severity = warning + +# Use XmlReader For XPathDocument +dotnet_diagnostic.CA5372.severity = warning + +# Do not use obsolete key derivation function +dotnet_diagnostic.CA5373.severity = warning + +# Do Not Use XslTransform +dotnet_diagnostic.CA5374.severity = warning + +# Do Not Use Account Shared Access Signature +dotnet_diagnostic.CA5375.severity = warning + +# Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5376.severity = warning + +# Use Container Level Access Policy +dotnet_diagnostic.CA5377.severity = warning + +# Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5378.severity = warning + +# Do Not Use Weak Key Derivation Function Algorithm +dotnet_diagnostic.CA5379.severity = warning + +# Do Not Add Certificates To Root Store +dotnet_diagnostic.CA5380.severity = warning + +# Ensure Certificates Are Not Added To Root Store +dotnet_diagnostic.CA5381.severity = warning + +# Use Secure Cookies In ASP.Net Core +dotnet_diagnostic.CA5382.severity = warning + +# Ensure Use Secure Cookies In ASP.Net Core +dotnet_diagnostic.CA5383.severity = warning + +# Do Not Use Digital Signature Algorithm (DSA) +dotnet_diagnostic.CA5384.severity = warning + +# Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size +dotnet_diagnostic.CA5385.severity = warning + +# Avoid hardcoding SecurityProtocolType value +dotnet_diagnostic.CA5386.severity = warning + +# Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +dotnet_diagnostic.CA5387.severity = warning + +# Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +dotnet_diagnostic.CA5388.severity = warning + +# Do Not Add Archive Item's Path To The Target File System Path +dotnet_diagnostic.CA5389.severity = warning + +# Do Not Hard Code Encryption Key +dotnet_diagnostic.CA5390.severity = error + +# Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_diagnostic.CA5391.severity = warning + +# Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_diagnostic.CA5392.severity = warning + +# Do not use unsafe DllImportSearchPath value +dotnet_diagnostic.CA5393.severity = warning + +# Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = warning + +# Miss HttpVerb attribute for action methods +dotnet_diagnostic.CA5395.severity = warning + +# Set HttpOnly to true for HttpCookie +dotnet_diagnostic.CA5396.severity = warning + +# Do not use deprecated SslProtocols values +dotnet_diagnostic.CA5397.severity = warning + +# Avoid hardcoded SslProtocols values +dotnet_diagnostic.CA5398.severity = warning + +# Definitely disable HttpClient certificate revocation list check +dotnet_diagnostic.CA5399.severity = warning + +# Ensure HttpClient certificate revocation list check is not disabled +dotnet_diagnostic.CA5400.severity = warning + +# Do not use CreateEncryptor with non-default IV +dotnet_diagnostic.CA5401.severity = suggestion + +# Use CreateEncryptor with the default IV +dotnet_diagnostic.CA5402.severity = warning + +# Do not hard-code certificate +dotnet_diagnostic.CA5403.severity = error + +# Do not disable token validation checks +dotnet_diagnostic.CA5404.severity = error + +# Do not always skip token validation in delegates +dotnet_diagnostic.CA5405.severity = error + +dotnet_diagnostic.SEC001.severity = error + +# SingleFile Rules (IL3000-IL3005) +# Single-file rules support single-file applications + + +[*.{cs,vb}] + +# Avoid using accessing Assembly file path when publishing as a single-file +dotnet_diagnostic.IL3000.severity = error + +# Avoid accessing Assembly file path when publishing as a single file +dotnet_diagnostic.IL3001.severity = error + +# Avoid calling members annotated with 'RequiresAssemblyFilesAttribute' when publishing as a single file +dotnet_diagnostic.IL3002.severity = error + +# RequiresAssemblyFilesAttribute annotations must match across all interface implementations or overrides +dotnet_diagnostic.IL3003.severity = error + +# RequiresAssemblyFilesAttribute cannot be placed directly on application entry point +dotnet_diagnostic.IL3005.severity = error + +# Style Rules (IDE0001-IDE0380) +# Style rules support consistent code style in your codebase + + +[*.{cs,vb}] + +# Simplify name +dotnet_diagnostic.IDE0001.severity = warning + +# Simplify member access +dotnet_diagnostic.IDE0002.severity = warning + +# this and Me preferences +dotnet_diagnostic.IDE0003.severity = suggestion +dotnet_diagnostic.IDE0009.severity = suggestion + +# Remove unnecessary cast +dotnet_diagnostic.IDE0004.severity = error + +# Remove unnecessary import +dotnet_diagnostic.IDE0005.severity = warning + +# Add missing cases to switch statement +dotnet_diagnostic.IDE0010.severity = warning + +# Add braces +dotnet_diagnostic.IDE0011.severity = silent + +# Use throw expression +dotnet_diagnostic.IDE0016.severity = warning + +# Use object initializers +dotnet_diagnostic.IDE0017.severity = suggestion + +# Inline variable declaration +dotnet_diagnostic.IDE0018.severity = suggestion + +# Use pattern matching to avoid 'as' followed by a 'null' check +dotnet_diagnostic.IDE0019.severity = warning + +# Use pattern matching to avoid 'is' check followed by a cast +dotnet_diagnostic.IDE0020.severity = warning + +# Use expression body for constructors +dotnet_diagnostic.IDE0021.severity = silent + +# Use expression body for methods +dotnet_diagnostic.IDE0022.severity = silent + +# Use expression body for operators +dotnet_diagnostic.IDE0023.severity = silent +dotnet_diagnostic.IDE0024.severity = silent + +# Use expression body for properties +dotnet_diagnostic.IDE0025.severity = silent + +# Use expression body for indexers +dotnet_diagnostic.IDE0026.severity = suggestion + +# Use expression body for accessors +dotnet_diagnostic.IDE0027.severity = silent + +# Use collection initializers +dotnet_diagnostic.IDE0028.severity = suggestion + +# Use coalesce expression +dotnet_diagnostic.IDE0029.severity = suggestion + +# Use coalesce expression (nullable types) +dotnet_diagnostic.IDE0030.severity = suggestion + +# Use null propagation +dotnet_diagnostic.IDE0031.severity = suggestion + +# Use auto property +dotnet_diagnostic.IDE0032.severity = suggestion + +# Use explicitly provided tuple name +dotnet_diagnostic.IDE0033.severity = suggestion + +# Simplify 'default' expression +dotnet_diagnostic.IDE0034.severity = suggestion + +# Remove unreachable code +dotnet_diagnostic.IDE0035.severity = error + +# Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# Use inferred member name +dotnet_diagnostic.IDE0037.severity = suggestion + +# Use pattern matching to avoid is check followed by a cast (without variable) +dotnet_diagnostic.IDE0038.severity = warning + +# Use local function instead of lambda +dotnet_diagnostic.IDE0039.severity = suggestion + +# IDE0040 is declared exactly once, in the "Accessibility modifiers" section above. A second entry +# here used to override that value (later entries win), which made the effective modifier policy +# ambiguous and let repos believe the rule had been turned off. + +# Use is null check +dotnet_diagnostic.IDE0041.severity = suggestion + +# Deconstruct variable declaration +dotnet_diagnostic.IDE0042.severity = suggestion + +# Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# Add readonly modifier +dotnet_diagnostic.IDE0044.severity = warning + +# Use conditional expression for assignment +dotnet_diagnostic.IDE0045.severity = suggestion + +# Use conditional expression for return +dotnet_diagnostic.IDE0046.severity = suggestion + +# Parentheses preferences +dotnet_diagnostic.IDE0047.severity = warning +dotnet_diagnostic.IDE0048.severity = warning + +# Use language keywords instead of framework type names for type references +dotnet_diagnostic.IDE0049.severity = warning + +# Convert anonymous type to tuple +dotnet_diagnostic.IDE0050.severity = suggestion + +# Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# Remove unread private member +dotnet_diagnostic.IDE0052.severity = error + +# Use expression body for lambdas +dotnet_diagnostic.IDE0053.severity = suggestion + +# Use compound assignment +dotnet_diagnostic.IDE0054.severity = suggestion + +# Fix formatting +dotnet_diagnostic.IDE0055.severity = suggestion + +# Use index operator +dotnet_diagnostic.IDE0056.severity = suggestion + +# Use range operator +dotnet_diagnostic.IDE0057.severity = suggestion + +# Remove unnecessary expression value +dotnet_diagnostic.IDE0058.severity = silent + +# Remove unnecessary value assignment +dotnet_diagnostic.IDE0059.severity = error + +# Remove unused parameter +dotnet_diagnostic.IDE0060.severity = error + +# Use expression body for local functions +dotnet_diagnostic.IDE0061.severity = suggestion + +# Make local function static +dotnet_diagnostic.IDE0062.severity = suggestion + +# Use simple 'using' statement +dotnet_diagnostic.IDE0063.severity = suggestion + +# Make struct fields writable +dotnet_diagnostic.IDE0064.severity = suggestion + +# 'using' directive placement +dotnet_diagnostic.IDE0065.severity = error + +# Use switch expression +dotnet_diagnostic.IDE0066.severity = warning + +# Use 'System.HashCode.Combine' +dotnet_diagnostic.IDE0070.severity = warning + +# Add missing cases to switch expression +dotnet_diagnostic.IDE0072.severity = suggestion + +# Require file header +dotnet_diagnostic.IDE0073.severity = warning + +# Use compound assignment +dotnet_diagnostic.IDE0074.severity = suggestion + +# Simplify conditional expression +dotnet_diagnostic.IDE0075.severity = suggestion + +# Remove invalid global 'SuppressMessageAttribute' +dotnet_diagnostic.IDE0076.severity = silent + +# Avoid legacy format target in global 'SuppressMessageAttribute' +dotnet_diagnostic.IDE0077.severity = warning + +# Use pattern matching +dotnet_diagnostic.IDE0078.severity = warning + +# Remove unnecessary suppression +dotnet_diagnostic.IDE0079.severity = error + +# Remove unnecessary suppression operator +dotnet_diagnostic.IDE0080.severity = suggestion + +# Remove ByVal +dotnet_diagnostic.IDE0081.severity = error + +# Convert typeof to nameof +dotnet_diagnostic.IDE0082.severity = warning + +# Use pattern matching (not operator) +dotnet_diagnostic.IDE0083.severity = warning + +# Use pattern matching (IsNot operator) +dotnet_diagnostic.IDE0084.severity = suggestion + +# Simplify new expression +dotnet_diagnostic.IDE0090.severity = warning + +# Remove unnecessary equality operator +dotnet_diagnostic.IDE0100.severity = silent + +# Use conditional delegate call +dotnet_diagnostic.IDE0105.severity = warning + +# Remove unnecessary discard +dotnet_diagnostic.IDE0110.severity = warning + +# Simplify LINQ expression +dotnet_diagnostic.IDE0120.severity = warning + +# Simplify LINQ type check and cast +dotnet_diagnostic.IDE0121.severity = warning + +# Simplify object creation +dotnet_diagnostic.IDE0140.severity = suggestion + +# Prefer 'null' check over type check +dotnet_diagnostic.IDE0150.severity = warning + +# Use block-scoped namespace +dotnet_diagnostic.IDE0160.severity = suggestion + +# Use file-scoped namespace +dotnet_diagnostic.IDE0161.severity = suggestion + +# Simplify property pattern +dotnet_diagnostic.IDE0170.severity = warning + +# Use tuple to swap values +dotnet_diagnostic.IDE0180.severity = suggestion + +# Unnecessary lambda expression +dotnet_diagnostic.IDE0200.severity = warning + +# Convert to top-level statements +dotnet_diagnostic.IDE0210.severity = warning + +# Convert to 'Program.Main' style program +dotnet_diagnostic.IDE0211.severity = suggestion + +# Add explicit cast in foreach loop +dotnet_diagnostic.IDE0220.severity = error + +# Use UTF-8 string literal +dotnet_diagnostic.IDE0230.severity = warning + +# Nullable directive is redundant +dotnet_diagnostic.IDE0240.severity = error + +# Nullable directive is unnecessary +dotnet_diagnostic.IDE0241.severity = error + +# Struct can be made 'readonly' +dotnet_diagnostic.IDE0250.severity = warning + +# Member can be made 'readonly' +dotnet_diagnostic.IDE0251.severity = warning + +# Use pattern matching +dotnet_diagnostic.IDE0260.severity = warning + +# Null check can be simplified +dotnet_diagnostic.IDE0270.severity = warning + +# Use 'nameof' +dotnet_diagnostic.IDE0280.severity = error + +# Use primary constructor +dotnet_diagnostic.IDE0290.severity = suggestion + +# Use collection expression for array +dotnet_diagnostic.IDE0300.severity = warning + +# Use collection expression for empty +dotnet_diagnostic.IDE0301.severity = suggestion + +# Use collection expression for stackalloc +dotnet_diagnostic.IDE0302.severity = warning + +# Use collection expression for Create() +dotnet_diagnostic.IDE0303.severity = warning + +# Use collection expression for builder +dotnet_diagnostic.IDE0304.severity = warning + +# Use collection expression for fluent +dotnet_diagnostic.IDE0305.severity = warning + +# Use collection expression for new +dotnet_diagnostic.IDE0306.severity = warning + +# Make anonymous function static +dotnet_diagnostic.IDE0320.severity = warning + +# Prefer 'System.Threading.Lock' +dotnet_diagnostic.IDE0330.severity = warning + +# Use unbound generic type +dotnet_diagnostic.IDE0340.severity = warning + +# Use implicitly typed lambda +dotnet_diagnostic.IDE0350.severity = warning + +# Simplify property accessor +dotnet_diagnostic.IDE0360.severity = suggestion + +# Remove unnecessary `unsafe` modifier +dotnet_diagnostic.IDE0380.severity = warning + +# Remove unnecessary suppression (null-forgiving operator) +dotnet_diagnostic.IDE0370.severity = warning + +# Naming rule violation - must stay visible: the field-naming policy is only enforced when this +# diagnostic is reported, because naming rules without their own severity inherit this one. +dotnet_diagnostic.IDE1006.severity = warning + +# Embedded statements must be on their own line +dotnet_diagnostic.IDE2001.severity = warning + +# Consecutive braces must not have blank line between them +dotnet_diagnostic.IDE2002.severity = warning + +# Blank line required between block and subsequent statement +dotnet_diagnostic.IDE2003.severity = warning + +# Blank line not allowed after constructor initializer colon +dotnet_diagnostic.IDE2004.severity = warning + +# Blank line not allowed after conditional expression token +dotnet_diagnostic.IDE2005.severity = warning + +# Blank line not allowed after arrow expression clause token +dotnet_diagnostic.IDE2006.severity = warning + +# C# Style Rules - var preferences + +[*.{cs,csx,cake}] + +dotnet_diagnostic.IDE0007.severity = warning +dotnet_diagnostic.IDE0008.severity = none +dotnet_diagnostic.PDS0003.severity = warning +dotnet_diagnostic.PDS0004.severity = warning + +# Usage Rules (CA1801, CA1816, CA2200-CA2267) +# Usage rules support proper usage of .NET + + +[*.{cs,vb}] + +# Review unused parameters +dotnet_diagnostic.CA1801.severity = warning + +# Call GC.SuppressFinalize correctly +dotnet_diagnostic.CA1816.severity = error + +# Rethrow to preserve stack details +dotnet_diagnostic.CA2200.severity = error + +# Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = error + +# Initialize value type static fields inline +dotnet_diagnostic.CA2207.severity = warning + +# Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = error + +# Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = error + +# Disposable fields should be disposed +dotnet_diagnostic.CA2213.severity = error + +# Do not call overridable methods in constructors +dotnet_diagnostic.CA2214.severity = warning + +# Dispose methods should call base class dispose +dotnet_diagnostic.CA2215.severity = error + +# Disposable types should declare finalizer +dotnet_diagnostic.CA2216.severity = warning + +# Do not mark enums with FlagsAttribute +dotnet_diagnostic.CA2217.severity = error + +# Override GetHashCode on overriding Equals +dotnet_diagnostic.CA2218.severity = warning + +# Do not raise exceptions in exception clauses +dotnet_diagnostic.CA2219.severity = error + +# Override Equals on overloading operator equals +dotnet_diagnostic.CA2224.severity = warning + +# Operator overloads have named alternates +dotnet_diagnostic.CA2225.severity = warning + +# Operators should have symmetrical overloads +dotnet_diagnostic.CA2226.severity = warning + +# Collection properties should be read only +dotnet_diagnostic.CA2227.severity = error + +# Implement serialization constructors +dotnet_diagnostic.CA2229.severity = warning + +# Overload operator equals on overriding ValueType.Equals +dotnet_diagnostic.CA2231.severity = warning + +# Pass System.Uri objects instead of strings +dotnet_diagnostic.CA2234.severity = warning + +# Mark all non-serializable fields +dotnet_diagnostic.CA2235.severity = suggestion + +# Mark ISerializable types with SerializableAttribute +dotnet_diagnostic.CA2237.severity = warning + +# Provide correct arguments to formatting methods +dotnet_diagnostic.CA2241.severity = warning + +# Test for NaN correctly +dotnet_diagnostic.CA2242.severity = warning + +# Attribute string literals should parse correctly +dotnet_diagnostic.CA2243.severity = warning + +# Do not duplicate indexed element initializations +dotnet_diagnostic.CA2244.severity = warning + +# Do not assign a property to itself +dotnet_diagnostic.CA2245.severity = error + +# Do not assign a symbol and its member in the same statement +dotnet_diagnostic.CA2246.severity = warning + +# Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +dotnet_diagnostic.CA2247.severity = warning + +# Provide correct enum argument to Enum.HasFlag +dotnet_diagnostic.CA2248.severity = warning + +# Consider using String.Contains instead of String.IndexOf +dotnet_diagnostic.CA2249.severity = error + +# Use ThrowIfCancellationRequested +dotnet_diagnostic.CA2250.severity = warning + +# Use String.Equals over String.Compare +dotnet_diagnostic.CA2251.severity = warning + +# Opt in to preview features +dotnet_diagnostic.CA2252.severity = suggestion + +# Named placeholders should not be numeric values +dotnet_diagnostic.CA2253.severity = warning + +# Template should be a static expression +dotnet_diagnostic.CA2254.severity = warning + +# The ModuleInitializer attribute should not be used in libraries +dotnet_diagnostic.CA2255.severity = warning + +# All members declared in parent interfaces must have an implementation in a DynamicInterfaceCastableImplementation-attributed interface +dotnet_diagnostic.CA2256.severity = warning + +# Members defined on an interface with 'DynamicInterfaceCastableImplementationAttribute' should be 'static' +dotnet_diagnostic.CA2257.severity = warning + +# Providing a 'DynamicInterfaceCastableImplementation' interface in Visual Basic is unsupported +dotnet_diagnostic.CA2258.severity = warning + +# Ensure ThreadStatic is only used with static fields +dotnet_diagnostic.CA2259.severity = error + +# Implement generic math interfaces correctly +dotnet_diagnostic.CA2260.severity = warning + +# Do not use ConfigureAwaitOptions.SuppressThrowing with Task +dotnet_diagnostic.CA2261.severity = warning + +# Set 'MaxResponseHeadersLength' properly +dotnet_diagnostic.CA2262.severity = warning + +# Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA2264.severity = warning + +# Do not compare Span to null or default +dotnet_diagnostic.CA2265.severity = error + +# Use correct type parameter +dotnet_diagnostic.CA2266.severity = warning + +# Use correct type parameter for 'IAsyncEnumerable.ConfigureAwait' +dotnet_diagnostic.CA2267.severity = warning + +# External Analyzer Rules +# Rules from StyleCop, Async, Dispose, Regex, JSON, MSTest, and other third-party analyzers + + + +[*.{cs,vb}] + +# StyleCop Analyzer Rules (SA) + +# A violation of this rule occurs when a compilation (project) contains files with DocumentationMode set to None +dotnet_diagnostic.SA0001.severity = warning + +# The spacing around a C# keyword is incorrect +dotnet_diagnostic.SA1000.severity = warning + +# DoNotPrefixCallsWithBaseUnlessLocalImplementationExists +dotnet_diagnostic.SA1100.severity = warning + +# StatementMustNotUseUnnecessaryParenthesis +dotnet_diagnostic.SA1119.severity = suggestion + +# CommentsMustContainText +dotnet_diagnostic.SA1120.severity = warning + +# DoNotUseRegions +dotnet_diagnostic.SA1124.severity = warning + +# UsingDirectivesMustBePlacedCorrectly +dotnet_diagnostic.SA1200.severity = warning + +# ElementsMustAppearInTheCorrectOrder +dotnet_diagnostic.SA1201.severity = warning + +# ElementsMustBeOrderedByAccess +dotnet_diagnostic.SA1202.severity = warning + +# ElementMustBeginWithUpperCaseLetter +dotnet_diagnostic.SA1300.severity = error + +# InterfaceNamesMustBeginWithI +dotnet_diagnostic.SA1302.severity = error + +# ConstFieldNamesMustBeginWithUpperCaseLetter +dotnet_diagnostic.SA1303.severity = error + +# FieldNamesMustNotUseHungarianNotation +dotnet_diagnostic.SA1305.severity = warning + +# VariableNamesMustNotBePrefixed +dotnet_diagnostic.SA1308.severity = error + +# A field name in C# begins with an underscore +dotnet_diagnostic.SA1309.severity = silent + +# VariableNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1312.severity = warning + +# ParameterNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1313.severity = error + +# TupleElementNamesShouldUseCorrectCasing +dotnet_diagnostic.SA1316.severity = warning + +# AccessModifierMustBeDeclared +dotnet_diagnostic.SA1400.severity = warning + +# FieldsMustBePrivate +dotnet_diagnostic.SA1401.severity = error + +# FileMayOnlyContainASingleNamespace +dotnet_diagnostic.SA1403.severity = error + +# A Code Analysis SuppressMessage attribute does not include a justification +dotnet_diagnostic.SA1404.severity = error + +# DebugAssertMustProvideMessageText +dotnet_diagnostic.SA1405.severity = warning + +# DebugFailMustProvideMessageText +dotnet_diagnostic.SA1406.severity = warning + +# RemoveUnnecessaryCode +dotnet_diagnostic.SA1409.severity = error + +# BracesForMultiLineStatementsMustNotShareLine +dotnet_diagnostic.SA1500.severity = error + +# ElementMustNotBeOnSingleLine +dotnet_diagnostic.SA1502.severity = error + +# BracesMustNotBeOmitted +dotnet_diagnostic.SA1503.severity = error + +# ClosingBraceMustBeFollowedByBlankLine +dotnet_diagnostic.SA1513.severity = warning + +# UseBracesConsistently +dotnet_diagnostic.SA1520.severity = error + +# File header copyright text should match +dotnet_diagnostic.SA1636.severity = none + +# ElementDocumentationMustBeSpelledCorrectly +dotnet_diagnostic.SA1650.severity = warning + +# Dispose Analyzer Rules (IDISP) + +# Dispose created +dotnet_diagnostic.IDISP001.severity = error + +# Dispose member +dotnet_diagnostic.IDISP002.severity = error + +# Dispose previous before re-assigning +dotnet_diagnostic.IDISP003.severity = error + +# Don't ignore created IDisposable +dotnet_diagnostic.IDISP004.severity = error + +# Return type should indicate that the value should be disposed +dotnet_diagnostic.IDISP005.severity = error + +# Implement IDisposable +dotnet_diagnostic.IDISP006.severity = error + +# Don't dispose injected +dotnet_diagnostic.IDISP007.severity = error + +# Don't assign member with injected and created disposables +dotnet_diagnostic.IDISP008.severity = error + +# Add IDisposable interface +dotnet_diagnostic.IDISP009.severity = error + +# Call base.Dispose(disposing) +dotnet_diagnostic.IDISP010.severity = error + +# Don't return disposed instance +dotnet_diagnostic.IDISP011.severity = warning + +# Property should not return created disposable +dotnet_diagnostic.IDISP012.severity = error + +# Await in using +dotnet_diagnostic.IDISP013.severity = error + +# Use a single instance of HttpClient +dotnet_diagnostic.IDISP014.severity = warning + +# Member should not return created and cached instance +dotnet_diagnostic.IDISP015.severity = error + +# Don't use disposed instance +dotnet_diagnostic.IDISP016.severity = error + +# Prefer using +dotnet_diagnostic.IDISP017.severity = error + +# Inline variable declaration +dotnet_diagnostic.IDISP018.severity = error + +# Call SuppressFinalize +dotnet_diagnostic.IDISP019.severity = error + +# Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDISP020.severity = error + +# Call this.Dispose(true) +dotnet_diagnostic.IDISP021.severity = error + +# Call this.Dispose(false) +dotnet_diagnostic.IDISP022.severity = error + +# Don't use reference types in finalizer context +dotnet_diagnostic.IDISP023.severity = error + +# Don't call GC.SuppressFinalize(this) when the type is sealed and has no finalizer +dotnet_diagnostic.IDISP024.severity = error + +# Class with no virtual dispose method should be sealed +dotnet_diagnostic.IDISP025.severity = error + +# Class with no virtual DisposeAsyncCore method should be sealed +dotnet_diagnostic.IDISP026.severity = error + +# Async Analyzer Rules (ASYNC, VSTHRD, RCS) + +# Asynchronous method names should end with Async +dotnet_diagnostic.ASYNC0001.severity = error + +# Non asynchronous method names should not end with Async +dotnet_diagnostic.ASYNC0002.severity = error + +# Avoid void returning asynchronous method +dotnet_diagnostic.ASYNC0003.severity = warning + +# Use ConfigureAwait(false) on await expression +dotnet_diagnostic.ASYNC0004.severity = error + +# Do not use blocking call (make method async) +dotnet_diagnostic.MA0045.severity = none + +# Call 'ConfigureAwait(false)' +dotnet_diagnostic.RCS1090.severity = error + +# Return completed task instead of returning null +dotnet_diagnostic.RCS1210.severity = error + +# AsyncifyInvocation: Use Task Async +dotnet_diagnostic.AsyncifyInvocation.severity = error + +# AsyncifyVariable: Use Task Async +dotnet_diagnostic.AsyncifyVariable.severity = error + +# Avoid legacy thread switching methods +dotnet_diagnostic.VSTHRD001.severity = error + +# Avoid problematic synchronous waits +dotnet_diagnostic.VSTHRD002.severity = error + +# Avoid awaiting foreign Tasks +dotnet_diagnostic.VSTHRD003.severity = error + +# Await SwitchToMainThreadAsync +dotnet_diagnostic.VSTHRD004.severity = error + +# Invoke single-threaded types on Main thread +dotnet_diagnostic.VSTHRD010.severity = error + +# Use AsyncLazy +dotnet_diagnostic.VSTHRD011.severity = error + +# Provide JoinableTaskFactory where allowed +dotnet_diagnostic.VSTHRD012.severity = error + +# Avoid async void methods +dotnet_diagnostic.VSTHRD100.severity = error + +# Avoid unsupported async delegates +dotnet_diagnostic.VSTHRD101.severity = error + +# Implement internal logic asynchronously +dotnet_diagnostic.VSTHRD102.severity = error + +# Call async methods when in an async method +dotnet_diagnostic.VSTHRD103.severity = error + +# Offer async option +dotnet_diagnostic.VSTHRD104.severity = warning + +# Avoid method overloads that assume TaskScheduler.Current +dotnet_diagnostic.VSTHRD105.severity = warning + +# Use InvokeAsync to raise async events +dotnet_diagnostic.VSTHRD106.severity = error + +# Await Task within using expression +dotnet_diagnostic.VSTHRD107.severity = error + +# Assert thread affinity unconditionally +dotnet_diagnostic.VSTHRD108.severity = error + +# Switch instead of assert in async methods +dotnet_diagnostic.VSTHRD109.severity = error + +# Observe result of async calls +dotnet_diagnostic.VSTHRD110.severity = error + +# Use .ConfigureAwait(bool) +dotnet_diagnostic.VSTHRD111.severity = error + +# Implement System.IAsyncDisposable +dotnet_diagnostic.VSTHRD112.severity = error + +# Check for System.IAsyncDisposable +dotnet_diagnostic.VSTHRD113.severity = error + +# Avoid returning a null Task +dotnet_diagnostic.VSTHRD114.severity = error + +# Use "Async" suffix for async methods +dotnet_diagnostic.VSTHRD200.severity = error + +# Asynchronous method name should end with 'Async' +dotnet_diagnostic.RCS1046.severity = error + +# Non-asynchronous method name should not end with 'Async' +dotnet_diagnostic.RCS1047.severity = error + +# Class Analyzer Rules + +# Seal Class +dotnet_diagnostic.CLASS0001.severity = warning + +# Enum Analyzer Rules + +# Default switch label +dotnet_diagnostic.ENUM0001.severity = error + +# Merge switch sections +dotnet_diagnostic.ENUM0002.severity = warning + +# Populate switch +dotnet_diagnostic.ENUM0003.severity = warning + +# Regex Analyzer Rules (RE) + +# Invalid regex pattern +dotnet_diagnostic.RE0001.severity = warning + +# JSON Analyzer Rules + +# Invalid JSON pattern +dotnet_diagnostic.JSON001.severity = suggestion + +# Probable JSON string detected +dotnet_diagnostic.JSON002.severity = suggestion + +# Return Analyzer Rules + +# Do not return null +dotnet_diagnostic.RETURN0001.severity = warning + +# Roslynator Analyzer Rules (RCS) + +# Simplify boolean comparison +dotnet_diagnostic.RCS1049.severity = silent + +# Remove unnecessary 'Imports' or 'using' directive +dotnet_diagnostic.RemoveUnnecessaryImportsFixable.severity = warning + +# MSTest Analyzer Rules (MSTEST) + +# Explicitly enable or disable tests parallelization +dotnet_diagnostic.MSTEST0001.severity = warning + +# Test classes should have valid layout +dotnet_diagnostic.MSTEST0002.severity = warning + +# Test methods should have valid layout +dotnet_diagnostic.MSTEST0003.severity = warning + +# Public types should be test classes +dotnet_diagnostic.MSTEST0004.severity = none + +# Test context property should have valid layout +dotnet_diagnostic.MSTEST0005.severity = warning + +# Avoid [ExpectedException] +dotnet_diagnostic.MSTEST0006.severity = warning + +# Use test attributes only on test methods +dotnet_diagnostic.MSTEST0007.severity = warning + +# TestInitialize method should have valid layout +dotnet_diagnostic.MSTEST0008.severity = warning + +# TestCleanup method should have valid layout +dotnet_diagnostic.MSTEST0009.severity = warning + +# ClassInitialize method should have valid layout +dotnet_diagnostic.MSTEST0010.severity = warning + +# ClassCleanup method should have valid layout +dotnet_diagnostic.MSTEST0011.severity = warning + +# AssemblyInitialize method should have valid layout +dotnet_diagnostic.MSTEST0012.severity = warning + +# AssemblyCleanup method should have valid layout +dotnet_diagnostic.MSTEST0013.severity = warning + +# DataRow should be valid +dotnet_diagnostic.MSTEST0014.severity = warning + +# Test method should not be ignored +dotnet_diagnostic.MSTEST0015.severity = warning + +# Test class should have test method +dotnet_diagnostic.MSTEST0016.severity = warning + +# Assertion arguments should be passed in the correct order +dotnet_diagnostic.MSTEST0017.severity = warning + +# DynamicData should be valid +dotnet_diagnostic.MSTEST0018.severity = warning + +# Prefer TestInitialize methods over constructors +dotnet_diagnostic.MSTEST0019.severity = warning + +# Prefer constructors over TestInitialize methods +dotnet_diagnostic.MSTEST0020.severity = warning + +# Prefer Dispose over TestCleanup methods +dotnet_diagnostic.MSTEST0021.severity = warning + +# Prefer TestCleanup methods over Dispose +dotnet_diagnostic.MSTEST0022.severity = warning + +# Do not negate boolean assertions +dotnet_diagnostic.MSTEST0023.severity = warning + +# Do not store TestContext in a static member +dotnet_diagnostic.MSTEST0024.severity = warning + +# Use 'Assert.Fail' instead of an always-failing assert +dotnet_diagnostic.MSTEST0025.severity = warning + +# Avoid conditional access in assertions +dotnet_diagnostic.MSTEST0026.severity = warning + +# Non-nullable reference not initialized suppressor +dotnet_diagnostic.MSTEST0027.severity = none + +# Non-nullable reference not initialized suppressor +dotnet_diagnostic.MSTEST0028.severity = none + +# Public method should be test method +dotnet_diagnostic.MSTEST0029.severity = warning + +# Type containing [TestMethod] should be marked with [TestClass] +dotnet_diagnostic.MSTEST0030.severity = warning + +# System.ComponentModel.DescriptionAttribute has no effect on test methods +dotnet_diagnostic.MSTEST0031.severity = warning + +# Review or remove the assertion as its condition is known to be always true +dotnet_diagnostic.MSTEST0032.severity = warning + +# Non-nullable reference not initialized suppressor +dotnet_diagnostic.MSTEST0033.severity = none + +# Use ClassCleanupBehavior.EndOfClass with the [ClassCleanup] +dotnet_diagnostic.MSTEST0034.severity = warning + +# [DeploymentItem] can be specified only on test class or test method +dotnet_diagnostic.MSTEST0035.severity = warning + +# Do not use shadowing inside test class +dotnet_diagnostic.MSTEST0036.severity = warning + +# Use proper 'Assert' methods +dotnet_diagnostic.MSTEST0037.severity = warning + +# Don't use 'Assert.AreSame' or 'Assert.AreNotSame' with value types +dotnet_diagnostic.MSTEST0038.severity = warning + +# Use newer 'Assert.Throws' methods +dotnet_diagnostic.MSTEST0039.severity = warning + +# Do not assert inside 'async void' contexts +dotnet_diagnostic.MSTEST0040.severity = warning + +# Use 'ConditionBaseAttribute' on test classes +dotnet_diagnostic.MSTEST0041.severity = warning + +# Obsolete API Rules (SYSLIB) +# Rules for obsolete .NET APIs and platform-specific warnings + + +[*.{cs,vb}] + +# The UTF-7 encoding is insecure +dotnet_diagnostic.SYSLIB0001.severity = error + +# PrincipalPermissionAttribute is obsolete +dotnet_diagnostic.SYSLIB0002.severity = error + +# Code access security is not supported +dotnet_diagnostic.SYSLIB0003.severity = error + +# The constrained execution region (CER) feature is not supported +dotnet_diagnostic.SYSLIB0004.severity = error + +# The global assembly cache (GAC) is not supported +dotnet_diagnostic.SYSLIB0005.severity = error + +# Thread.Abort is not supported +dotnet_diagnostic.SYSLIB0006.severity = error + +# Default implementations of cryptography algorithms not supported +dotnet_diagnostic.SYSLIB0007.severity = error + +# CreatePdbGenerator is not supported +dotnet_diagnostic.SYSLIB0008.severity = error + +# The AuthenticationManager Authenticate and PreAuthenticate methods are not supported +dotnet_diagnostic.SYSLIB0009.severity = error + +# Unsupported remoting APIs +dotnet_diagnostic.SYSLIB0010.severity = error + +# BinaryFormatter serialization is obsolete +dotnet_diagnostic.SYSLIB0011.severity = error + +# Type or member is obsolete +dotnet_diagnostic.SYSLIB0012.severity = error + +# EscapeUriString is obsolete +dotnet_diagnostic.SYSLIB0013.severity = error + +# WebRequest, HttpWebRequest, ServicePoint, WebClient are obsolete +dotnet_diagnostic.SYSLIB0014.severity = error + +# DisablePrivateReflectionAttribute is obsolete +dotnet_diagnostic.SYSLIB0015.severity = error + +# GetContextInfo() is obsolete +dotnet_diagnostic.SYSLIB0016.severity = error + +# Strong-name signing is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0017.severity = error + +# Reflection-only loading is not supported and throws PlatformNotSupportedException +dotnet_diagnostic.SYSLIB0018.severity = error + +# Some RuntimeEnvironment APIs are obsolete +dotnet_diagnostic.SYSLIB0019.severity = error + +# IgnoreNullValues is obsolete +dotnet_diagnostic.SYSLIB0020.severity = error + +# Derived cryptographic types are obsolete +dotnet_diagnostic.SYSLIB0021.severity = error + +# The Rijndael and RijndaelManaged types are obsolete +dotnet_diagnostic.SYSLIB0022.severity = error + +# RNGCryptoServiceProvider is obsolete +dotnet_diagnostic.SYSLIB0023.severity = error + +# Creating and unloading AppDomains is not supported and throws an exception +dotnet_diagnostic.SYSLIB0024.severity = error + +# SuppressIldasmAttribute is obsolete +dotnet_diagnostic.SYSLIB0025.severity = error + +# X509Certificate and X509Certificate2 are immutable +dotnet_diagnostic.SYSLIB0026.severity = error + +# PublicKey.Key is obsolete +dotnet_diagnostic.SYSLIB0027.severity = error + +# X509Certificate2.PrivateKey is obsolete +dotnet_diagnostic.SYSLIB0028.severity = error + +# ProduceLegacyHmacValues is obsolete +dotnet_diagnostic.SYSLIB0029.severity = error + +# HMACSHA1 always uses the algorithm implementation provided by the platform +dotnet_diagnostic.SYSLIB0030.severity = error + +# EncodeOID is obsolete +dotnet_diagnostic.SYSLIB0031.severity = error + +# Recovery from corrupted process state exceptions is not supported +dotnet_diagnostic.SYSLIB0032.severity = error + +# Rfc2898DeriveBytes.CryptDeriveKey is obsolete +dotnet_diagnostic.SYSLIB0033.severity = error + +# CmsSigner(CspParameters) constructor is obsolete +dotnet_diagnostic.SYSLIB0034.severity = error + +# ComputeCounterSignature without specifying a CmsSigner is obsolete +dotnet_diagnostic.SYSLIB0035.severity = error + +# Regex.CompileToAssembly is obsolete +dotnet_diagnostic.SYSLIB0036.severity = error + +# AssemblyName members HashAlgorithm, ProcessorArchitecture, and VersionCompatibility are obsolete +dotnet_diagnostic.SYSLIB0037.severity = error + +# SerializationFormat.Binary is obsolete +dotnet_diagnostic.SYSLIB0038.severity = error + +# SslProtocols.Tls and SslProtocols.Tls11 are obsolete +dotnet_diagnostic.SYSLIB0039.severity = error + +# EncryptionPolicy.NoEncryption and EncryptionPolicy.AllowNoEncryption are obsolete +dotnet_diagnostic.SYSLIB0040.severity = error + +# Some Rfc2898DeriveBytes constructors are obsolete +dotnet_diagnostic.SYSLIB0041.severity = error + +# FromXmlString and ToXmlString on ECC types are obsolete +dotnet_diagnostic.SYSLIB0042.severity = error + +# ECDiffieHellmanPublicKey.ToByteArray is obsolete +dotnet_diagnostic.SYSLIB0043.severity = error + +# AssemblyName.CodeBase and AssemblyName.EscapedCodeBase are obsolete +dotnet_diagnostic.SYSLIB0044.severity = error + +# Some cryptographic factory methods are obsolete +dotnet_diagnostic.SYSLIB0045.severity = error + +# ControlledExecution.Run should not be used +dotnet_diagnostic.SYSLIB0046.severity = error + +# XmlSecureResolver is obsolete +dotnet_diagnostic.SYSLIB0047.severity = error + +# RSA.EncryptValue(Byte[]) and RSA.DecryptValue(Byte[]) are obsolete +dotnet_diagnostic.SYSLIB0048.severity = error + +# JsonSerializerOptions.AddContext is obsolete +dotnet_diagnostic.SYSLIB0049.severity = error + +# Formatter-based serialization is obsolete +dotnet_diagnostic.SYSLIB0050.severity = error + +# APIs that support obsolete formatter-based serialization are obsolete +dotnet_diagnostic.SYSLIB0051.severity = error + +# APIs that support obsolete mechanisms for Regex extensibility are obsolete +dotnet_diagnostic.SYSLIB0052.severity = error + +# AesGcm should indicate the required tag size for encryption and decryption +dotnet_diagnostic.SYSLIB0053.severity = error + +# Thread.VolatileRead and Thread.VolatileWrite are obsolete +dotnet_diagnostic.SYSLIB0054.severity = warning + +# AdvSimd.ShiftRightLogicalRoundedNarrowingSaturate* methods with signed parameters are obsolete +dotnet_diagnostic.SYSLIB0055.severity = warning + +# Assembly.LoadFrom that takes an AssemblyHashAlgorithm is obsolete +dotnet_diagnostic.SYSLIB0056.severity = warning + +# X509Certificate2 and X509Certificate constructors for binary and file content are obsolete +dotnet_diagnostic.SYSLIB0057.severity = warning + +# The KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherAlgorithmStrength, HashAlgorithm, and HashStrength properties of SslStream are obsolete +dotnet_diagnostic.SYSLIB0058.severity = warning + +# SystemEvents.EventsThreadShutdown callbacks aren't run before the process exits +dotnet_diagnostic.SYSLIB0059.severity = warning + +# Rfc2898DeriveBytes constructors are obsolete +dotnet_diagnostic.SYSLIB0060.severity = error + +# System.Linq.Queryable.MaxBy and System.Linq.Queryable.MinBy taking an IComparer are obsolete +dotnet_diagnostic.SYSLIB0061.severity = error + +# Source Generator Diagnostics (SYSLIB1xxx) + +# Logging method names can't start with an underscore +dotnet_diagnostic.SYSLIB1001.severity = error + +# Don't include log level parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1002.severity = error + +# Logging method parameter names can't start with an underscore +dotnet_diagnostic.SYSLIB1003.severity = error + +# Could not find a required type definition +dotnet_diagnostic.SYSLIB1005.severity = error + +# Multiple logging methods cannot use the same event ID +dotnet_diagnostic.SYSLIB1006.severity = error + +# Logging methods must return void +dotnet_diagnostic.SYSLIB1007.severity = error + +# One of the arguments to a logging method must implement the ILogger interface +dotnet_diagnostic.SYSLIB1008.severity = error + +# Logging methods must be static +dotnet_diagnostic.SYSLIB1009.severity = error + +# Logging methods must be partial +dotnet_diagnostic.SYSLIB1010.severity = error + +# Logging methods cannot be generic +dotnet_diagnostic.SYSLIB1011.severity = error + +# Redundant qualifier in logging message +dotnet_diagnostic.SYSLIB1012.severity = error + +# Don't include exception parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1013.severity = error + +# Logging template has no corresponding method argument +dotnet_diagnostic.SYSLIB1014.severity = error + +# Argument is not referenced from the logging message +dotnet_diagnostic.SYSLIB1015.severity = error + +# Logging methods cannot have a body +dotnet_diagnostic.SYSLIB1016.severity = error + +# A LogLevel value must be supplied in the LoggerMessage attribute or as a parameter to the logging method +dotnet_diagnostic.SYSLIB1017.severity = error + +# Don't include logger parameters as templates in the logging message +dotnet_diagnostic.SYSLIB1018.severity = error + +# Couldn't find a field of type ILogger +dotnet_diagnostic.SYSLIB1019.severity = error + +# Found multiple fields of type ILogger +dotnet_diagnostic.SYSLIB1020.severity = error + +# Multiple message-template item names differ only by case +dotnet_diagnostic.SYSLIB1021.severity = error + +# Can't have malformed format strings +dotnet_diagnostic.SYSLIB1022.severity = error + +# Generating more than six arguments is not supported +dotnet_diagnostic.SYSLIB1023.severity = error + +# System.Text.Json source generator did not generate output for type +dotnet_diagnostic.SYSLIB1030.severity = error + +# System.Text.Json source generator encountered a duplicate type info property name +dotnet_diagnostic.SYSLIB1031.severity = error + +# Context classes to be augmented by the System.Text.Json source generator must be declared as partial +dotnet_diagnostic.SYSLIB1032.severity = error + +# System.Text.Json source generator encountered a type with multiple [JsonConstructor] annotations +dotnet_diagnostic.SYSLIB1033.severity = warning + +# JsonSourceGenerator encountered a [JsonStringEnumConverter] annotation +dotnet_diagnostic.SYSLIB1034.severity = warning + +# System.Text.Json source generator encountered a type with multiple [JsonExtensionData] annotations +dotnet_diagnostic.SYSLIB1035.severity = error + +# System.Text.Json source generator encountered an invalid [JsonExtensionData] annotation +dotnet_diagnostic.SYSLIB1036.severity = error + +# System.Text.Json source generator encountered a type with init-only properties which are not supported for deserialization +dotnet_diagnostic.SYSLIB1037.severity = error + +# System.Text.Json source generator encountered a property annotated with [JsonInclude] but with inaccessible accessors +dotnet_diagnostic.SYSLIB1038.severity = error + +# Invalid GeneratedRegexAttribute usage +dotnet_diagnostic.SYSLIB1040.severity = error + +# Multiple GeneratedRegexAttribute attributes were applied to the same method, but only one is allowed +dotnet_diagnostic.SYSLIB1041.severity = error + +# The specified regular expression is invalid +dotnet_diagnostic.SYSLIB1042.severity = error + +# A GeneratedRegexAttribute method must be partial, parameterless, non-generic, and non-abstract, and return Regex +dotnet_diagnostic.SYSLIB1043.severity = error + +# The regex generator couldn't generate a complete source implementation for the specified regular expression due to an internal limitation +dotnet_diagnostic.SYSLIB1044.severity = error + +# Use GeneratedRegexAttribute to generate the regular expression implementation at compile time +dotnet_diagnostic.SYSLIB1045.severity = warning + +# Invalid LibraryImportAttribute usage +dotnet_diagnostic.SYSLIB1050.severity = error + +# The specified type is not supported by source-generated p/invokes +dotnet_diagnostic.SYSLIB1051.severity = error + +# The specified configuration is not supported by source-generated p/invokes +dotnet_diagnostic.SYSLIB1052.severity = error + +# The specified LibraryImportAttribute arguments cannot be forwarded to DllImportAttribute +dotnet_diagnostic.SYSLIB1053.severity = error + +# Use LibraryImportAttribute instead of DllImportAttribute to generate p/invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1054.severity = error + +# Invalid CustomMarshallerAttribute usage +dotnet_diagnostic.SYSLIB1055.severity = error + +# The specified native type is invalid +dotnet_diagnostic.SYSLIB1056.severity = error + +# The marshaller type does not have the required shape +dotnet_diagnostic.SYSLIB1057.severity = error + +# Invalid NativeMarshallingAttribute usage +dotnet_diagnostic.SYSLIB1058.severity = error + +# The marshaller type does not support an allocating constructor +dotnet_diagnostic.SYSLIB1059.severity = error + +# The specified marshaller type is invalid +dotnet_diagnostic.SYSLIB1060.severity = error + +# The marshaller type has incompatible method signatures +dotnet_diagnostic.SYSLIB1061.severity = error + +# The project must be updated with true +dotnet_diagnostic.SYSLIB1062.severity = error + +# Invalid JSImportAttribute usage +dotnet_diagnostic.SYSLIB1070.severity = error + +# Invalid JSExportAttribute usage +dotnet_diagnostic.SYSLIB1071.severity = error + +# The specified type is not supported by source-generated JavaScript interop +dotnet_diagnostic.SYSLIB1072.severity = error + +# The specified configuration is not supported by source-generated JavaScript interop +dotnet_diagnostic.SYSLIB1073.severity = error + +# JSImportAttribute requires unsafe code +dotnet_diagnostic.SYSLIB1074.severity = error + +# JSExportAttribute requires unsafe code +dotnet_diagnostic.SYSLIB1075.severity = error + +# Invalid GeneratedComInterfaceAttribute usage +dotnet_diagnostic.SYSLIB1090.severity = error + +# Method is declared in different partial declaration than the GeneratedComInterface attribute +dotnet_diagnostic.SYSLIB1091.severity = error + +# Usage of LibraryImport or GeneratedComInterface attribute does not follow recommendation +dotnet_diagnostic.SYSLIB1092.severity = error + +# Analysis for COM interface generation has failed +dotnet_diagnostic.SYSLIB1093.severity = error + +# The base COM interface failed to generate source. Code will not be generated for this interface +dotnet_diagnostic.SYSLIB1094.severity = error + +# Invalid GeneratedComClassAttribute usage +dotnet_diagnostic.SYSLIB1095.severity = error + +# Use GeneratedComInterfaceAttribute instead of ComImportAttribute to generate COM marshalling code at compile time +dotnet_diagnostic.SYSLIB1096.severity = error + +# This type implements at least one type with the GeneratedComInterfaceAttribute attribute +dotnet_diagnostic.SYSLIB1097.severity = error + +# .NET COM hosting with EnableComHosting only supports built-in COM interop +dotnet_diagnostic.SYSLIB1098.severity = error + +# COM Interop APIs on System.Runtime.InteropServices.Marshal do not support source-generated COM and will fail at run time +dotnet_diagnostic.SYSLIB1099.severity = error + +# Type is not supported +dotnet_diagnostic.SYSLIB1100.severity = error + +# Property on type is not supported +dotnet_diagnostic.SYSLIB1101.severity = error + +# Project's language version must be at least C# 11 +dotnet_diagnostic.SYSLIB1102.severity = error + +# Value types are invalid inputs to configuration 'Bind' methods +dotnet_diagnostic.SYSLIB1103.severity = error + +# Generator cannot determine the target configuration type +dotnet_diagnostic.SYSLIB1104.severity = error + +# Can't use ValidateObjectMembersAttribute or ValidateEnumeratedItemsAttribute on fields or properties with open generic types +dotnet_diagnostic.SYSLIB1201.severity = error + +# A member type has no fields or properties to validate +dotnet_diagnostic.SYSLIB1202.severity = error + +# A type has no fields or properties to validate +dotnet_diagnostic.SYSLIB1203.severity = error + +# Deriving from a GeneratedComInterface-attributed interface defined in another assembly is not supported +dotnet_diagnostic.SYSLIB1230.severity = error + +# Other Platform Diagnostics + +# SVE is a preview feature can be used by enabling EnablePreviewFeatures flag +dotnet_diagnostic.SYSLIB5003.severity = error + +# CS9035 - should be warning or error +dotnet_diagnostic.CS9035.severity = none + +# For Verify package based content +[*.{received,verified}.{cs,txt}] +charset = "utf-8-bom" +end_of_line = lf +indent_size = unset +indent_style = unset +insert_final_newline = false +tab_width = unset +trim_trailing_whitespace = false + +# The following sections are about +# ignoring certain rules for specific files or folders, such as settings, models, DTOs, etc. +# including when using the Extensions/ folder for matching non-local namespace extensions such as +# - Extensions/System/Linq/EnumerableExtensions.cs +# - Extensions/Microsoft/Extensions/DepdendencyInjection/ServiceCollectionExtensions.cs + +# If it's a settings, model, dto, etc file, ignore the 'properties' cannot have arrays +# and other annoying rules - the following section is duplicated +[**/*{ValueObjects,Settings,Options,Model,Models,DTO,Entity,Response,Request}{.Nav,}.{cs,vb}] +dotnet_diagnostic.CA1002.severity = none +dotnet_diagnostic.CA1024.severity = none +dotnet_diagnostic.CA1056.severity = none +dotnet_diagnostic.CA1819.severity = none +dotnet_diagnostic.CA2227.severity = none +dotnet_diagnostic.CS8618.severity = none +dotnet_diagnostic.CS8767.severity = none + +[**/{ValueObjects,Migrations,Entities,Models,DTOs,Settings,Options,Configuration,Requests,Responses}/**.{cs,vb}] +dotnet_diagnostic.CA1002.severity = none +dotnet_diagnostic.CA1024.severity = none +dotnet_diagnostic.CA1056.severity = none +dotnet_diagnostic.CA1819.severity = none +dotnet_diagnostic.CA2227.severity = none +dotnet_diagnostic.CS8618.severity = none +dotnet_diagnostic.CS8767.severity = none + +[**/Migrations/**.{cs,vb}] +# EF Core migrations (and their .Designer.cs snapshots) are generated; any diagnostics are worthless. +generated_code = true +dotnet_diagnostic.CA1062.severity = none +dotnet_diagnostic.CA1825.severity = none +dotnet_diagnostic.CA1861.severity = none +dotnet_diagnostic.IDE0053.severity = none +dotnet_diagnostic.IDE0300.severity = none +dotnet_diagnostic.RCS1021.severity = none +dotnet_diagnostic.RCS1205.severity = none +# PDS0004: Use correct acronym capitalization +dotnet_diagnostic.PDS0004.severity = none + +[**/Extensions/**.{cs,vb}] +# Files under the Extensions/ namespace-reset convention must not force developers to add pragmas; +# suppress only the namespace-conflict diagnostics that arise from declaring framework-style +# namespaces. Accessibility/design rules (for example CA1034) are deliberately not suppressed here. +dotnet_diagnostic.IDE0130.severity = none +dotnet_diagnostic.CA1724.severity = none +dotnet_diagnostic.CS0436.severity = none +dotnet_diagnostic.CS1591.severity = none +dotnet_diagnostic.IDE0005.severity = none + +[**/Generated/**/*.{cs,vb}] +generated_code = true +dotnet_diagnostic.CS8602.severity = none + +[**/*.Designer.cs] +# Designer/design-time generated content +generated_code = true + +[**/obj/**/*.cs] +[**/bin/**/*.cs] +# Compiler/build output (generated AssemblyInfo, GlobalUsings, etc.) +generated_code = true From 334473f52bec283cc7ef3f118d5d7c3b3e6a47c4 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 30 Sep 2026 17:27:35 +0100 Subject: [PATCH 3/4] docs: added docs and linted solution --- .github/copilot-instructions.md | 25 ++++ .github/workflows/pr.yml | 30 ++++ .github/workflows/release.yml | 22 +++ .gitignore | 2 +- AGENTS.md | 134 ++++++++++++++++++ Justfile | 7 +- README.md | 39 +++-- .../{architecture.md => wiki/Architecture.md} | 2 +- .../Contributing-Modules.md} | 2 +- docs/wiki/Contributing.md | 68 +++++++++ docs/wiki/Getting-Started.md | 95 +++++++++++++ docs/wiki/Home.md | 83 +++++++++++ docs/{lifecycle.md => wiki/Lifecycle.md} | 0 docs/{modules.md => wiki/Modules.md} | 3 +- docs/{networking.md => wiki/Networking.md} | 0 docs/wiki/Packaging.md | 68 +++++++++ docs/wiki/Release-Flow.md | 57 ++++++++ docs/wiki/Testing.md | 52 +++++++ .../Wait-Strategies.md} | 0 .../Wslc-Api-Investigation.md} | 0 .../Wslc-Capability-Matrix.md} | 0 docs/wiki/_Sidebar.md | 14 ++ docs/wiki/index.md | 20 +++ mkdocs.yml | 21 +++ purview-build.json | 71 ++++++++++ spikes/WslcSpikes/Program.cs | 4 +- .../Spikes/ConcurrentExecProcesses.cs | 24 ++-- .../WslcSpikes/Spikes/ConcurrentSharedPath.cs | 20 ++- src/Directory.Build.props | 17 +++ src/WSLTestContainers.slnx | 4 +- src/src/Azurite/AzuriteBuilder.cs | 3 +- src/src/Azurite/Sdk/README.md | 46 ++++++ src/src/MinIo/MinIO.csproj | 13 -- src/src/MinIo/MinIOBuilder.cs | 105 -------------- src/src/MinIo/MinIOConfiguration.cs | 13 -- src/src/MinIo/MinIOContainer.cs | 35 ----- src/src/MsSql/MsSqlBuilder.cs | 5 +- src/src/MsSql/Sdk/README.md | 55 +++++++ src/src/MySql/MySqlBuilder.cs | 5 +- src/src/MySql/Sdk/README.md | 55 +++++++ src/src/Nats/Sdk/README.md | 44 ++++++ src/src/PostgreSql/PostgreSqlBuilder.cs | 5 +- src/src/PostgreSql/Sdk/README.md | 49 +++++++ src/src/RabbitMq/RabbitMqBuilder.cs | 5 +- src/src/RabbitMq/RabbitMqContainer.cs | 2 +- src/src/RabbitMq/Sdk/README.md | 56 ++++++++ src/src/Redis/Sdk/README.md | 51 +++++++ .../WslContainers/ContainerConfiguration.cs | 2 +- .../Diagnostics/SecretRedactor.cs | 11 +- .../Runtime/WslContainerException.cs | 21 +-- src/src/WslContainers/Sdk/README.md | 104 ++++++++++++++ .../WslContainers/Waiting/HttpWaitStrategy.cs | 4 +- .../Waiting/TcpPortWaitStrategy.cs | 6 +- .../Waiting/WaitStrategyRunner.cs | 22 +-- src/src/WslContainers/WslInspectParser.cs | 5 +- .../AzuriteIntegrationTests.cs | 2 +- .../Azurite.UnitTests/AzuriteBuilderTests.cs | 19 ++- .../MinIO.IntegrationTests.csproj | 1 - .../MinIOIntegrationTests.cs | 62 -------- .../MinIo.UnitTests/MinIO.UnitTests.csproj | 1 - .../MinIo.UnitTests/MinIOBuilderTests.cs | 34 ----- .../MsSqlIntegrationTests.cs | 12 +- .../MsSql.UnitTests/MsSqlBuilderTests.cs | 2 +- .../MySqlIntegrationTests.cs | 2 +- .../MySql.UnitTests/MySqlBuilderTests.cs | 2 +- .../NatsIntegrationTests.cs | 2 +- src/tests/Nats.UnitTests/NatsBuilderTests.cs | 2 +- .../PostgreSqlIntegrationTests.cs | 2 +- .../PostgreSqlBuilderTests.cs | 2 +- .../RabbitMqIntegrationTests.cs | 10 +- .../RabbitMqBuilderTests.cs | 2 +- .../RedisIntegrationTests.cs | 6 +- .../Redis.UnitTests/RedisBuilderTests.cs | 6 +- src/tests/SharedTestingFramework/WslcTest.cs | 4 +- .../AlpineLifecycleTests.cs | 8 +- .../CleanupTests.cs | 8 +- .../ConcurrencyStressTests.cs | 3 +- .../ExecTests.cs | 3 +- .../PerformanceTests.cs | 13 +- .../PortMappingTests.cs | 3 +- .../RuntimeInfoTests.cs | 3 +- .../SharedStorageTests.cs | 31 ++-- .../VolumeTests.cs | 3 +- .../WaitStrategyTests.cs | 3 +- .../WslContainers.IntegrationTests.csproj | 2 +- .../ContainerBuilderTests.cs | 2 +- .../WslContainers.UnitTests/FakeContainer.cs | 2 +- .../HttpWaitStrategyTests.cs | 56 ++++---- .../WslContainers.UnitTests/ImageTests.cs | 2 +- .../WslContainers.UnitTests/SecretTests.cs | 2 +- .../StorageModeTests.cs | 2 +- .../WaitContextTests.cs | 5 +- .../WaitStrategyRunnerTests.cs | 12 +- .../WaitStrategyTests.cs | 19 +-- .../WslContainerRuntimeTests.cs | 2 +- .../WslInspectParserTests.cs | 5 +- 96 files changed, 1467 insertions(+), 501 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/release.yml create mode 100644 AGENTS.md rename docs/{architecture.md => wiki/Architecture.md} (99%) rename docs/{contributing-modules.md => wiki/Contributing-Modules.md} (95%) create mode 100644 docs/wiki/Contributing.md create mode 100644 docs/wiki/Getting-Started.md create mode 100644 docs/wiki/Home.md rename docs/{lifecycle.md => wiki/Lifecycle.md} (100%) rename docs/{modules.md => wiki/Modules.md} (96%) rename docs/{networking.md => wiki/Networking.md} (100%) create mode 100644 docs/wiki/Packaging.md create mode 100644 docs/wiki/Release-Flow.md create mode 100644 docs/wiki/Testing.md rename docs/{wait-strategies.md => wiki/Wait-Strategies.md} (100%) rename docs/{wslc-api-investigation.md => wiki/Wslc-Api-Investigation.md} (100%) rename docs/{wslc-capability-matrix.md => wiki/Wslc-Capability-Matrix.md} (100%) create mode 100644 docs/wiki/_Sidebar.md create mode 100644 docs/wiki/index.md create mode 100644 mkdocs.yml create mode 100644 purview-build.json create mode 100644 src/src/Azurite/Sdk/README.md delete mode 100644 src/src/MinIo/MinIO.csproj delete mode 100644 src/src/MinIo/MinIOBuilder.cs delete mode 100644 src/src/MinIo/MinIOConfiguration.cs delete mode 100644 src/src/MinIo/MinIOContainer.cs create mode 100644 src/src/MsSql/Sdk/README.md create mode 100644 src/src/MySql/Sdk/README.md create mode 100644 src/src/Nats/Sdk/README.md create mode 100644 src/src/PostgreSql/Sdk/README.md create mode 100644 src/src/RabbitMq/Sdk/README.md create mode 100644 src/src/Redis/Sdk/README.md create mode 100644 src/src/WslContainers/Sdk/README.md delete mode 100644 src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj delete mode 100644 src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs delete mode 100644 src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj delete mode 100644 src/tests/MinIo.UnitTests/MinIOBuilderTests.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ea2e258 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,25 @@ +# GitHub Copilot Instructions + +## Primary instruction source + +Use the repository root [`AGENTS.md`](../AGENTS.md) as the **primary** source of truth for behavior, +architecture context, testing standards, and completion criteria. + +If this file and `AGENTS.md` appear to conflict, prefer `AGENTS.md` unless this file explicitly states a +GitHub Copilot-only exception. + +## Copilot-specific guidance + +This file should only contain **GitHub Copilot-specific** instruction details. +Keep product, architecture, and general engineering standards centralized in `AGENTS.md`. + +## Operating expectations for Copilot + +- Apply the `AGENTS.md` testing bar strictly: TUnit tests, `*.UnitTests`/`*.IntegrationTests` project + placement, and the rule that WSLC integration modules run serially. +- Treat work as incomplete until the relevant tests pass; unit tests need no WSLC host, integration tests do. +- Never weaken the WSLC session/store invariants (single shared session, serialised session mutations, the + shared-store fallback, `Secret` redaction) to make a test pass. +- Consult the repository `.agents/` folder for additional skills/workflows that may improve execution + quality. Skill content is delivered by NuGet packages and is read-only here. +- Keep edits minimal, focused, and aligned with existing SDK and repository conventions. diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..3959ea1 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,30 @@ +name: PR + +on: + pull_request: + branches: [main] + # `ready_for_review` is not one of the default activity types, so a draft PR would never + # re-evaluate the required build gate once it is marked ready for review. + types: [opened, synchronize, reopened, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and test + uses: purview-dev/build/.github/workflows/purview-build.yml@main + with: + # The shared workflow defaults to the 10.0.x SDK, but this repository targets net11.0 and pins + # the .NET 11 SDK in global.json. Without the matching SDK the runner cannot resolve global.json + # ("A compatible .NET SDK was not found") and every step fails. + # Keep in sync with `sdk.version` in global.json. + dotnet-version: "11.0.100-rc.1.26425.128" + # Mirrors Build:TestFilter in purview-build.json: the SDK categorises unit test projects as Unit, + # so the WSLC integration suites are never selected and the runner needs no WSLC host. + test-filter: "/*/*/*/*[Category=Unit]" + # All eight library projects are packable, so pack and validate the produced packages. + run-pack: true + validate-pack: true + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..655aa10 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + release: + name: Release packages + uses: purview-dev/build/.github/workflows/purview-release.yml@main + with: + # The shared workflow defaults to the 10.0.x SDK, but this repository targets net11.0 and pins + # the .NET 11 SDK in global.json; without it the release job cannot resolve global.json. + # Keep in sync with `sdk.version` in global.json (and with .github/workflows/pr.yml). + dotnet-version: "11.0.100-rc.1.26425.128" + release-mode: NuGet + release-branch: main + secrets: inherit diff --git a/.gitignore b/.gitignore index 22c0210..35511eb 100644 --- a/.gitignore +++ b/.gitignore @@ -657,4 +657,4 @@ sketch !scripts/* -./.tools/purview-build/**/* \ No newline at end of file +.tools/purview-build/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..da4a905 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,134 @@ +# Agent Instructions + +## 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 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. +- `.github/copilot-instructions.md` defers to this file. +- Follow explicit user instructions first, then the nearest applicable repository instructions, then + established code patterns. +- Operate only in this repository unless the user explicitly expands the scope. +- Never read, copy, log, or commit secrets from excluded files, environment variables, user profiles, local + configuration, test output, or registry credentials. + +## Source of truth and layout + +| 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/**` would ship MSBuild assets | +| `src/tests` | TUnit unit (`*.UnitTests`) and WSLC integration (`*.IntegrationTests`) projects | +| `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 | +| `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 | +| `Justfile` | Supported local workflow commands | +| `.agents` | Package-delivered skills, agents and prompts (read-only here) | +| `.purview/agent-sync.cache` | SDK-managed manifest recording which `.agents` files are already mirrored | + +## Standard workflow + +1. Read this file, inspect the working tree, and locate the implementation, tests, documentation and existing + patterns relevant to the task. +2. Confirm behaviour from code and tests rather than relying on memory or documentation alone. +3. Make the smallest coherent change. Preserve public behaviour unless the task explicitly changes it. +4. Update tests for fixes and behaviour changes. Update documentation when public behaviour changes. +5. Run the narrowest meaningful validation first, then broader validation in proportion to risk. +6. Review the diff for unrelated edits, generated noise, compatibility risks, and missing docs or tests. + +## Runtime invariants + +- **One shared, process-wide session** (`WslContainerRuntime.Instance`), lazily started, named + `wslc-{pid}-{random8}`. Container isolation comes from unique names and unique host ports, not from extra + sessions. +- **Images are shared by default** (`StorageMode.Shared` → `%LOCALAPPDATA%\Purview\WslContainers\images`). A + session exclusively locks its `storage.vhdx`; the runtime verifies the store once and falls back to an + isolated per-process store on a sharing violation (`0x80070020`). Do not remove that fallback. +- **All session-mutating operations are serialised** through the runtime's gate (concurrent `Start` can fail + with `0x8000FFFF`). Keep new lifecycle paths inside it. +- **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`). +- **`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). + +## Module rules + +- A module supplies only defaults: image, ports, environment, module configuration (`WithXxx`), a readiness + 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`. +- 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. + +## Packaging and validation + +- Every project under `src/src` sets `true` **literally** in the `.csproj`: the SDK + 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`. +- `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. +- Validate with `just pack` and `just pipeline-pack-validate`. + +## Testing + +- Test assemblies are categorised by the SDK: `*.UnitTests` → `Unit`, `*.IntegrationTests` → `Integration`. +- `purview-build.json` filters pipeline runs to `[Category=Unit]`, so the shared pipeline never needs a WSLC + host; keep it that way unless the task requires real containers. +- WSLC integration tests run **serially** (`--max-parallel-test-modules 1` in the `Justfile`): a session + 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. + +## Local commands + +```text +just build # dotnet build (Debug) +just test '/*/*/*/*[Category=Unit]' # unit tests only +just lint-check / just lint-fix # CSharpier check / format +just pack # build + dotnet pack into ./artifacts +just pipeline-pack-validate # restore, build, lint, test, pack, validate +just scrub # reset bin/obj, clean, forced restore, build-server shutdown +``` + +Commit messages follow Conventional Commits enforced by the `commit-msg` lefthook +(`.config/lefthook.yml` → `npx commitlint`); allowed types are in `commitlint.config.mts`. + +## Versioning and releases + +- `package.json` is the authoritative release and package version. Do not diverge project versions by hand. +- `.github/workflows/pr.yml` builds and tests pull requests through the shared + `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 + `purview-build.json` pointing at `src/WSLTestContainers.slnx`. + +## Completion checklist + +Before handing work back: + +- Confirm the requested behaviour and scope are satisfied, and only intended files changed. +- Review public API, package-content and dependency-direction implications. +- Update the affected package `Sdk/README.md`, the root `README.md`, `docs/wiki` (plus `_Sidebar.md` and + `mkdocs.yml` for new pages), `AGENTS.md` and `purview-build.json` when the change affects them. +- Watch for the packaging traps: a new packable project missing `true` or a + `RequiredContent` entry, and a new `Sdk/README.md` that does not describe its own package. +- Run the appropriate build, test, formatting and pack checks in proportion to risk. +- State exactly what validation ran. If a check was skipped or blocked, give the concrete reason and the + remaining risk. diff --git a/Justfile b/Justfile index cc6e479..0486d93 100644 --- a/Justfile +++ b/Justfile @@ -1,5 +1,10 @@ set quiet +export TESTINGPLATFORM_EXITCODE_IGNORE := "8" +export DOTNET_CLI_TELEMETRY_OPTOUT := "1" +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE := "1" +export DO_NOT_TRACK := "1" + root_folder := "./src/" solution_file := root_folder + "WSLTestContainers.slnx" test_solution := solution_file @@ -87,7 +92,7 @@ restore *args: # Test modules run serially by default: a WSLC session exclusively locks its image-store VHD, so parallel # test assemblies contend for the shared store and fail with 0x80070020 ("file is being used by another # process"). Override by appending the flag, e.g. -# just test '/*/*/*/*/' --max-parallel-test-modules 4 +# just test '/*/*/*/*/' --max-parallel-test-modules 4 [group('Build and Test')] test filter="/*/*/*/*/" *args: echo "Running tests for {{ BLUE }}{{ test_solution }}{{ NORMAL }} with {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}..." diff --git a/README.md b/README.md index b7762a4..38a2862 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,22 @@ A **WSLC-native** Testcontainers-style library for .NET that runs throwaway Linu 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. > **Status: Phase 8 in progress.** Core runtime, wait strategies, the `Image`/`Tag` parser, registry auth, -> observability, hardening, and **eight service modules** (PostgreSQL, Redis, SQL Server, RabbitMQ, Azurite, -> NATS, MySQL, MinIO). **Images are shared by default** (`StorageMode.Shared`): sessions reuse a stable +> 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. 69+ unit tests pass across all modules. ## Prerequisites - Windows 10/11 -- **WSL ≥ 2.9.3** with WSL Containers, installed via `wsl --install --no-distribution` +- **WSL ≥ 3.0.1.0** with WSL Containers, installed via `wsl --install --no-distribution` - .NET SDK 11 (the repo pins `11.0.100-rc.1`) Verify: ```powershell -wsl --version # needs 2.9.3+ -wslc version # prints e.g. 3.0.1.0 +wsl --version # needs 3.0.1.0+ +wslc version # needs 3.0.1.0+ ``` The library reports missing prerequisites via `WslContainerRuntime.GetInfoAsync()`; it never installs or updates WSL on its own. @@ -77,7 +77,7 @@ string amqp = rabbitMq.GetConnectionString(); ## Key design decisions (verified by spikes) | Decision | Evidence | -|---|---| +| --- | --- | | One shared process-wide session, shared storage path | image store is keyed by storage path; session start ~20 ms (S2); concurrent sessions can't share the VHD (S18) → auto-fallback to isolated store | | Unique session names `wslc-{pid}-{rand}` | session names are machine-reserved (S1, S13) | | Default `NetworkingMode = Bridged` | port mappings require Bridged; default is `none` (S4) | @@ -99,7 +99,6 @@ src/ 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) - WslContainers.MinIo/ MinIO module (builder, container, S3 API + console endpoints) tests/ SharedTestingFramework/ shared WSLC skip/helper for integration tests WslContainers.UnitTests/ @@ -117,11 +116,29 @@ tests/ spikes/ WslcSpikes/ Phase 0 investigation harness (run: see below) docs/ - architecture.md wslc-api-investigation.md wslc-capability-matrix.md - lifecycle.md networking.md wait-strategies.md modules.md - contributing-modules.md + wiki/ project wiki (mkdocs.yml -> docs_dir: docs/wiki) + index.md Home.md _Sidebar.md Getting-Started.md Testing.md + Architecture.md Lifecycle.md Networking.md Wait-Strategies.md Modules.md + Packaging.md Release-Flow.md Contributing.md Contributing-Modules.md + Wslc-Api-Investigation.md Wslc-Capability-Matrix.md ``` +## Documentation + +The project documentation lives in [`docs/wiki`](docs/wiki/Home.md) and is published as a MkDocs site +(`mkdocs.yml`, `docs_dir: docs/wiki`, aggregated by the purview-dev website): + +- [Getting Started](docs/wiki/Getting-Started.md) — prerequisites, first container, first module. +- [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. +- [Testing](docs/wiki/Testing.md) — unit vs integration categories and the serial WSLC test rule. +- [Packaging](docs/wiki/Packaging.md) and [Release Flow](docs/wiki/Release-Flow.md) — what ships and how it is released. +- [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. + The PostgreSQL, Redis, SQL Server and RabbitMQ modules work today: ```csharp @@ -215,4 +232,4 @@ dotnet run --project spikes/WslcSpikes -- sfull ## License -MIT. This project is not affiliated with, or endorsed by, the Testcontainers project or Microsoft. \ No newline at end of file +MIT. This project is not affiliated with, or endorsed by, the Testcontainers project or Microsoft. diff --git a/docs/architecture.md b/docs/wiki/Architecture.md similarity index 99% rename from docs/architecture.md rename to docs/wiki/Architecture.md index 0623a3f..572d7fe 100644 --- a/docs/architecture.md +++ b/docs/wiki/Architecture.md @@ -105,4 +105,4 @@ fallback. Isolated stores are transient and are removed when their session termi - Public surface deliberately small; WSLC types internal. - One opt-in accessor (`IWslContainerAccessor`) exposes `Inspect()` / raw handle for advanced users. -- Third-party modules subclass the generic builder (see `docs/modules.md`). \ No newline at end of file +- Third-party modules subclass the generic builder (see [Modules](Modules.md)). \ No newline at end of file diff --git a/docs/contributing-modules.md b/docs/wiki/Contributing-Modules.md similarity index 95% rename from docs/contributing-modules.md rename to docs/wiki/Contributing-Modules.md index bfeb6a4..fbf77a3 100644 --- a/docs/contributing-modules.md +++ b/docs/wiki/Contributing-Modules.md @@ -65,7 +65,7 @@ public class MyServiceBuilder : ContainerBuilder/Sdk/README.md`** — the package's own README, shipped inside the `.nupkg`. Update it + whenever a module's public API, defaults, readiness or endpoints change. +- **`docs/wiki`** — the user-facing wiki aggregated by the purview-dev website. Update the topic page + ([Architecture](Architecture.md), [Lifecycle](Lifecycle.md), [Networking](Networking.md), + [Wait Strategies](Wait-Strategies.md), [Modules](Modules.md), [Testing](Testing.md), + [Packaging](Packaging.md), [Release Flow](Release-Flow.md)) and `_Sidebar.md` when adding a page. +- **`purview-build.json`** — the exhaustive `PackValidation.RequiredContent` manifest has to keep matching + what the packages actually contain; see [Packaging](Packaging.md). +- **`README.md`** — the repository front page, when the shape of the project changes. + +## Before you raise a PR + +- Confirm the requested behaviour and scope are satisfied and that only intended files changed. +- Review public API, package-content and dependency-direction implications. +- Run `just lint-check`, the relevant tests, and `just pipeline-pack-validate` when packaging, dependencies or + package assets changed. +- State exactly what validation ran; if something could not run, say why and what risk remains. + +## Related + +- [Contributing Modules](Contributing-Modules.md) — the module contract for new service packages. +- [Packaging](Packaging.md) — package contents and validation rules. +- [Release Flow](Release-Flow.md) — versioning and what CI does on a PR and on release. diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md new file mode 100644 index 0000000..6268106 --- /dev/null +++ b/docs/wiki/Getting-Started.md @@ -0,0 +1,95 @@ +# Getting Started + +This guide installs the packages, runs a first generic container, switches to a typed service module, and +points you at the test workflow. + +## Requirements + +- Windows 10/11 with **WSL ≥ 2.9.3** including WSL Containers (`wsl --install --no-distribution`). +- .NET SDK 11 or later; the packages target `net11.0-windows10.0.19041.0` (x64). +- Verify the host with `wsl --version` (needs 2.9.3+) and `wslc version`. The library never installs or + updates WSL for you — call `WslContainerRuntime.GetInfoAsync()` to report what is missing. + +## 1. Reference a package + +Reference the core runtime directly for a generic container: + +```bash +dotnet add package Purview.WslContainers +``` + +or a service module, which depends on the core package: + +```bash +dotnet add package Purview.WslContainers.PostgreSql +``` + +## 2. Run a generic container + +```csharp +using Purview.WslContainers; +using Purview.WslContainers.Waiting; + +await using var container = new ContainerBuilder() + .WithImage("docker.io/library/redis:latest") + .WithPortBinding(6379, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForTcpPort(6379)) + .Build(); + +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). + +## 3. Use a typed module + +```csharp +using Npgsql; +using Purview.WslContainers.PostgreSql; + +await using var postgres = new PostgreSqlBuilder() + .WithDatabase("tests") + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + +await postgres.StartAsync(); // waits for pg_isready + +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 +[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) +``` + +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. + +## 5. Build and pack locally + +```powershell +just build # dotnet build of src/WSLTestContainers.slnx (Debug) +just pack # build + dotnet pack into ./artifacts +just pipeline-pack-validate # shared pipeline: restore, build, lint, test, pack, validate +``` + +See [Contributing](Contributing.md) for the full local workflow and [Release Flow](Release-Flow.md) for +versioning and publishing. + +## Next steps + +- [Architecture](Architecture.md) — the session model, concurrency rules and cleanup decisions. +- [Wait Strategies](Wait-Strategies.md) — started vs ready, and composing your own checks. +- [Networking](Networking.md) — port mapping behaviour under WSLC. +- [Packaging](Packaging.md) — what lands in each package. diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md new file mode 100644 index 0000000..21cf11d --- /dev/null +++ b/docs/wiki/Home.md @@ -0,0 +1,83 @@ +# Purview.WslContainers 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. + +This wiki is the project documentation hub. The packages are published under the +`Purview.WslContainers.*` package IDs. + +## Start here + +- [Getting Started](Getting-Started.md) +- [Architecture](Architecture.md) +- [Lifecycle](Lifecycle.md) +- [Networking](Networking.md) +- [Wait Strategies](Wait-Strategies.md) +- [Modules](Modules.md) + +## Packages + +| 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. | + +## Feature highlights + +- **One shared, process-wide session** — a stable storage path gives a warm image cache + (`StorageMode.Shared`), and the runtime transparently falls back to an isolated per-process store when a + concurrent process holds the shared store VHD. +- **Race-free random host ports** — native `windowsPort=0` allocation, read back from the container's mapped + 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. +- **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, + connection strings) on top of the core runtime; they never re-implement container infrastructure. +- **Observability and secrecy** — an `ActivitySource` spans session, pull, lifecycle, exec and wait; + credentials live in a `Secret` wrapper and never reach logs or `ToString()` output. + +## Requirements + +- Windows 10/11. +- **WSL ≥ 2.9.3** with WSL Containers, installed via `wsl --install --no-distribution`. +- .NET SDK 11 (the repository pins `11.0.100-rc.1.26425.128`). + +```powershell +wsl --version # needs 2.9.3+ +wslc version # prints e.g. 3.0.1.0 +``` + +The library reports missing prerequisites through `WslContainerRuntime.GetInfoAsync()`; it never installs or +updates WSL on its own. + +## 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/` | Service modules; each carries a bespoke `Sdk/README.md` that ships as the package README. | +| `src/tests` | TUnit unit and integration test projects. | +| `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. | +| `.agents` | Package-delivered agent skills, agents and prompts. | + +## Workflow + +- [Testing](Testing.md) — unit vs integration categories, session locking, and the serial test rule. +- [Packaging](Packaging.md) — what each package ships and how the pack manifest is validated. +- [Release Flow](Release-Flow.md) — versioning, pipeline commands and the release gate. +- [Contributing](Contributing.md) — build, lint, test and pack locally. +- [Contributing Modules](Contributing-Modules.md) — adding a new service module. diff --git a/docs/lifecycle.md b/docs/wiki/Lifecycle.md similarity index 100% rename from docs/lifecycle.md rename to docs/wiki/Lifecycle.md diff --git a/docs/modules.md b/docs/wiki/Modules.md similarity index 96% rename from docs/modules.md rename to docs/wiki/Modules.md index 4099100..ee1b3bc 100644 --- a/docs/modules.md +++ b/docs/wiki/Modules.md @@ -129,10 +129,9 @@ public sealed class PostgreSqlContainer : WslContainer | 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 | | MySQL | `mysql:8` | host `MySqlConnector` connection | MySqlConnector | ✅ implemented — uses a real connection poll (the image logs `"ready for connections"` during its temporary init server) | -| MinIO | `quay.io/minio/minio` | log `"Console:"` | S3 SDK | ✅ implemented — S3 API + console endpoints; note MinIO's official image is on Quay.io (not Docker Hub) | Note on RabbitMQ readiness: the default wait uses the canonical **`Server startup complete`** log signal rather than `rabbitmq-diagnostics ping` — under WSLC the exec-based probe races with the Erlang cookie setup and can trigger a startup failure (`eacces` reading `.erlang.cookie`). WSLC auto-provisions image `VOLUME` declarations on an ext4 device by default; bind-mounting a Windows directory onto one replaces it with drvfs, which ignores Unix `chown` and breaks permission-sensitive images — do not bind-mount onto image volumes unless you intend that. ## Custom modules (third-party) -See `docs/contributing-modules.md`. \ No newline at end of file +See [Contributing Modules](Contributing-Modules.md). \ No newline at end of file diff --git a/docs/networking.md b/docs/wiki/Networking.md similarity index 100% rename from docs/networking.md rename to docs/wiki/Networking.md diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md new file mode 100644 index 0000000..fd44694 --- /dev/null +++ b/docs/wiki/Packaging.md @@ -0,0 +1,68 @@ +# Packaging + +Every project under `src/src` is packable and ships as `Purview.WslContainers.*`; test and spike projects +never pack. Packages are produced into `./artifacts` with a matching `.snupkg`. + +## What a package contains + +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. | +| `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`). | + +Portable PDBs are delivered through the `.snupkg`, never inside the `.nupkg`. + +## The `Sdk/` folder convention + +`Purview.BuildSdk` packs a packable project's `Sdk/` folder automatically (`PurviewAutoSdkPack`): + +| Source path | Package path | +| --- | --- | +| `Sdk/*.md`, `Sdk/*.png`, `Sdk/*.svg`, … | package root | +| `Sdk/build/**`, `Sdk/buildTransitive/**`, `Sdk/buildMultiTargeting/**` | the matching package folder | +| `Sdk/**` (anything else) | `Sdk/**` | + +Two consequences matter for this repository: + +1. **Each packable project owns its documentation** in `src/src//Sdk/README.md`. Because a + README-named file is already being packed, the SDK leaves the repository-root `README.md` out of the + package, so every package ships documentation specific to itself. +2. **The icon is linked, not copied.** `src/Directory.Build.props` adds the shared logo as a `None` item + with `Link="Sdk/purview-logo-light.png"`, and the SDK packs linked `Sdk/` files at the package root and + declares ``/``. Projects must therefore keep + `true` literally in the `.csproj`: the SDK resolves `IsPackable` by scanning the + project file, and both property groups are conditioned on it. + +## Pack validation + +`just pipeline-pack-validate` runs the shared `Purview.Build` pipeline with `Build:RunPack=true`, +`Build:ValidatePack=true` and `Release:Mode=None`. `purview-build.json` configures the validation: + +- `RequireSymbolPackage` / `RequireSymbolFiles` — every `.nupkg` needs a matching `.snupkg`, and every + `.snupkg` must contain at least one `.pdb`. +- `RequiredContent` — because `RequireExplicitContent` defaults to `true`, this map is the **exhaustive** + declaration of every package's contents: a produced package with no matching rule fails, and any packed + entry not matched by one of that package's globs fails. `$(TFM)` expands to each target framework the + package actually ships, so the rules are satisfied per framework. +- Additional checks always run: file names must match the nuspec id/version, and `.nupkg` files must not + contain PDBs outside `tools/`/`analyzers/`. + +Adding a packable project therefore means adding a `RequiredContent` entry keyed by its lowercase package id +alongside the `Sdk/README.md`. Removing content from a package means removing the corresponding glob. + +## Adding a package + +1. Create `src/src//.csproj` with `true`, an assembly/package id, a + description and tags, and add it to `src/WSLTestContainers.slnx`. +2. Add `Sdk/README.md` documenting the package (see the existing modules for the shape). +3. Add the package to `RequiredContent` in `purview-build.json`. +4. Prove it with `just pack` and `just pipeline-pack-validate`. + +## Related + +- [Release Flow](Release-Flow.md) — how packages are versioned and published. +- [Contributing Modules](Contributing-Modules.md) — the module contract a new package follows. diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md new file mode 100644 index 0000000..815d8fd --- /dev/null +++ b/docs/wiki/Release-Flow.md @@ -0,0 +1,57 @@ +# Release Flow + +How this repository builds, versions, validates and publishes `Purview.WslContainers.*`. + +## Versioning + +`package.json` at the repository root is the authoritative version (`1.0.0-prerelease.1` today). The SDK +applies that value to `Version` and `PackageVersion` for every project, so a release is a `package.json` +bump — never a manual project-file edit. `package.json` also carries the repository, homepage and issue URLs +that end up in each nuspec. + +## Local commands + +The `Justfile` wraps the common steps: + +| Command | What it does | +| --- | --- | +| `just build` | `dotnet build src/WSLTestContainers.slnx` (Debug). | +| `just test` | `dotnet test` across the solution, one test module at a time (see [Testing](Testing.md)). | +| `just lint-check` / `just lint-fix` | CSharpier check / format over the repository root. | +| `just pack` | Build (Debug) and `dotnet pack` into `./artifacts`. | +| `just scrub` | Delete `bin`/`obj`, clean, re-restore with `--force-evaluate`, and shut down the build server. | +| `just pipeline-pack-validate` | Shared pipeline: restore, build, lint, test, pack and **validate** the packages, without publishing. | + +## The shared pipeline + +All pipeline recipes first install the `Purview.Build` tool into `.tools/purview-build` and then run it +against `purview-build.json`: + +| Recipe | Pipeline arguments | +| --- | --- | +| `just pipeline-pr` | default (full PR pipeline) | +| `just pipeline-build` | `--Build:RunTests=false --Release:Mode=None` | +| `just pipeline-tests` | `--Build:RunTests=true --Release:Mode=None` | +| `just pipeline-pack-validate` | `--Build:RunPack=true --Build:ValidatePack=true --Release:Mode=None` | +| `just pipeline-local-release` | `--Release:Mode=LocalNuGet` | +| `just pipeline-release` | `--Release:Mode=NuGet` | + +The pipeline cleans `artifacts/` before it runs, builds in **Release**, lints with CSharpier, runs the +test filter from `purview-build.json` (`[Category=Unit]`, so no WSLC host is needed), packs, and then +validates the produced packages against the exhaustive `RequiredContent` manifest. Any module failure fails +the run with the failing module's output. + +## GitHub Actions + +- `.github/workflows/pr.yml` — pull requests build and test via the shared + `purview-dev/build/.github/workflows/purview-build.yml`, with pack and validation enabled. +- `.github/workflows/release.yml` — a push to `main` calls `purview-dev/build/.github/workflows/purview-release.yml` + with `release-mode: NuGet`, which packs, publishes to NuGet and creates the `v` GitHub release. + +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`. + +## Related + +- [Packaging](Packaging.md) — the package contents and the validation manifest. +- [Contributing](Contributing.md) — the local workflow before raising a PR. diff --git a/docs/wiki/Testing.md b/docs/wiki/Testing.md new file mode 100644 index 0000000..fb585da --- /dev/null +++ b/docs/wiki/Testing.md @@ -0,0 +1,52 @@ +# Testing + +How the test suite is organised, why it runs serially, and how to run a subset. + +## Test categories + +Test projects are discovered under `src/tests` and the SDK stamps every test assembly with a TUnit category: + +| Project suffix | Category | Needs WSLC | +| --- | --- | --- | +| `*.UnitTests` | `Unit` | No — pure logic, parsing, configuration and wait-strategy units, some with in-process fakes. | +| `*.IntegrationTests` | `Integration` | Yes — real containers on a shared WSLC session. | + +`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. + +```powershell +just test # every discovered test project +just test '/*/*/*/*[Category=Unit]' # unit tests only +just test '/*/*/*/*[Category=Integration]' --max-parallel-test-modules 1 +``` + +## Why test modules run serially + +A WSLC session **exclusively locks its `storage.vhdx`**, and the lock is taken lazily on the first store +access rather than at session start. Running test assemblies in parallel therefore makes the losers fail with +`0x80070020`: + +```text +The process cannot access the file because it is being used by another process. +``` + +`just test` passes `--max-parallel-test-modules 1` for that reason. The runtime verifies the store once and +transparently falls back to an isolated per-process store, but running modules serially keeps the warm shared +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. + +## Expectations + +- The `WslContainers.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. +- `spikes/WslcSpikes` is a manual investigation harness (`dotnet run --project spikes/WslcSpikes -- sfull`, + or `s1`..`s14` for individual behaviour probes); it is not part of the test run. + +## Related + +- [Architecture](Architecture.md) — session lifetime, the concurrency gate and the shared-store fallback. +- [Getting Started](Getting-Started.md) — running the tests locally. diff --git a/docs/wait-strategies.md b/docs/wiki/Wait-Strategies.md similarity index 100% rename from docs/wait-strategies.md rename to docs/wiki/Wait-Strategies.md diff --git a/docs/wslc-api-investigation.md b/docs/wiki/Wslc-Api-Investigation.md similarity index 100% rename from docs/wslc-api-investigation.md rename to docs/wiki/Wslc-Api-Investigation.md diff --git a/docs/wslc-capability-matrix.md b/docs/wiki/Wslc-Capability-Matrix.md similarity index 100% rename from docs/wslc-capability-matrix.md rename to docs/wiki/Wslc-Capability-Matrix.md diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md new file mode 100644 index 0000000..d575768 --- /dev/null +++ b/docs/wiki/_Sidebar.md @@ -0,0 +1,14 @@ +- [Home](Home.md) +- [Getting Started](Getting-Started.md) +- [Architecture](Architecture.md) +- [Lifecycle](Lifecycle.md) +- [Networking](Networking.md) +- [Wait Strategies](Wait-Strategies.md) +- [Modules](Modules.md) +- [Testing](Testing.md) +- [Packaging](Packaging.md) +- [Release Flow](Release-Flow.md) +- [Contributing Modules](Contributing-Modules.md) +- [Contributing](Contributing.md) +- [WSLC API Investigation](Wslc-Api-Investigation.md) +- [WSLC Capability Matrix](Wslc-Capability-Matrix.md) diff --git a/docs/wiki/index.md b/docs/wiki/index.md new file mode 100644 index 0000000..f02968e --- /dev/null +++ b/docs/wiki/index.md @@ -0,0 +1,20 @@ +# 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. + +[Get started](Getting-Started.md){ .md-button .md-button--primary } +[Documentation overview](Home.md){ .md-button } + +## Guides + +- [Architecture](Architecture.md) +- [Lifecycle](Lifecycle.md) +- [Networking](Networking.md) +- [Wait strategies](Wait-Strategies.md) +- [Modules](Modules.md) +- [Testing](Testing.md) +- [Packaging](Packaging.md) +- [Release flow](Release-Flow.md) +- [Contributing modules](Contributing-Modules.md) +- [Contributing](Contributing.md) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c487fe2 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,21 @@ +site_name: Purview WSL Test Containers +site_description: Developer documentation for Purview WSL Test Containers +repo_url: https://github.com/purview-dev/wsl-testcontainers +edit_uri: edit/main/docs/wiki/ +docs_dir: docs/wiki + +theme: + name: material + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: indigo + accent: cyan + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: indigo + accent: cyan + features: [navigation.instant, navigation.sections, navigation.top, search.highlight, search.suggest, content.code.copy] + +plugins: [search, techdocs-core] +markdown_extensions: [admonition, attr_list, md_in_html, pymdownx.details, pymdownx.superfences] diff --git a/purview-build.json b/purview-build.json new file mode 100644 index 0000000..b5876a0 --- /dev/null +++ b/purview-build.json @@ -0,0 +1,71 @@ +{ + "Build": { + "Solution": "src/WSLTestContainers.slnx", + "TestRoot": "src/tests", + "TestPatterns": "*UnitTests.csproj", + "TestProjects": "*Tests.csproj", + "TestFilter": "/*/*/*/*[Category=Unit]" + }, + "PackValidation": { + "RequireSymbolPackage": true, + "RequireSymbolFiles": true, + // RequireExplicitContent defaults to true, so this is the exhaustive declaration of what each + // package ships: the library assembly and its XML docs, the bespoke package README + // (src/src//Sdk/README.md) and the shared Purview logo + // (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", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.azurite": [ + "lib/$(TFM)/Purview.WslContainers.Azurite.dll", + "lib/$(TFM)/Purview.WslContainers.Azurite.xml", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.mssql": [ + "lib/$(TFM)/Purview.WslContainers.MsSql.dll", + "lib/$(TFM)/Purview.WslContainers.MsSql.xml", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.mysql": [ + "lib/$(TFM)/Purview.WslContainers.MySql.dll", + "lib/$(TFM)/Purview.WslContainers.MySql.xml", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.nats": [ + "lib/$(TFM)/Purview.WslContainers.Nats.dll", + "lib/$(TFM)/Purview.WslContainers.Nats.xml", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.postgresql": [ + "lib/$(TFM)/Purview.WslContainers.PostgreSql.dll", + "lib/$(TFM)/Purview.WslContainers.PostgreSql.xml", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.rabbitmq": [ + "lib/$(TFM)/Purview.WslContainers.RabbitMq.dll", + "lib/$(TFM)/Purview.WslContainers.RabbitMq.xml", + "README.md", + "purview-logo-light.png" + ], + "purview.wslcontainers.redis": [ + "lib/$(TFM)/Purview.WslContainers.Redis.dll", + "lib/$(TFM)/Purview.WslContainers.Redis.xml", + "README.md", + "purview-logo-light.png" + ] + } + }, + "Release": { + "Mode": "None" + } +} \ No newline at end of file diff --git a/spikes/WslcSpikes/Program.cs b/spikes/WslcSpikes/Program.cs index 8019dfb..237cbe7 100644 --- a/spikes/WslcSpikes/Program.cs +++ b/spikes/WslcSpikes/Program.cs @@ -5,7 +5,9 @@ if (args.Length == 0) { - Console.Error.WriteLine("No spike specified. Available: s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s10child s10child-norelease s11 s12 s13 s14 s18 s19 sfull"); + Console.Error.WriteLine( + "No spike specified. Available: s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s10child s10child-norelease s11 s12 s13 s14 s18 s19 sfull" + ); return 1; } diff --git a/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs b/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs index 03c8594..7bdb322 100644 --- a/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs +++ b/spikes/WslcSpikes/Spikes/ConcurrentExecProcesses.cs @@ -36,7 +36,12 @@ public static async Task RunAsync() await Task.Delay(500); for (int i = 0; i < containerCount; i++) { - Console.WriteLine("[s19] container s19-{0} state={1} init={2}", i, containers[i].State, containers[i].InitProcess.State); + Console.WriteLine( + "[s19] container s19-{0} state={1} init={2}", + i, + containers[i].State, + containers[i].InitProcess.State + ); } int failures = 0; @@ -52,13 +57,14 @@ public static async Task RunAsync() int ei = e; execs[index++] = Task.Run(async () => { - Process process = containers[ci].CreateProcess( - new ProcessSettings - { - CommandLine = new List { "/bin/echo", "hi" }, - OutputMode = ProcessOutputMode.Event, - } - ); + Process process = containers[ci] + .CreateProcess( + new ProcessSettings + { + CommandLine = new List { "/bin/echo", "hi" }, + OutputMode = ProcessOutputMode.Event, + } + ); ProcessCapture capture = SpikeSupport.Capture(process); try { @@ -110,4 +116,4 @@ public static async Task RunAsync() Console.WriteLine("[s19] S19 complete."); return 0; } -} \ No newline at end of file +} diff --git a/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs b/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs index c79d02f..1c67b2d 100644 --- a/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs +++ b/spikes/WslcSpikes/Spikes/ConcurrentSharedPath.cs @@ -34,7 +34,9 @@ public static async Task RunAsync() } catch (Exception ex) { - Console.WriteLine("[s18] RESULT: session B FAILED to start on the shared path (concurrent sharing unsafe):"); + Console.WriteLine( + "[s18] RESULT: session B FAILED to start on the shared path (concurrent sharing unsafe):" + ); SpikeSupport.Dump(ex); return 0; } @@ -57,8 +59,18 @@ public static async Task RunAsync() Console.WriteLine("[s18] starting containers in A and B concurrently..."); try { - Container ca = SpikeSupport.CreateContainer(a, "docker.io/library/alpine:latest", "s18-a", new[] { "/bin/echo", "from-a" }); - Container cb = SpikeSupport.CreateContainer(b, "docker.io/library/alpine:latest", "s18-b", new[] { "/bin/echo", "from-b" }); + Container ca = SpikeSupport.CreateContainer( + a, + "docker.io/library/alpine:latest", + "s18-a", + new[] { "/bin/echo", "from-a" } + ); + Container cb = SpikeSupport.CreateContainer( + b, + "docker.io/library/alpine:latest", + "s18-b", + new[] { "/bin/echo", "from-b" } + ); await Task.WhenAll(Task.Run(() => ca.Start()), Task.Run(() => cb.Start())); await Task.Delay(1500); Console.WriteLine("[s18] A container state={0}, B container state={1}", ca.State, cb.State); @@ -89,4 +101,4 @@ public static async Task RunAsync() Console.WriteLine("[s18] S18 complete."); return 0; } -} \ No newline at end of file +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c6a3b70..b964508 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -7,4 +7,21 @@ + + + + purview-logo-light.png + README.md + + + + + diff --git a/src/WSLTestContainers.slnx b/src/WSLTestContainers.slnx index 10548b4..cf4e852 100644 --- a/src/WSLTestContainers.slnx +++ b/src/WSLTestContainers.slnx @@ -5,12 +5,12 @@ + - @@ -23,8 +23,6 @@ - - diff --git a/src/src/Azurite/AzuriteBuilder.cs b/src/src/Azurite/AzuriteBuilder.cs index 22554d5..5990692 100644 --- a/src/src/Azurite/AzuriteBuilder.cs +++ b/src/src/Azurite/AzuriteBuilder.cs @@ -57,5 +57,6 @@ protected override AzuriteConfiguration BuildConfiguration() } /// - protected override AzuriteContainer CreateContainer(AzuriteConfiguration configuration) => new(configuration, Runtime ?? WslContainerRuntime.Instance); + protected override AzuriteContainer CreateContainer(AzuriteConfiguration configuration) => + new(configuration, Runtime ?? WslContainerRuntime.Instance); } diff --git a/src/src/Azurite/Sdk/README.md b/src/src/Azurite/Sdk/README.md new file mode 100644 index 0000000..07203df --- /dev/null +++ b/src/src/Azurite/Sdk/README.md @@ -0,0 +1,46 @@ +# Purview.WslContainers.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. + +```bash +dotnet add package Purview.WslContainers.Azurite +``` + +Depends on `Purview.WslContainers` (the core runtime) and needs Windows with WSL ≥ 2.9.3 (WSL Containers). +See the [Getting Started guide](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Getting-Started.md). + +## Quick start + +```csharp +using Purview.WslContainers.Azurite; + +await using var azurite = new AzuriteBuilder().Build(); + +await azurite.StartAsync(); // waits for the blob/queue/table listeners + +Uri blob = azurite.GetBlobEndpoint(); +string connectionString = azurite.GetConnectionString(); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `AzuriteBuilder()` / `AzuriteBuilder(string image)` | Default image `mcr.microsoft.com/azure-storage/azurite`, or a custom image. | +| `AzuriteBuilder.BlobPort` (10000), `QueuePort` (10001), `TablePort` (10002) | Container service ports; all three are mapped to random host ports. | +| `AzuriteContainer.GetBlobEndpoint()`, `GetQueueEndpoint()`, `GetTableEndpoint()` | Service endpoints (`http://127.0.0.1:{mappedPort}/devstoreaccount1`). | +| `AzuriteContainer.GetConnectionString()` | Azure Storage connection string for the emulator. | +| `AzuriteAccount.Name` | The well-known account name `devstoreaccount1`. | + +The container starts Azurite with `--blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0` and waits +for the `successfully listening` log signal. Endpoints and the connection string are only meaningful after +`StartAsync()` because they resolve the mapped host ports. + +## Account key + +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-testcontainers/blob/main/docs/wiki/Modules.md) +for the module contract. diff --git a/src/src/MinIo/MinIO.csproj b/src/src/MinIo/MinIO.csproj deleted file mode 100644 index e02446b..0000000 --- a/src/src/MinIo/MinIO.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - true - MinIO module for Purview.WslContainers: throwaway S3-compatible object storage on WSL Containers. - wsl;containers;minio;s3;object;storage;testing;integration - MIT - Purview - - - - - - diff --git a/src/src/MinIo/MinIOBuilder.cs b/src/src/MinIo/MinIOBuilder.cs deleted file mode 100644 index 0d0ffd6..0000000 --- a/src/src/MinIo/MinIOBuilder.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; -using Purview.WslContainers.Waiting; - -namespace Purview.WslContainers.MinIO; - -/// Fluent builder for a MinIO (S3-compatible object storage) test container. -public class MinIOBuilder : ContainerBuilder -{ - /// Default S3 API port. - public const ushort APIPort = 9000; - - /// Default console port. - public const ushort ConsolePort = 9001; - - /// Default image (MinIO's official registry is Quay.io). - public const string MinIOImage = "quay.io/minio/minio:latest"; - - string _accessKey = "minioadmin"; - - Secret _secretKey = Secret.From("minioadmin"); - - /// Creates a builder with the default image. - public MinIOBuilder() - : this(MinIOImage) { } - - /// Creates a builder with a custom image. - public MinIOBuilder(string image) - { - WithImage(image) - .WithCommand("minio", "server", "/data", "--console-address", ":9001") - .WithPortBinding(APIPort, assignRandomHostPort: true) - .WithPortBinding(ConsolePort, assignRandomHostPort: true); - } - - /// Creates a builder using an explicit runtime. - public MinIOBuilder(IContainerRuntime runtime) - : base(runtime) - { - WithImage(MinIOImage) - .WithCommand("minio", "server", "/data", "--console-address", ":9001") - .WithPortBinding(APIPort, assignRandomHostPort: true) - .WithPortBinding(ConsolePort, assignRandomHostPort: true); - } - - /// Sets the root access key. - public MinIOBuilder WithAccessKey(string accessKey) - { - _accessKey = accessKey; - WithEnvironment("MINIO_ROOT_USER", accessKey); - return this; - } - - /// Sets the root secret key. - public MinIOBuilder WithSecretKey(string secretKey) - { - _secretKey = Secret.From(secretKey); - WithEnvironment("MINIO_ROOT_PASSWORD", secretKey); - return this; - } - - /// Builds the immutable configuration (internal; used by the module's own tests). - internal MinIOConfiguration BuildConfigurationForTesting() => BuildConfiguration(); - - /// - protected override MinIOConfiguration BuildConfiguration() - { - var configuration = base.BuildConfiguration(); - Dictionary environment = new( - configuration.Environment, - StringComparer.Ordinal - ) - { - ["MINIO_ROOT_USER"] = _accessKey, - ["MINIO_ROOT_PASSWORD"] = _secretKey.Value, - }; - var waitStrategies = - configuration.WaitStrategies.Count > 0 - ? configuration.WaitStrategies - : new[] { (IWaitStrategy)Wait.ForLogMessage("Console:") }; - return configuration with - { - AccessKey = _accessKey, - SecretKey = _secretKey, - Environment = environment, - WaitStrategies = waitStrategies, - }; - } - - /// - protected override void Validate(ContainerConfiguration configuration) - { - base.Validate(configuration); - if (string.IsNullOrWhiteSpace(_accessKey) || string.IsNullOrEmpty(_secretKey.Value)) - { - throw new WslContainerConfigurationException("MinIO access key and secret key cannot be empty."); - } - } - - /// - protected override MinIOContainer CreateContainer(MinIOConfiguration configuration) - { - return new MinIOContainer(configuration, Runtime ?? WslContainerRuntime.Instance); - } -} diff --git a/src/src/MinIo/MinIOConfiguration.cs b/src/src/MinIo/MinIOConfiguration.cs deleted file mode 100644 index 1cf623f..0000000 --- a/src/src/MinIo/MinIOConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Purview.WslContainers.Diagnostics; - -namespace Purview.WslContainers.MinIO; - -/// Immutable configuration for a MinIO test container. -public sealed record MinIOConfiguration : ContainerConfiguration -{ - /// Root access key (MINIO_ROOT_USER). - public string AccessKey { get; init; } = "minioadmin"; - - /// Root secret key (MINIO_ROOT_PASSWORD). Rendered as redacted. - public Secret SecretKey { get; init; } = Secret.From("minioadmin"); -} diff --git a/src/src/MinIo/MinIOContainer.cs b/src/src/MinIo/MinIOContainer.cs deleted file mode 100644 index 848c559..0000000 --- a/src/src/MinIo/MinIOContainer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Purview.WslContainers.Diagnostics; - -namespace Purview.WslContainers.MinIO; - -/// A throwaway MinIO (S3-compatible object storage) instance running on WSL Containers. -public sealed class MinIOContainer : WslContainer -{ - readonly MinIOConfiguration _configuration; - - internal MinIOContainer(MinIOConfiguration configuration, IContainerRuntime runtime) - : base(configuration, runtime) - { - _configuration = configuration; - } - - /// S3 API endpoint. Safe to call after . - public Uri GetAPIEndpoint() - { - return new Uri($"http://127.0.0.1:{GetMappedPublicPort(MinIOBuilder.APIPort)}"); - } - - /// Web console endpoint. - public Uri GetConsoleEndpoint() - { - return new Uri($"http://127.0.0.1:{GetMappedPublicPort(MinIOBuilder.ConsolePort)}"); - } - - /// Root access key. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate")] - public string GetAccessKey() => _configuration.AccessKey; - - /// Root secret key. Handle as a secret. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate")] - public Secret GetSecretKey() => _configuration.SecretKey; -} diff --git a/src/src/MsSql/MsSqlBuilder.cs b/src/src/MsSql/MsSqlBuilder.cs index d2f0269..1431fb2 100644 --- a/src/src/MsSql/MsSqlBuilder.cs +++ b/src/src/MsSql/MsSqlBuilder.cs @@ -60,10 +60,7 @@ public MsSqlBuilder AcceptLicense() protected override MsSqlConfiguration BuildConfiguration() { var configuration = base.BuildConfiguration(); - Dictionary environment = new( - configuration.Environment, - StringComparer.Ordinal - ) + Dictionary environment = new(configuration.Environment, StringComparer.Ordinal) { ["MSSQL_SA_PASSWORD"] = _password.Value, ["MSSQL_PID"] = "Developer", diff --git a/src/src/MsSql/Sdk/README.md b/src/src/MsSql/Sdk/README.md new file mode 100644 index 0000000..db20410 --- /dev/null +++ b/src/src/MsSql/Sdk/README.md @@ -0,0 +1,55 @@ +# Purview.WslContainers.MsSql + +Throwaway Microsoft SQL Server databases for .NET integration testing, running as WSLC containers on +**Microsoft WSL Containers** — no Docker installation. + +```bash +dotnet add package Purview.WslContainers.MsSql +``` + +Depends on `Purview.WslContainers` (the core runtime) and brings `Microsoft.Data.SqlClient` for +connection-string generation and readiness probing. + +## Quick start + +```csharp +using Microsoft.Data.SqlClient; +using Purview.WslContainers.MsSql; + +await using var sqlServer = new MsSqlBuilder() + .WithPassword("SomeStrong!Password1") + .AcceptLicense() // explicit EULA acceptance is required + .Build(); + +await sqlServer.StartAsync(); // waits for a real SQL Server connection + +await using var connection = new SqlConnection(sqlServer.GetConnectionString()); +await connection.OpenAsync(); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `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`. | +| `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. | + +## Requirements and behaviour + +- **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. + 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 + connection string targets `127.0.0.1`, because WSLC maps IPv4 loopback only and `Microsoft.Data.SqlClient` + hangs on the IPv6 `::1` address that `localhost` can resolve to. + +## Documentation + +- [Modules](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Getting Started](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Getting-Started.md) — prerequisites and first-container walkthrough. diff --git a/src/src/MySql/MySqlBuilder.cs b/src/src/MySql/MySqlBuilder.cs index 05cc758..908cd6e 100644 --- a/src/src/MySql/MySqlBuilder.cs +++ b/src/src/MySql/MySqlBuilder.cs @@ -75,10 +75,7 @@ public MySqlBuilder WithRootPassword(string rootPassword) protected override MySqlConfiguration BuildConfiguration() { var configuration = base.BuildConfiguration(); - Dictionary environment = new( - configuration.Environment, - StringComparer.Ordinal - ) + Dictionary environment = new(configuration.Environment, StringComparer.Ordinal) { ["MYSQL_DATABASE"] = _database, ["MYSQL_USER"] = _username, diff --git a/src/src/MySql/Sdk/README.md b/src/src/MySql/Sdk/README.md new file mode 100644 index 0000000..917de3b --- /dev/null +++ b/src/src/MySql/Sdk/README.md @@ -0,0 +1,55 @@ +# Purview.WslContainers.MySql + +Throwaway MySQL databases for .NET integration testing, running as WSLC containers on +**Microsoft WSL Containers** — no Docker installation. + +```bash +dotnet add package Purview.WslContainers.MySql +``` + +Depends on `Purview.WslContainers` (the core runtime) and brings `MySqlConnector` for readiness probing and +connection-string generation. + +## Quick start + +```csharp +using MySqlConnector; +using Purview.WslContainers.MySql; + +await using var mysql = new MySqlBuilder() + .WithDatabase("tests") + .WithUsername("test") + .WithPassword("test") + .Build(); + +await mysql.StartAsync(); // waits for a real host-side connection + +await using var connection = new MySqlConnection(mysql.GetConnectionString()); +await connection.OpenAsync(); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `MySqlBuilder()` / `MySqlBuilder(string image)` | Default image `mysql:8`, or a custom image. | +| `MySqlBuilder.MySqlPort` (3306) | Container port, mapped to a random host port. | +| `WithDatabase(string)` | Sets `MYSQL_DATABASE` (default `test`). | +| `WithUsername(string)` | Sets `MYSQL_USER` (default `test`). | +| `WithPassword(string)` | Sets `MYSQL_PASSWORD` (default `test`); stored as a redacted `Secret`. | +| `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`. + +## Readiness + +The image logs `ready for connections` while its temporary initialisation server is still running, so a log +match or a bare TCP check would report readiness too early. The default wait strategy therefore opens a real +host-side `MySqlConnector` connection (every second) and only returns once it succeeds. Supply your own +strategy with `WithWaitStrategy(...)` if you need different behaviour. + +## Documentation + +- [Modules](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Nats/Sdk/README.md b/src/src/Nats/Sdk/README.md new file mode 100644 index 0000000..be41e12 --- /dev/null +++ b/src/src/Nats/Sdk/README.md @@ -0,0 +1,44 @@ +# Purview.WslContainers.Nats + +Throwaway NATS brokers for .NET integration testing, running as WSLC containers on +**Microsoft WSL Containers** — no Docker installation. + +```bash +dotnet add package Purview.WslContainers.Nats +``` + +Depends on `Purview.WslContainers` (the core runtime). `NATS.Client.Core` is not referenced by this +package — bring your own client. + +## Quick start + +```csharp +using Purview.WslContainers.Nats; +using NATS.Client.Core; + +await using var nats = new NatsBuilder().Build(); + +await nats.StartAsync(); // waits for 'Listening for client connections' + +await using var client = new NatsConnection(new NatsOpts { Url = nats.GetConnectionString() }); +await client.PublishAsync("orders.created", "42"); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `NatsBuilder()` / `NatsBuilder(string image)` | Default image `nats:2`, or a custom image. | +| `NatsBuilder.ClientPort` (4222), `MonitoringPort` (8222) | Container ports, both mapped to random host ports. | +| `NatsContainer.GetClientEndpoint()` | `nats://127.0.0.1:{mappedPort}` as a `Uri`. | +| `NatsContainer.GetConnectionString()` | The client endpoint as a string. | +| `NatsContainer.GetMonitoringEndpoint()` | Monitoring HTTP endpoint (`http://127.0.0.1:{mappedPort}`), e.g. `/varz`. | + +The default wait strategy matches the `Listening for client connections` log line unless you supply your own +with `WithWaitStrategy(...)`. Endpoint accessors resolve the mapped host ports, so call them after +`StartAsync()`. + +## Documentation + +- [Modules](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/PostgreSql/PostgreSqlBuilder.cs b/src/src/PostgreSql/PostgreSqlBuilder.cs index 456f1bc..0a6ca1a 100644 --- a/src/src/PostgreSql/PostgreSqlBuilder.cs +++ b/src/src/PostgreSql/PostgreSqlBuilder.cs @@ -65,10 +65,7 @@ public PostgreSqlBuilder WithPassword(string password) protected override PostgreSqlConfiguration BuildConfiguration() { var configuration = base.BuildConfiguration(); - Dictionary environment = new( - configuration.Environment, - StringComparer.Ordinal - ) + Dictionary environment = new(configuration.Environment, StringComparer.Ordinal) { ["POSTGRES_DB"] = _database, ["POSTGRES_USER"] = _username, diff --git a/src/src/PostgreSql/Sdk/README.md b/src/src/PostgreSql/Sdk/README.md new file mode 100644 index 0000000..6b58cb2 --- /dev/null +++ b/src/src/PostgreSql/Sdk/README.md @@ -0,0 +1,49 @@ +# Purview.WslContainers.PostgreSql + +Throwaway PostgreSQL databases for .NET integration testing, running as WSLC containers on +**Microsoft WSL Containers** — no Docker installation. + +```bash +dotnet add package Purview.WslContainers.PostgreSql +``` + +Depends on `Purview.WslContainers` (the core runtime) and brings `Npgsql` for connection-string generation. +See the [Getting Started guide](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Getting-Started.md). + +## Quick start + +```csharp +using Npgsql; +using Purview.WslContainers.PostgreSql; + +await using var postgres = new PostgreSqlBuilder() + .WithDatabase("tests") + .WithUsername("postgres") + .WithPassword("postgres") + .Build(); + +await postgres.StartAsync(); // waits for pg_isready + +await using var connection = new NpgsqlConnection(postgres.GetConnectionString()); +await connection.OpenAsync(); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `PostgreSqlBuilder()` / `PostgreSqlBuilder(string image)` | Default image `postgres:17`, or a custom image. | +| `PostgreSqlBuilder.PostgreSqlPort` (5432) | Container port, mapped to a random host port. | +| `WithDatabase(string)` | Sets `POSTGRES_DB` (default `postgres`). | +| `WithUsername(string)` | Sets `POSTGRES_USER` (default `postgres`). | +| `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 +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 + +- [Modules](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/RabbitMq/RabbitMqBuilder.cs b/src/src/RabbitMq/RabbitMqBuilder.cs index 10ebf04..8d257aa 100644 --- a/src/src/RabbitMq/RabbitMqBuilder.cs +++ b/src/src/RabbitMq/RabbitMqBuilder.cs @@ -72,10 +72,7 @@ public RabbitMqBuilder WithVirtualHost(string virtualHost) protected override RabbitMqConfiguration BuildConfiguration() { var configuration = base.BuildConfiguration(); - Dictionary environment = new( - configuration.Environment, - StringComparer.Ordinal - ) + Dictionary environment = new(configuration.Environment, StringComparer.Ordinal) { ["RABBITMQ_DEFAULT_USER"] = _username, ["RABBITMQ_DEFAULT_PASS"] = _password.Value, diff --git a/src/src/RabbitMq/RabbitMqContainer.cs b/src/src/RabbitMq/RabbitMqContainer.cs index bfe4c1d..78b8283 100644 --- a/src/src/RabbitMq/RabbitMqContainer.cs +++ b/src/src/RabbitMq/RabbitMqContainer.cs @@ -30,7 +30,7 @@ public Uri GetAmqpEndpoint() Port = GetMappedPublicPort(RabbitMqBuilder.AmqpPort), UserName = _configuration.Username, Password = _configuration.Password.Value, - Path = _configuration.VirtualHost is "/" or "" ? "/" : "/" + _configuration.VirtualHost.TrimStart('/') + Path = _configuration.VirtualHost is "/" or "" ? "/" : "/" + _configuration.VirtualHost.TrimStart('/'), }; return builder.Uri; } diff --git a/src/src/RabbitMq/Sdk/README.md b/src/src/RabbitMq/Sdk/README.md new file mode 100644 index 0000000..ab497ed --- /dev/null +++ b/src/src/RabbitMq/Sdk/README.md @@ -0,0 +1,56 @@ +# Purview.WslContainers.RabbitMq + +Throwaway RabbitMQ brokers for .NET integration testing, running as WSLC containers on +**Microsoft WSL Containers** — no Docker installation. + +```bash +dotnet add package Purview.WslContainers.RabbitMq +``` + +Depends on `Purview.WslContainers` (the core runtime). `RabbitMQ.Client` is not referenced by this package — +bring your own client. + +## Quick start + +```csharp +using Purview.WslContainers.RabbitMq; +using RabbitMQ.Client; + +await using var rabbitMq = new RabbitMqBuilder() + .WithUsername("guest") + .WithPassword("guest") + .Build(); + +await rabbitMq.StartAsync(); // waits for 'Server startup complete' + +var factory = new ConnectionFactory { Uri = rabbitMq.GetAmqpEndpoint() }; +await using var connection = await factory.CreateConnectionAsync(); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `RabbitMqBuilder()` / `RabbitMqBuilder(string image)` | Default image `rabbitmq:3-management` (management plugin included), or a custom image. | +| `RabbitMqBuilder.AmqpPort` (5672), `ManagementPort` (15672) | Container ports, both mapped to random host ports. | +| `WithUsername(string)` | Sets `RABBITMQ_DEFAULT_USER` (default `guest`). | +| `WithPassword(string)` | Sets `RABBITMQ_DEFAULT_PASS` (default `guest`); stored as a redacted `Secret`. | +| `WithVirtualHost(string)` | Sets `RABBITMQ_DEFAULT_VHOST` (default `/`). | +| `RabbitMqContainer.GetAmqpEndpoint()` | `amqp://user:pass@localhost:{port}/{vhost}` as a `Uri` for `ConnectionFactory.Uri`. | +| `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`. +Call the endpoint accessors after `StartAsync()`. + +## Readiness + +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-testcontainers/blob/main/docs/wiki/Modules.md) for the full note. + +## Documentation + +- [Modules](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/Redis/Sdk/README.md b/src/src/Redis/Sdk/README.md new file mode 100644 index 0000000..05a9305 --- /dev/null +++ b/src/src/Redis/Sdk/README.md @@ -0,0 +1,51 @@ +# Purview.WslContainers.Redis + +Throwaway Redis-compatible instances for .NET integration testing, running as WSLC containers on +**Microsoft WSL Containers** — no Docker installation. + +```bash +dotnet add package Purview.WslContainers.Redis +``` + +Depends on `Purview.WslContainers` (the core runtime). `StackExchange.Redis` is not referenced by this +package — bring your own client. + +## Quick start + +```csharp +using Purview.WslContainers.Redis; +using StackExchange.Redis; + +await using var redis = new RedisBuilder().Build(); + +await redis.StartAsync(); // waits for redis-cli ping + +using var connection = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString()); +await connection.GetDatabase().PingAsync(); +``` + +## API + +| Member | Purpose | +| -- | -- | +| `RedisBuilder()` / `RedisBuilder(string image)` | Default image `redis:7`, or a custom image. | +| `RedisBuilder.RedisPort` (6379) | Container port, mapped to a random host port. | +| `RedisContainer.GetConnectionString()` | `localhost:{mappedPort}` for `ConnectionMultiplexer.ConnectAsync`. | + +The default wait strategy runs `redis-cli ping` inside the container unless you supply your own with +`WithWaitStrategy(...)`. Call `GetConnectionString()` after `StartAsync()`. + +## Redis-compatible images + +The module is a thin Redis client-neutral wrapper, so other Redis-compatible servers work through the image +constructor: + +```csharp +await using var valkey = new RedisBuilder("valkey/valkey:7").Build(); +await using var garnet = new RedisBuilder("ghcr.io/microsoft/garnet:latest").Build(); +``` + +## Documentation + +- [Modules](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Modules.md) — the module contract and readiness choices. +- [Wait Strategies](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Wait-Strategies.md) — overriding readiness checks. diff --git a/src/src/WslContainers/ContainerConfiguration.cs b/src/src/WslContainers/ContainerConfiguration.cs index c205abf..020b91b 100644 --- a/src/src/WslContainers/ContainerConfiguration.cs +++ b/src/src/WslContainers/ContainerConfiguration.cs @@ -1,9 +1,9 @@ +using System.Text; using Purview.WslContainers.Diagnostics; using Purview.WslContainers.Images; using Purview.WslContainers.Mounts; using Purview.WslContainers.Networking; using Purview.WslContainers.Waiting; -using System.Text; namespace Purview.WslContainers; diff --git a/src/src/WslContainers/Diagnostics/SecretRedactor.cs b/src/src/WslContainers/Diagnostics/SecretRedactor.cs index 2824eee..08e7e66 100644 --- a/src/src/WslContainers/Diagnostics/SecretRedactor.cs +++ b/src/src/WslContainers/Diagnostics/SecretRedactor.cs @@ -3,16 +3,7 @@ namespace Purview.WslContainers.Diagnostics; /// Redacts sensitive values from diagnostics and configuration rendering. static class SecretRedactor { - static readonly string[] SensitiveKeyMarkers = - [ - "PASSWORD", - "PASS", - "TOKEN", - "SECRET", - "PWD", - "CREDENTIAL", - "KEY", - ]; + static readonly string[] SensitiveKeyMarkers = ["PASSWORD", "PASS", "TOKEN", "SECRET", "PWD", "CREDENTIAL", "KEY"]; public static string RedactEnvironment(IReadOnlyDictionary environment) { diff --git a/src/src/WslContainers/Runtime/WslContainerException.cs b/src/src/WslContainers/Runtime/WslContainerException.cs index 5a0ff1c..344d5de 100644 --- a/src/src/WslContainers/Runtime/WslContainerException.cs +++ b/src/src/WslContainers/Runtime/WslContainerException.cs @@ -23,26 +23,17 @@ public WslContainerConfigurationException(string message, Exception innerExcepti } /// WSL/WSL Containers prerequisites are missing or incompatible. -public class WslContainerPrerequisiteException(string message) : WslContainerException(message) -{ -} +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) -{ -} +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) -{ -} +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) -{ -} +public class WslContainerTimeoutException(string message) : WslContainerException(message) { } /// The backing WSLC session terminated unexpectedly. -public class WslContainerSessionTerminatedException(string message) : WslContainerException(message) -{ -} +public class WslContainerSessionTerminatedException(string message) : WslContainerException(message) { } diff --git a/src/src/WslContainers/Sdk/README.md b/src/src/WslContainers/Sdk/README.md new file mode 100644 index 0000000..5e464ba --- /dev/null +++ b/src/src/WslContainers/Sdk/README.md @@ -0,0 +1,104 @@ +# Purview.WslContainers + +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. + +```bash +dotnet add package Purview.WslContainers +``` + +## Requirements + +- Windows 10/11 with **WSL ≥ 2.9.3** including WSL Containers (`wsl --install --no-distribution`). +- .NET 11 SDK or later (`net11.0-windows10.0.19041.0`). +- Verify with `wsl --version` (needs 2.9.3+) and `wslc version`. The library never installs or updates WSL + itself; `WslContainerRuntime.GetInfoAsync()` reports what is missing. + +## Quick start + +```csharp +using Purview.WslContainers; +using Purview.WslContainers.Waiting; + +await using var container = new ContainerBuilder() + .WithImage("docker.io/library/redis:latest") + .WithPortBinding(6379, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForTcpPort(6379)) + .Build(); + +await container.StartAsync(); + +ushort port = container.GetMappedPublicPort(6379); +``` + +`Build()` snapshots the accumulated builder state into an immutable configuration and validates it, so +invalid images, ports and mounts fail during configuration rather than at pull time. + +## Builder surface + +| Member | Purpose | +| -- | -- | +| `WithImage(string)`, `WithImage(Image)`, `WithTag(string)` | Image reference; parsed and validated eagerly (`Image.Parse`). | +| `WithName`, `WithHostname`, `WithDomainName` | Container identity. Names are generated uniquely when unset. | +| `WithEnvironment(string, string)`, `WithEnvironment(IReadOnlyDictionary)` | Init-process environment. | +| `WithCommand(params string[])`, `WithWorkingDirectory` | Init-process argv (no shell) and working directory. | +| `WithPortBinding(ushort, bool assignRandomHostPort)`, `WithPortBinding(ushort, ushort hostPort, ...)`, `WithPortBinding(PortBinding)` | Host port mappings; random host ports use native `windowsPort=0` allocation. | +| `WithBindMount(string hostPath, string containerPath, bool readOnly = false)` | Bind-mount a Windows directory. | +| `WithVolumeMount(string name, string containerPath, bool readOnly = false)` | Named volume on the session VHD. | +| `WithNetworkMode`, `WithPrivileged`, `WithGPU`, `WithAutoRemove`, `WithPullPolicy`, `WithStartupTimeout`, `WithWaitStrategy`, `WithRegistryCredentials`, `WithRuntime` | Runtime behaviour and readiness. | + +## Container API + +`IContainer` (and `WslContainer`) exposes `StartAsync`, `StopAsync`, `ExecAsync`, `GetMappedPublicPort`, +`GetMappedPublicPorts`, `GetLogsAsync(LogOutput?)` and a tailing `IAsyncEnumerable` +`GetLogsAsync(CancellationToken)`. `Id`, `Name`, `State` and `Image` describe the running container. +`DisposeAsync` stops and deletes the container and is idempotent; it never terminates the shared session. + +## Wait strategies + +`Wait` composes readiness checks that run inside `StartAsync`: + +| Factory | Ready when | +| -- | -- | +| `Wait.ForContainerRunning()` | The init process is running (started, not necessarily a ready service). | +| `Wait.ForTcpPort(port)` | A TCP connection to the mapped host port succeeds. | +| `Wait.ForHttp(path)` | An HTTP(S) request succeeds; configure `ForPort`, `ForStatusCode`, `ForHeader`, `AllowInsecureTls`. | +| `Wait.ForLogMessage(string)` / `Wait.ForLogMessage(Regex)` | Accumulated init-process output matches. | +| `Wait.ForCommand(params string[])` | A command executed in the container exits with the expected code. | +| `Wait.ForCustom(predicate)` | An arbitrary predicate returns `true`. | +| `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 +a diagnostic naming the container, image, state, mapped ports, strategy and a bounded, secret-redacted log tail. + +## Runtime, sessions and storage + +`WslContainerRuntime.Instance` owns a single lazily started, process-wide WSLC session named +`wslc-{pid}-{random8}`. Images are **shared by default** (`StorageMode.Shared`): every session uses +`%LOCALAPPDATA%\Purview\WslContainers\images`, so images are pulled once and reused across process runs. +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`. + +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 +and wait spans. Credentials and other sensitive values are wrapped in `Secret` and redacted from `ToString()` +and diagnostics. + +## Documentation + +See the [project wiki](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Home.md): +[Getting Started](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Getting-Started.md), +[Architecture](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Architecture.md), +[Lifecycle](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Lifecycle.md), +[Networking](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Networking.md) and +[Wait Strategies](https://github.com/purview-dev/wsl-testcontainers/blob/main/docs/wiki/Wait-Strategies.md). +Ready-made service modules ship as `Purview.WslContainers.PostgreSql`, `Redis`, `MsSql`, `RabbitMq`, +`Azurite`, `Nats` and `MySql`. diff --git a/src/src/WslContainers/Waiting/HttpWaitStrategy.cs b/src/src/WslContainers/Waiting/HttpWaitStrategy.cs index 0cd8326..17ca9f0 100644 --- a/src/src/WslContainers/Waiting/HttpWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/HttpWaitStrategy.cs @@ -99,9 +99,7 @@ public override async Task UntilAsync(WaitContext context, CancellationTok request.Headers.TryAddWithoutValidation(name, value); } - using var response = await client - .SendAsync(request, cancellationToken) - .ConfigureAwait(false); + using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); var status = (int)response.StatusCode; return _statusPredicate?.Invoke(status) ?? (_expectedStatusCode is HttpStatusCode expected && status == (int)expected); diff --git a/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs b/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs index 9f75495..f3f2b55 100644 --- a/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs +++ b/src/src/WslContainers/Waiting/TcpPortWaitStrategy.cs @@ -6,7 +6,6 @@ namespace Purview.WslContainers.Waiting; /// Waits until a TCP connection to the mapped host port succeeds (IPv4 loopback). public sealed class TcpPortWaitStrategy(ushort containerPort, TimeSpan connectTimeout) : WaitStrategy { - /// public override async Task UntilAsync(WaitContext context, CancellationToken cancellationToken) { @@ -17,10 +16,7 @@ public override async Task UntilAsync(WaitContext context, CancellationTok } using CancellationTokenSource timeoutSource = new(connectTimeout); - using var linked = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - timeoutSource.Token - ); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token); try { using TcpClient client = new(); diff --git a/src/src/WslContainers/Waiting/WaitStrategyRunner.cs b/src/src/WslContainers/Waiting/WaitStrategyRunner.cs index c316ed2..21b44d6 100644 --- a/src/src/WslContainers/Waiting/WaitStrategyRunner.cs +++ b/src/src/WslContainers/Waiting/WaitStrategyRunner.cs @@ -1,8 +1,8 @@ -using Purview.WslContainers.Diagnostics; -using Purview.WslContainers.Runtime; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; +using Purview.WslContainers.Diagnostics; +using Purview.WslContainers.Runtime; namespace Purview.WslContainers.Waiting; @@ -39,9 +39,7 @@ CancellationToken cancellationToken activity?.SetTag("strategy", strategy.GetType().Name); var timeout = strategy.Timeout ?? defaultTimeout; var interval = strategy.Interval > TimeSpan.Zero ? strategy.Interval : TimeSpan.FromMilliseconds(250); - using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken - ); + using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutSource.CancelAfter(timeout); var token = timeoutSource.Token; @@ -85,13 +83,19 @@ CancellationToken cancellationToken } cancellationToken.ThrowIfCancellationRequested(); - var diagnostics = await BuildDiagnosticsAsync(context, timeout, lastError, cancellationToken).ConfigureAwait(false); + var diagnostics = await BuildDiagnosticsAsync(context, timeout, lastError, cancellationToken) + .ConfigureAwait(false); throw new WslContainerTimeoutException( $"Container '{context.Container.Name}' did not become ready within {timeout} for strategy {strategy.GetType().Name}. {diagnostics}" ); } - static async Task BuildDiagnosticsAsync(WaitContext context, TimeSpan timeout, Exception? lastError, CancellationToken cancellationToken) + static async Task BuildDiagnosticsAsync( + WaitContext context, + TimeSpan timeout, + Exception? lastError, + CancellationToken cancellationToken + ) { StringBuilder builder = new(); builder.Append( @@ -122,9 +126,7 @@ static async Task BuildDiagnosticsAsync(WaitContext context, TimeSpan ti try { - var logs = await context - .Container.GetLogsAsync(stream: null, cancellationToken) - .ConfigureAwait(false); + var logs = await context.Container.GetLogsAsync(stream: null, cancellationToken).ConfigureAwait(false); var tail = logs.Length > 2000 ? logs[^2000..] : logs; if (tail.Length > 0) { diff --git a/src/src/WslContainers/WslInspectParser.cs b/src/src/WslContainers/WslInspectParser.cs index 41b1498..dc460bf 100644 --- a/src/src/WslContainers/WslInspectParser.cs +++ b/src/src/WslContainers/WslInspectParser.cs @@ -11,10 +11,7 @@ public static IReadOnlyDictionary ParseHostPorts(string inspectJ try { using var document = JsonDocument.Parse(inspectJson); - if ( - !document.RootElement.TryGetProperty("Ports", out var ports) - || ports.ValueKind != JsonValueKind.Object - ) + if (!document.RootElement.TryGetProperty("Ports", out var ports) || ports.ValueKind != JsonValueKind.Object) { return result; } diff --git a/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs b/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs index e532cc3..1d7365a 100644 --- a/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs +++ b/src/tests/Azurite.IntegrationTests/AzuriteIntegrationTests.cs @@ -1,6 +1,6 @@ namespace Purview.WslContainers.Azurite; -class AzuriteIntegrationTests +public class AzuriteIntegrationTests { [Test] public async Task Azurite_StartsAndExposesEndpoints() diff --git a/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs b/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs index 542ecd7..e2600e3 100644 --- a/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs +++ b/src/tests/Azurite.UnitTests/AzuriteBuilderTests.cs @@ -2,9 +2,18 @@ namespace Purview.WslContainers.Azurite; -class AzuriteBuilderTests +public class AzuriteBuilderTests { - static readonly string[] Expected = ["azurite", "--blobHost", "0.0.0.0", "--queueHost", "0.0.0.0", "--tableHost", "0.0.0.0"]; + static readonly string[] Expected = + [ + "azurite", + "--blobHost", + "0.0.0.0", + "--queueHost", + "0.0.0.0", + "--tableHost", + "0.0.0.0", + ]; [Test] public async Task BuildConfig_AppliesDefaults() @@ -12,11 +21,7 @@ public async Task BuildConfig_AppliesDefaults() var configuration = new AzuriteBuilder().BuildConfigurationForTesting(); await Assert.That(configuration.Image).IsEqualTo("mcr.microsoft.com/azure-storage/azurite:latest"); - await Assert - .That(configuration.Command) - .IsEquivalentTo( - Expected - ); + await Assert.That(configuration.Command).IsEquivalentTo(Expected); await Assert.That(configuration.PortBindings.Count).IsEqualTo(3); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 10000 && p.AssignRandomHostPort); await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 10001 && p.AssignRandomHostPort); diff --git a/src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj b/src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj deleted file mode 100644 index e6c35ec..0000000 --- a/src/tests/MinIo.IntegrationTests/MinIO.IntegrationTests.csproj +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs b/src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs deleted file mode 100644 index 44ddb1e..0000000 --- a/src/tests/MinIo.IntegrationTests/MinIOIntegrationTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -using TUnit.Core.Exceptions; - -namespace Purview.WslContainers.MinIO; - -class MinIOIntegrationTests -{ - [Test] - public async Task MinIO_ExposesAPIAndConsole() - { - await using var minio = await StartOrSkipAsync(); - - await WslcTest.WaitForTcpAsync(minio.GetMappedPublicPort(MinIOBuilder.APIPort), TimeSpan.FromSeconds(30)); - await WslcTest.WaitForTcpAsync(minio.GetMappedPublicPort(MinIOBuilder.ConsolePort), TimeSpan.FromSeconds(30)); - - using HttpClient client = new(); - using var response = await client.GetAsync(minio.GetConsoleEndpoint()); - await Assert.That((int)response.StatusCode).IsEqualTo(200); - } - - [Test] - public async Task Endpoints_UseMappedRandomPorts() - { - await using var minio = await StartOrSkipAsync(); - - var apiPort = minio.GetMappedPublicPort(MinIOBuilder.APIPort); - var consolePort = minio.GetMappedPublicPort(MinIOBuilder.ConsolePort); - - await Assert.That(minio.GetAPIEndpoint().ToString()).IsEqualTo($"http://127.0.0.1:{apiPort}"); - await Assert.That(minio.GetConsoleEndpoint().ToString()).IsEqualTo($"http://127.0.0.1:{consolePort}"); - await Assert.That(minio.GetAccessKey()).IsEqualTo("minioadmin"); - } - - // MinIO's official image lives on quay.io, which some networks/WSL sessions cannot reach - // (e.g. IPv6-only resolution with no route). Skip gracefully rather than fail the suite. - static async Task StartOrSkipAsync() - { - await WslcTest.SkipIfUnavailableAsync(); - - var minio = new MinIOBuilder().Build(); - try - { - await minio.StartAsync(); - return minio; - } - catch (Exception ex) - { - await minio.DisposeAsync(); - var message = ex.ToString(); - if ( - message.Contains("network is unreachable", StringComparison.OrdinalIgnoreCase) - || message.Contains("unreachable", StringComparison.OrdinalIgnoreCase) - || message.Contains("unauthorized", StringComparison.OrdinalIgnoreCase) - || message.Contains("does not exist", StringComparison.OrdinalIgnoreCase) - ) - { - throw new SkipTestException($"MinIO image could not be pulled from its registry: {ex.Message}"); - } - - throw; - } - } -} diff --git a/src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj b/src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj deleted file mode 100644 index e6c35ec..0000000 --- a/src/tests/MinIo.UnitTests/MinIO.UnitTests.csproj +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/tests/MinIo.UnitTests/MinIOBuilderTests.cs b/src/tests/MinIo.UnitTests/MinIOBuilderTests.cs deleted file mode 100644 index 6dcab62..0000000 --- a/src/tests/MinIo.UnitTests/MinIOBuilderTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Purview.WslContainers.Runtime; - -namespace Purview.WslContainers.MinIO; - -class MinIOBuilderTests -{ - static readonly string[] Expected = ["minio", "server", "/data", "--console-address", ":9001"]; - - [Test] - public async Task BuildConfig_AppliesDefaults() - { - var configuration = new MinIOBuilder().BuildConfigurationForTesting(); - - await Assert.That(configuration.Image).IsEqualTo("quay.io/minio/minio:latest"); - await Assert.That(configuration.AccessKey).IsEqualTo("minioadmin"); - await Assert.That(configuration.SecretKey.Value).IsEqualTo("minioadmin"); - await Assert.That(configuration.Environment["MINIO_ROOT_USER"]).IsEqualTo("minioadmin"); - await Assert.That(configuration.Environment["MINIO_ROOT_PASSWORD"]).IsEqualTo("minioadmin"); - await Assert - .That(configuration.Command) - .IsEquivalentTo(Expected); - await Assert.That(configuration.PortBindings.Count).IsEqualTo(2); - await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 9000 && p.AssignRandomHostPort); - await Assert.That(configuration.PortBindings).Contains(p => p.ContainerPort == 9001 && p.AssignRandomHostPort); - } - - [Test] - public async Task BuildConfig_EmptySecret_Throws() - { - var builder = new MinIOBuilder().WithSecretKey(string.Empty); - - await Assert.That(() => builder.Build()).Throws(); - } -} diff --git a/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs b/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs index 92a1fa5..fe21b4f 100644 --- a/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs +++ b/src/tests/MsSql.IntegrationTests/MsSqlIntegrationTests.cs @@ -2,17 +2,14 @@ namespace Purview.WslContainers.MsSql; -class MsSqlIntegrationTests +public class MsSqlIntegrationTests { [Test] public async Task MsSql_ConnectsAndRunsQuery() { await WslcTest.SkipIfUnavailableAsync(); - await using var sqlServer = new MsSqlBuilder() - .WithPassword("SomeStrong!Password1") - .AcceptLicense() - .Build(); + await using var sqlServer = new MsSqlBuilder().WithPassword("SomeStrong!Password1").AcceptLicense().Build(); await sqlServer.StartAsync(); @@ -29,10 +26,7 @@ public async Task ConnectionString_UsesMappedRandomPort() { await WslcTest.SkipIfUnavailableAsync(); - await using var sqlServer = new MsSqlBuilder() - .WithPassword("SomeStrong!Password1") - .AcceptLicense() - .Build(); + await using var sqlServer = new MsSqlBuilder().WithPassword("SomeStrong!Password1").AcceptLicense().Build(); await sqlServer.StartAsync(); diff --git a/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs index 4915107..173a10e 100644 --- a/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs +++ b/src/tests/MsSql.UnitTests/MsSqlBuilderTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.MsSql; -class MsSqlBuilderTests +public class MsSqlBuilderTests { [Test] public async Task Build_WithoutAcceptLicense_Throws() diff --git a/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs b/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs index 6ddae54..a88f51d 100644 --- a/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs +++ b/src/tests/MySql.IntegrationTests/MySqlIntegrationTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.MySql; -class MySqlIntegrationTests +public class MySqlIntegrationTests { [Test] public async Task MySql_ConnectsAndRunsQuery() diff --git a/src/tests/MySql.UnitTests/MySqlBuilderTests.cs b/src/tests/MySql.UnitTests/MySqlBuilderTests.cs index c978e85..8f3e5f0 100644 --- a/src/tests/MySql.UnitTests/MySqlBuilderTests.cs +++ b/src/tests/MySql.UnitTests/MySqlBuilderTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.MySql; -class MySqlBuilderTests +public class MySqlBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() diff --git a/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs b/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs index 6bb1982..3e72d1e 100644 --- a/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs +++ b/src/tests/Nats.IntegrationTests/NatsIntegrationTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.Nats; -class NatsIntegrationTests +public class NatsIntegrationTests { [Test] public async Task Nats_RespondsToPing() diff --git a/src/tests/Nats.UnitTests/NatsBuilderTests.cs b/src/tests/Nats.UnitTests/NatsBuilderTests.cs index 50f2884..96e4ea1 100644 --- a/src/tests/Nats.UnitTests/NatsBuilderTests.cs +++ b/src/tests/Nats.UnitTests/NatsBuilderTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.Nats; -class NatsBuilderTests +public class NatsBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() diff --git a/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs b/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs index f12c7de..ebefacf 100644 --- a/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs +++ b/src/tests/PostgreSql.IntegrationTests/PostgreSqlIntegrationTests.cs @@ -3,7 +3,7 @@ namespace Purview.WslContainers.PostgreSql; -class PostgreSqlIntegrationTests +public class PostgreSqlIntegrationTests { [Test] public async Task PostgreSql_ConnectsAndRunsQuery() diff --git a/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs b/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs index 36befbf..12c93dc 100644 --- a/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs +++ b/src/tests/PostgreSql.UnitTests/PostgreSqlBuilderTests.cs @@ -4,7 +4,7 @@ namespace Purview.WslContainers.PostgreSql; -class PostgreSqlBuilderTests +public class PostgreSqlBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() diff --git a/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs b/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs index 1df8262..837944f 100644 --- a/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs +++ b/src/tests/RabbitMq.IntegrationTests/RabbitMqIntegrationTests.cs @@ -3,22 +3,18 @@ namespace Purview.WslContainers.RabbitMq; -class RabbitMqIntegrationTests +public class RabbitMqIntegrationTests { [Test] public async Task RabbitMq_PublishesAndConsumes() { await WslcTest.SkipIfUnavailableAsync(); - await using var rabbit = new RabbitMqBuilder() - .WithUsername("guest") - .WithPassword("guest") - .Build(); + await using var rabbit = new RabbitMqBuilder().WithUsername("guest").WithPassword("guest").Build(); await rabbit.StartAsync(); - ConnectionFactory factory = new() - { Uri = rabbit.GetAmqpEndpoint() }; + ConnectionFactory factory = new() { Uri = rabbit.GetAmqpEndpoint() }; using var connection = await factory.CreateConnectionAsync(); using var channel = await connection.CreateChannelAsync(); diff --git a/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs b/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs index e90e2e0..a4524d6 100644 --- a/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs +++ b/src/tests/RabbitMq.UnitTests/RabbitMqBuilderTests.cs @@ -3,7 +3,7 @@ namespace Purview.WslContainers.RabbitMq; -class RabbitMqBuilderTests +public class RabbitMqBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() diff --git a/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs b/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs index 344cdb3..3c47a9d 100644 --- a/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs +++ b/src/tests/Redis.IntegrationTests/RedisIntegrationTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.Redis; -class RedisIntegrationTests +public class RedisIntegrationTests { [Test] public async Task Redis_RespondsToPing() @@ -13,9 +13,7 @@ public async Task Redis_RespondsToPing() await redis.StartAsync(); - await using var connection = await ConnectionMultiplexer.ConnectAsync( - redis.GetConnectionString() - ); + await using var connection = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString()); var db = connection.GetDatabase(); var pong = await db.ExecuteAsync("PING"); diff --git a/src/tests/Redis.UnitTests/RedisBuilderTests.cs b/src/tests/Redis.UnitTests/RedisBuilderTests.cs index e962d00..4629e9f 100644 --- a/src/tests/Redis.UnitTests/RedisBuilderTests.cs +++ b/src/tests/Redis.UnitTests/RedisBuilderTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers.Redis; -class RedisBuilderTests +public class RedisBuilderTests { [Test] public async Task BuildConfig_AppliesDefaults() @@ -28,9 +28,7 @@ public async Task BuildConfig_SupportsCompatibleImages() [Test] public async Task BuildConfig_PreservesCustomWaitStrategy() { - var configuration = new RedisBuilder() - .WithWaitStrategy(Wait.ForTcpPort(6379)) - .BuildConfigurationForTesting(); + var configuration = new RedisBuilder().WithWaitStrategy(Wait.ForTcpPort(6379)).BuildConfigurationForTesting(); await Assert.That(configuration.WaitStrategies.Count).IsEqualTo(1); await Assert.That(configuration.WaitStrategies[0]).IsTypeOf(); diff --git a/src/tests/SharedTestingFramework/WslcTest.cs b/src/tests/SharedTestingFramework/WslcTest.cs index f7f8e7d..c867ef0 100644 --- a/src/tests/SharedTestingFramework/WslcTest.cs +++ b/src/tests/SharedTestingFramework/WslcTest.cs @@ -21,9 +21,7 @@ public static async Task SkipIfUnavailableAsync() public static async Task WaitForTcpAsync(int port, TimeSpan timeout) { using System.Net.Sockets.TcpClient client = new(); - using CancellationTokenSource timeoutSource = new( - timeout - ); + using CancellationTokenSource timeoutSource = new(timeout); await client.ConnectAsync(System.Net.IPAddress.Loopback, port, timeoutSource.Token); } } diff --git a/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs b/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs index 7859e65..5556c5d 100644 --- a/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs +++ b/src/tests/WslContainers.IntegrationTests/AlpineLifecycleTests.cs @@ -2,7 +2,8 @@ namespace Purview.WslContainers; -class AlpineLifecycleTests +[Explicit] +public class AlpineLifecycleTests { [Test] public async Task EchoContainer_ProducesOutputAndCleansUp() @@ -43,10 +44,7 @@ public async Task StopAsync_ThenDisposeAsync_IsIdempotent() { await WslcTest.SkipIfUnavailableAsync(); - var container = new ContainerBuilder() - .WithImage("alpine:latest") - .WithCommand("/bin/sleep", "300") - .Build(); + var container = new ContainerBuilder().WithImage("alpine:latest").WithCommand("/bin/sleep", "300").Build(); await container.StartAsync(); await Assert.That(container.State).IsEqualTo(ContainerState.Running); diff --git a/src/tests/WslContainers.IntegrationTests/CleanupTests.cs b/src/tests/WslContainers.IntegrationTests/CleanupTests.cs index 57a5678..be011ca 100644 --- a/src/tests/WslContainers.IntegrationTests/CleanupTests.cs +++ b/src/tests/WslContainers.IntegrationTests/CleanupTests.cs @@ -3,7 +3,8 @@ namespace Purview.WslContainers; -class CleanupTests +[Explicit] +public class CleanupTests { [Test] public async Task Dispose_AfterWaitTimeout_IsIdempotent() @@ -39,10 +40,7 @@ public async Task Stop_ThenDispose_IsIdempotent() { await WslcTest.SkipIfUnavailableAsync(); - var container = new ContainerBuilder() - .WithImage("alpine:latest") - .WithCommand("/bin/sleep", "300") - .Build(); + var container = new ContainerBuilder().WithImage("alpine:latest").WithCommand("/bin/sleep", "300").Build(); await container.StartAsync(); await container.StopAsync(); diff --git a/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs b/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs index bf632f1..caf78f4 100644 --- a/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs +++ b/src/tests/WslContainers.IntegrationTests/ConcurrencyStressTests.cs @@ -2,7 +2,8 @@ namespace Purview.WslContainers; -class ConcurrencyStressTests +[Explicit] +public class ConcurrencyStressTests { [Test] public async Task ConcurrentContainers_GetDistinctUsablePorts() diff --git a/src/tests/WslContainers.IntegrationTests/ExecTests.cs b/src/tests/WslContainers.IntegrationTests/ExecTests.cs index b388d07..405ee13 100644 --- a/src/tests/WslContainers.IntegrationTests/ExecTests.cs +++ b/src/tests/WslContainers.IntegrationTests/ExecTests.cs @@ -2,7 +2,8 @@ namespace Purview.WslContainers; -class ExecTests +[Explicit] +public class ExecTests { [Test] public async Task Exec_ReturnsStdoutAndExitCode() diff --git a/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs b/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs index ac77ee7..c38891f 100644 --- a/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs +++ b/src/tests/WslContainers.IntegrationTests/PerformanceTests.cs @@ -2,7 +2,8 @@ namespace Purview.WslContainers; -class PerformanceTests +[Explicit] +public class PerformanceTests { [Test] [NotInParallel] @@ -12,10 +13,7 @@ public async Task WarmContainerStart_IsFast(CancellationToken cancellationToken) // Warm the image into the shared session store. await using ( - var warm = new ContainerBuilder() - .WithImage("alpine:latest") - .WithCommand("/bin/echo", "warm") - .Build() + var warm = new ContainerBuilder().WithImage("alpine:latest").WithCommand("/bin/echo", "warm").Build() ) { await warm.StartAsync(cancellationToken); @@ -29,7 +27,10 @@ public async Task WarmContainerStart_IsFast(CancellationToken cancellationToken) await container.StartAsync(cancellationToken); stopwatch.Stop(); - await TestContext.Current!.OutputWriter.WriteLineAsync($"warm container start: {stopwatch.Elapsed}", cancellationToken); + await TestContext.Current!.OutputWriter.WriteLineAsync( + $"warm container start: {stopwatch.Elapsed}", + cancellationToken + ); await Assert.That(stopwatch.Elapsed).IsLessThan(TimeSpan.FromSeconds(60)); } } diff --git a/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs b/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs index 3b53d77..6ec39f1 100644 --- a/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs +++ b/src/tests/WslContainers.IntegrationTests/PortMappingTests.cs @@ -4,7 +4,8 @@ namespace Purview.WslContainers; -class PortMappingTests +[Explicit] +public class PortMappingTests { [Test] public async Task RandomHostPort_IsAssignedAndReachable() diff --git a/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs b/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs index 5147ee6..83f625c 100644 --- a/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs +++ b/src/tests/WslContainers.IntegrationTests/RuntimeInfoTests.cs @@ -1,6 +1,7 @@ namespace Purview.WslContainers; -class RuntimeInfoTests +[Explicit] +public class RuntimeInfoTests { [Test] public async Task GetInfoAsync_ReportsAvailability() diff --git a/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs b/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs index 21bee0e..fb5b134 100644 --- a/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs +++ b/src/tests/WslContainers.IntegrationTests/SharedStorageTests.cs @@ -2,7 +2,8 @@ namespace Purview.WslContainers; -class SharedStorageTests +[Explicit] +public class SharedStorageTests { [Test] public async Task SharedStore_IsReusedAcrossSequentialRuntimes() @@ -14,11 +15,11 @@ public async Task SharedStore_IsReusedAcrossSequentialRuntimes() try { IReadOnlyList firstRun; - await using (var runtime1 = new WslContainerRuntime(new WslContainerRuntimeOptions - { - StoragePath = path, - DisableProcessExitCleanup = true, - })) + await using ( + var runtime1 = new WslContainerRuntime( + new WslContainerRuntimeOptions { StoragePath = path, DisableProcessExitCleanup = true } + ) + ) { await using var container = new ContainerBuilder(runtime1) .WithImage("alpine:latest") @@ -30,18 +31,22 @@ public async Task SharedStore_IsReusedAcrossSequentialRuntimes() } IReadOnlyList secondRun; - await using (var runtime2 = new WslContainerRuntime(new WslContainerRuntimeOptions - { - StoragePath = path, - DisableProcessExitCleanup = true, - })) + await using ( + var runtime2 = new WslContainerRuntime( + new WslContainerRuntimeOptions { StoragePath = path, DisableProcessExitCleanup = true } + ) + ) { var session2 = await runtime2.GetSessionAsync(); secondRun = await session2.ListImagesAsync(); } - await Assert.That(firstRun.Any(image => image.Name.Contains("alpine", StringComparison.OrdinalIgnoreCase))).IsTrue(); - await Assert.That(secondRun.Any(image => image.Name.Contains("alpine", StringComparison.OrdinalIgnoreCase))).IsTrue(); + await Assert + .That(firstRun.Any(image => image.Name.Contains("alpine", StringComparison.OrdinalIgnoreCase))) + .IsTrue(); + await Assert + .That(secondRun.Any(image => image.Name.Contains("alpine", StringComparison.OrdinalIgnoreCase))) + .IsTrue(); } finally { diff --git a/src/tests/WslContainers.IntegrationTests/VolumeTests.cs b/src/tests/WslContainers.IntegrationTests/VolumeTests.cs index c877fe1..d17d656 100644 --- a/src/tests/WslContainers.IntegrationTests/VolumeTests.cs +++ b/src/tests/WslContainers.IntegrationTests/VolumeTests.cs @@ -1,6 +1,7 @@ namespace Purview.WslContainers; -class VolumeTests +[Explicit] +public class VolumeTests { [Test] public async Task BindMount_ReadsHostFile(CancellationToken cancellationToken) diff --git a/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs b/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs index e1debf0..c57787a 100644 --- a/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs +++ b/src/tests/WslContainers.IntegrationTests/WaitStrategyTests.cs @@ -5,7 +5,8 @@ namespace Purview.WslContainers; -class WaitStrategyTests +[Explicit] +public class WaitStrategyTests { [Test] public async Task LogMessage_WaitsForReadiness() diff --git a/src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj b/src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj index e6c35ec..6b512ec 100644 --- a/src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj +++ b/src/tests/WslContainers.IntegrationTests/WslContainers.IntegrationTests.csproj @@ -1 +1 @@ - + diff --git a/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs b/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs index 58c970f..2ef605e2 100644 --- a/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs +++ b/src/tests/WslContainers.UnitTests/ContainerBuilderTests.cs @@ -4,7 +4,7 @@ namespace Purview.WslContainers; -class ContainerBuilderTests +public class ContainerBuilderTests { sealed class TestBuilder : ContainerBuilder { diff --git a/src/tests/WslContainers.UnitTests/FakeContainer.cs b/src/tests/WslContainers.UnitTests/FakeContainer.cs index 3eec705..2651bb7 100644 --- a/src/tests/WslContainers.UnitTests/FakeContainer.cs +++ b/src/tests/WslContainers.UnitTests/FakeContainer.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers; -sealed class FakeContainer : IContainer +public sealed class FakeContainer : IContainer { public string Id { get; init; } = "fake-id"; diff --git a/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs b/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs index 1af5d81..2cd6749 100644 --- a/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs +++ b/src/tests/WslContainers.UnitTests/HttpWaitStrategyTests.cs @@ -1,11 +1,11 @@ -using Purview.WslContainers.Waiting; using System.Net; using System.Net.Sockets; using System.Text; +using Purview.WslContainers.Waiting; namespace Purview.WslContainers; -class HttpWaitStrategyTests +public class HttpWaitStrategyTests { [Test] public async Task Http_ReturnsTrueOnExpectedStatus(CancellationToken cancellationToken) @@ -60,9 +60,7 @@ public async Task Http_StatusPredicate_IsUsed(CancellationToken cancellationToke PortMappings = new Dictionary { [80] = (ushort)port }, }; WaitContext context = new(container, container.PortMappings, networkIp: null); - var strategy = Wait.ForHttp("/") - .ForPort(80) - .ForStatusPredicate(status => status is >= 200 and < 300); + var strategy = Wait.ForHttp("/").ForPort(80).ForStatusPredicate(status => status is >= 200 and < 300); await Assert.That(await strategy.UntilAsync(context, cancellationToken)).IsTrue(); } @@ -82,7 +80,10 @@ public async Task Http_ReturnsFalseWhenPortNotMapped(CancellationToken cancellat await Assert.That(await strategy.UntilAsync(context, cancellationToken)).IsFalse(); } - static (int Port, HttpListener Listener, Task Handler) StartServer(HttpStatusCode status, CancellationToken cancellationToken) + static (int Port, HttpListener Listener, Task Handler) StartServer( + HttpStatusCode status, + CancellationToken cancellationToken + ) { using TcpListener probe = new(IPAddress.Loopback, 0); probe.Start(); @@ -93,29 +94,32 @@ public async Task Http_ReturnsFalseWhenPortNotMapped(CancellationToken cancellat listener.Prefixes.Add($"http://127.0.0.1:{port}/"); listener.Start(); - var handler = Task.Run(async () => - { - while (listener.IsListening && !cancellationToken.IsCancellationRequested) + var handler = Task.Run( + async () => { - try - { - var ctx = await listener.GetContextAsync(); - ctx.Response.StatusCode = (int)status; - var body = Encoding.UTF8.GetBytes("ok"); - ctx.Response.ContentLength64 = body.Length; - await ctx.Response.OutputStream.WriteAsync(body, cancellationToken); - ctx.Response.Close(); - } - catch (HttpListenerException) - { - break; - } - catch (ObjectDisposedException) + while (listener.IsListening && !cancellationToken.IsCancellationRequested) { - break; + try + { + var ctx = await listener.GetContextAsync(); + ctx.Response.StatusCode = (int)status; + var body = Encoding.UTF8.GetBytes("ok"); + ctx.Response.ContentLength64 = body.Length; + await ctx.Response.OutputStream.WriteAsync(body, cancellationToken); + ctx.Response.Close(); + } + catch (HttpListenerException) + { + break; + } + catch (ObjectDisposedException) + { + break; + } } - } - }, cancellationToken); + }, + cancellationToken + ); return (port, listener, handler); } diff --git a/src/tests/WslContainers.UnitTests/ImageTests.cs b/src/tests/WslContainers.UnitTests/ImageTests.cs index 11234ea..ae97d1a 100644 --- a/src/tests/WslContainers.UnitTests/ImageTests.cs +++ b/src/tests/WslContainers.UnitTests/ImageTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers; -class ImageTests +public class ImageTests { [Test] public async Task Parse_ShortName_DefaultsToDockerHubLibrary() diff --git a/src/tests/WslContainers.UnitTests/SecretTests.cs b/src/tests/WslContainers.UnitTests/SecretTests.cs index e3b032f..6d15f67 100644 --- a/src/tests/WslContainers.UnitTests/SecretTests.cs +++ b/src/tests/WslContainers.UnitTests/SecretTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers; -class SecretTests +public class SecretTests { [Test] public async Task ToString_RedactsValue() diff --git a/src/tests/WslContainers.UnitTests/StorageModeTests.cs b/src/tests/WslContainers.UnitTests/StorageModeTests.cs index 7f422c8..f980eb2 100644 --- a/src/tests/WslContainers.UnitTests/StorageModeTests.cs +++ b/src/tests/WslContainers.UnitTests/StorageModeTests.cs @@ -1,6 +1,6 @@ namespace Purview.WslContainers; -class StorageModeTests +public class StorageModeTests { [Test] public async Task DefaultOptions_UseSharedStorage() diff --git a/src/tests/WslContainers.UnitTests/WaitContextTests.cs b/src/tests/WslContainers.UnitTests/WaitContextTests.cs index 68c22b0..9bf29f3 100644 --- a/src/tests/WslContainers.UnitTests/WaitContextTests.cs +++ b/src/tests/WslContainers.UnitTests/WaitContextTests.cs @@ -2,12 +2,11 @@ namespace Purview.WslContainers; -class WaitContextTests +public class WaitContextTests { static (WaitContext, IAsyncDisposable) Create(IReadOnlyDictionary mappings) { - FakeContainer container = new() - { PortMappings = mappings }; + FakeContainer container = new() { PortMappings = mappings }; return (new WaitContext(container, mappings, "172.17.0.9"), container); } diff --git a/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs b/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs index c372fdc..ad95ae0 100644 --- a/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs +++ b/src/tests/WslContainers.UnitTests/WaitStrategyRunnerTests.cs @@ -3,13 +3,12 @@ namespace Purview.WslContainers; -class WaitStrategyRunnerTests +public class WaitStrategyRunnerTests { [Test] public async Task RunAsync_SucceedsWhenConditionEventuallyTrue(CancellationToken cancellationToken) { - await using FakeContainer container = new() - { Name = "runner-test" }; + await using FakeContainer container = new() { Name = "runner-test" }; WaitContext context = new(container, container.PortMappings, networkIp: null); var calls = 0; IWaitStrategy strategy = Wait.ForCustom((_, _) => Task.FromResult(++calls >= 3)) @@ -37,12 +36,7 @@ public async Task RunAsync_TimesOut_ThrowsWithDiagnostics(CancellationToken canc Exception? thrown = null; try { - await WaitStrategyRunner.RunAsync( - context, - new[] { strategy }, - TimeSpan.FromSeconds(5), - cancellationToken - ); + await WaitStrategyRunner.RunAsync(context, new[] { strategy }, TimeSpan.FromSeconds(5), cancellationToken); } catch (WslContainerTimeoutException ex) { diff --git a/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs b/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs index 3eadce3..e6b2498 100644 --- a/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs +++ b/src/tests/WslContainers.UnitTests/WaitStrategyTests.cs @@ -1,10 +1,10 @@ +using System.Text.RegularExpressions; using Purview.WslContainers.Containers; using Purview.WslContainers.Waiting; -using System.Text.RegularExpressions; namespace Purview.WslContainers; -class WaitStrategyTests +public class WaitStrategyTests { static WaitContext Context(FakeContainer container) { @@ -26,8 +26,7 @@ public async Task LogMessage_MatchesAccumulatedOutput(CancellationToken cancella [Test] public async Task LogMessage_DoesNotMatchAbsentMessage(CancellationToken cancellationToken) { - await using FakeContainer container = new() - { Logs = "starting up\n" }; + await using FakeContainer container = new() { Logs = "starting up\n" }; #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. var strategy = Wait.ForLogMessage(new Regex("ready to accept")); #pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. @@ -38,8 +37,7 @@ public async Task LogMessage_DoesNotMatchAbsentMessage(CancellationToken cancell [Test] public async Task Command_ReturnsTrueOnExitCodeZero(CancellationToken cancellationToken) { - await using FakeContainer container = new() - { Exec = _ => new ExecResult(0, "ok", string.Empty) }; + await using FakeContainer container = new() { Exec = _ => new ExecResult(0, "ok", string.Empty) }; var strategy = Wait.ForCommand("/bin/true"); await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsTrue(); @@ -48,8 +46,7 @@ public async Task Command_ReturnsTrueOnExitCodeZero(CancellationToken cancellati [Test] public async Task Command_ReturnsFalseOnNonZeroExit(CancellationToken cancellationToken) { - await using FakeContainer container = new() - { Exec = _ => new ExecResult(1, string.Empty, "boom") }; + await using FakeContainer container = new() { Exec = _ => new ExecResult(1, string.Empty, "boom") }; var strategy = Wait.ForCommand("/bin/false"); await Assert.That(await strategy.UntilAsync(Context(container), cancellationToken)).IsFalse(); @@ -87,10 +84,8 @@ public async Task ForAll_RequiresEveryStrategy(CancellationToken cancellationTok [Test] public async Task ContainerRunning_ReflectsState(CancellationToken cancellationToken) { - await using FakeContainer running = new() - { State = ContainerState.Running }; - await using FakeContainer exited = new() - { State = ContainerState.Exited }; + await using FakeContainer running = new() { State = ContainerState.Running }; + await using FakeContainer exited = new() { State = ContainerState.Exited }; var strategy = Wait.ForContainerRunning(); await Assert.That(await strategy.UntilAsync(Context(running), cancellationToken)).IsTrue(); diff --git a/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs b/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs index 2d9fdd3..f716143 100644 --- a/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs +++ b/src/tests/WslContainers.UnitTests/WslContainerRuntimeTests.cs @@ -2,7 +2,7 @@ namespace Purview.WslContainers; -class WslContainerRuntimeTests +public class WslContainerRuntimeTests { const int ErrorSharingViolation = unchecked((int)0x80070020); diff --git a/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs b/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs index 12ded9f..87d12c0 100644 --- a/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs +++ b/src/tests/WslContainers.UnitTests/WslInspectParserTests.cs @@ -1,8 +1,9 @@ namespace Purview.WslContainers; -class WslInspectParserTests +public class WslInspectParserTests { - const string SampleInspect = /*lang=json,strict*/ """ + const string SampleInspect = /*lang=json,strict*/ + """ { "HostConfig": { "NetworkMode": "bridge" }, "NetworkSettings": { From 8df66b2207bec48a7d3c406b413d1fbd934e2fc4 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Wed, 30 Sep 2026 17:29:57 +0100 Subject: [PATCH 4/4] build: added EnableWindowsTargeting --- src/Directory.Build.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b964508..099810b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,6 +4,7 @@ net11.0-windows10.0.19041.0 x64 10.0.26100.80 + true