From f1bb8c2f9521a38caa1a3310d96eb7a3303c8f92 Mon Sep 17 00:00:00 2001 From: McNultyyy Date: Wed, 27 May 2026 08:13:26 +0100 Subject: [PATCH 1/2] Add cross-platform dev environment for local testing Introduces a unified scripts/dev-env.ps1 tool that supports all 5 platform + backend testing combinations: - Windows + in-memory - Windows + Windows emulator - Windows + Linux emulator (Docker) - Linux + in-memory (Docker) - Linux + Linux emulator (Docker) The script manages Docker container lifecycle (start/stop/status), provides an exec command for arbitrary Linux commands, and a test command that auto-starts required services before running tests. Also adds docker-compose.dev.yml defining the dev and emulator services, and removes the now-superseded test-linux.ps1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 46 ++++++ CONTRIBUTING.md | 36 +++- docker-compose.dev.yml | 65 ++++++++ scripts/dev-env.ps1 | 368 +++++++++++++++++++++++++++++++++++++++++ test-linux.ps1 | 83 ---------- 5 files changed, 511 insertions(+), 87 deletions(-) create mode 100644 docker-compose.dev.yml create mode 100644 scripts/dev-env.ps1 delete mode 100644 test-linux.ps1 diff --git a/AGENTS.md b/AGENTS.md index 047847f..7c5193d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,52 @@ Tests are split into two projects. When creating or moving tests, follow these r ### Key constraint The Integration project does **not** have `InternalsVisibleTo` access. If a test needs internal APIs, it belongs in Unit. +## Cross-Platform Testing (Dev Environment) + +When investigating platform-specific test failures, use `scripts/dev-env.ps1` to reproduce issues locally on any platform + backend combination. + +### Quick Reference + +```powershell +# Check current state +./scripts/dev-env.ps1 status + +# Reproduce a Linux CI failure +./scripts/dev-env.ps1 test -Platform linux -Target inmemory -Project integration -Filter "FullyQualifiedName~FailingTest" + +# Reproduce a Linux + emulator CI failure +./scripts/dev-env.ps1 test -Platform linux -Target emulator-linux -Project integration -Filter "FullyQualifiedName~FailingTest" + +# Run against Linux emulator from Windows host (tests emulator behavioral differences) +./scripts/dev-env.ps1 test -Platform windows -Target emulator-linux -Project integration + +# Run arbitrary commands in the Linux container +./scripts/dev-env.ps1 exec -Cmd "dotnet build -c Release" + +# Tear down when done +./scripts/dev-env.ps1 stop +``` + +### Investigation Workflow + +When a test fails in CI on Linux but passes locally on Windows: + +1. First try reproducing on Linux with in-memory: `./scripts/dev-env.ps1 test -Platform linux -Target inmemory -Filter "FullyQualifiedName~TestName"` +2. If it fails → it's a Linux .NET runtime difference (gateway fallback, string handling, etc.) +3. If it passes → try with the emulator: `./scripts/dev-env.ps1 test -Platform linux -Target emulator-linux -Filter "FullyQualifiedName~TestName"` +4. If that fails → it's a Linux + emulator interaction issue +5. Edit source files normally (they're mounted into the container), then re-run + +### Supported Scenarios + +| Platform | Target | Docker Required | +|----------|--------|----------------| +| windows | inmemory | No | +| windows | emulator-windows | No (needs Windows emulator installed) | +| windows | emulator-linux | Yes (emulator container) | +| linux | inmemory | Yes (dev container) | +| linux | emulator-linux | Yes (dev + emulator containers) | + ## Documentation After any changes are made that might effect the public API or functionality, documentation must be updated to reflect those changes. The documentation should be clear and comprehensive, covering all new features, changes to existing features, and any deprecations or removals. This includes updating README file (if relevant), but mainly the wiki which can be found in a sister folder to the main repository - ../CosmosDB.InMemoryEmulator.wiki. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ea39fd..8d56be9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ Thank you for your interest in contributing! - .NET 8.0 SDK (or later) - PowerShell (for test scripts) +- Docker (for cross-platform testing — Docker Desktop or Rancher Desktop) ## Building @@ -24,16 +25,43 @@ dotnet build CosmosDB.InMemoryEmulator.sln ## Running Tests ```powershell -# Unit tests only +# Unit tests only (Windows, in-memory) dotnet test tests/CosmosDB.InMemoryEmulator.Tests.Unit -# Integration tests (in-memory) +# Integration tests (Windows, in-memory) dotnet test tests/CosmosDB.InMemoryEmulator.Tests.Integration +``` + +## Cross-Platform Testing + +This project runs CI on both Windows and Linux. To reproduce platform-specific issues locally, use the dev environment script: + +```powershell +# See what's running and available scenarios +./scripts/dev-env.ps1 status + +# Run tests on Linux (catches gateway fallback / runtime differences) +./scripts/dev-env.ps1 test -Platform linux -Target inmemory -Project integration -# Full parity validation (requires Docker) -./scripts/validate-parity.ps1 +# Run tests on Linux against the Linux Cosmos emulator +./scripts/dev-env.ps1 test -Platform linux -Target emulator-linux -Project integration + +# Run tests on Windows against the Linux Cosmos emulator (emulator behavior differences) +./scripts/dev-env.ps1 test -Platform windows -Target emulator-linux -Project integration + +# Run a specific failing test on Linux for investigation +./scripts/dev-env.ps1 test -Platform linux -Target inmemory -Filter "FullyQualifiedName~MyFailingTest" + +# Start the Linux dev container for interactive exploration +./scripts/dev-env.ps1 start +./scripts/dev-env.ps1 exec -Cmd "dotnet build -c Release" + +# Tear down all containers +./scripts/dev-env.ps1 stop ``` +See [scripts/dev-env.ps1](scripts/dev-env.ps1) for full documentation of all commands and parameters. + ## Guidelines - **TDD**: Write a failing test first, then implement the minimum code to make it pass, then refactor. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..98384cc --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,65 @@ +# Cross-Platform Development Environment +# +# Defines services for local cross-platform testing: +# dev — Linux .NET SDK container for running tests on Linux +# emulator — Linux Cosmos DB emulator for integration tests +# +# Usage: managed via scripts/dev-env.ps1 (preferred) or directly: +# docker compose -f docker-compose.dev.yml up -d dev +# docker compose -f docker-compose.dev.yml up -d emulator +# docker compose -f docker-compose.dev.yml up -d # both + +services: + dev: + image: mcr.microsoft.com/dotnet/sdk:8.0 + container_name: cosmosdb-dev-linux + volumes: + - .:/src + - nuget-cache:/root/.nuget/packages + working_dir: /src + # Install PowerShell on first start — run-tests.ps1 requires it. + # The install is cached in the container's writable layer as long as it stays running. + command: > + bash -c " + if ! command -v pwsh &>/dev/null; then + apt-get update -qq && apt-get install -y -qq powershell >/dev/null 2>&1; + fi; + sleep infinity + " + environment: + - DOTNET_CLI_TELEMETRY_OPTOUT=1 + - DOTNET_NOLOGO=1 + networks: + - dev-net + + emulator: + image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator + container_name: cosmosdb-emulator-linux + ports: + - "8081:8081" + - "10250:10250" + - "10251:10251" + - "10252:10252" + - "10253:10253" + - "10254:10254" + - "10255:10255" + - "10256:10256" + environment: + AZURE_COSMOS_EMULATOR_PARTITION_COUNT: 30 + AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: "false" + AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE: "127.0.0.1" + healthcheck: + test: ["CMD", "curl", "-fks", "https://localhost:8081/_explorer/emulator.pem"] + interval: 10s + timeout: 5s + retries: 60 + start_period: 30s + networks: + - dev-net + +volumes: + nuget-cache: + +networks: + dev-net: + driver: bridge diff --git a/scripts/dev-env.ps1 b/scripts/dev-env.ps1 new file mode 100644 index 0000000..b62849f --- /dev/null +++ b/scripts/dev-env.ps1 @@ -0,0 +1,368 @@ +<# +.SYNOPSIS + Manages the cross-platform development environment for local testing. + +.DESCRIPTION + Provides a unified interface for running tests on different platform + backend + combinations. Manages Docker containers for Linux test execution and the Linux + Cosmos DB emulator. + + Supported scenarios: + Platform=windows, Target=inmemory — runs tests directly on host + Platform=windows, Target=emulator-windows — runs tests on host against Windows emulator + Platform=windows, Target=emulator-linux — starts Linux emulator, runs tests on host + Platform=linux, Target=inmemory — runs tests inside Linux container + Platform=linux, Target=emulator-linux — runs tests in Linux container against emulator + +.PARAMETER Command + The action to perform: start, stop, status, exec, test. + +.PARAMETER Platform + Test runner platform: windows or linux. Required for 'test' command. + +.PARAMETER Target + Cosmos backend target: inmemory, emulator-linux, or emulator-windows. Required for 'test'. + +.PARAMETER Project + Test project: unit, integration, or both. Default: both. + +.PARAMETER Framework + Target framework. Default: net8.0. + +.PARAMETER Filter + dotnet test filter expression. + +.PARAMETER WithEmulator + For 'start' command: also start the Linux Cosmos emulator service. + +.PARAMETER EmulatorOnly + For 'start' command: start only the emulator (not the dev container). + +.PARAMETER Cmd + For 'exec' command: the command to run inside the Linux dev container. + +.EXAMPLE + ./scripts/dev-env.ps1 start + ./scripts/dev-env.ps1 start -WithEmulator + ./scripts/dev-env.ps1 test -Platform linux -Target inmemory -Project integration + ./scripts/dev-env.ps1 test -Platform linux -Target emulator-linux -Filter "FullyQualifiedName~CrudTests" + ./scripts/dev-env.ps1 test -Platform windows -Target emulator-linux -Project integration + ./scripts/dev-env.ps1 exec -Cmd "dotnet build -c Release" + ./scripts/dev-env.ps1 status + ./scripts/dev-env.ps1 stop +#> +param( + [Parameter(Position = 0)] + [ValidateSet('start', 'stop', 'status', 'exec', 'test')] + [string]$Command = 'status', + + [ValidateSet('windows', 'linux')] + [string]$Platform, + + [ValidateSet('inmemory', 'emulator-linux', 'emulator-windows')] + [string]$Target, + + [ValidateSet('unit', 'integration', 'both')] + [string]$Project = 'both', + + [string]$Framework = 'net8.0', + [string]$Filter, + + [switch]$WithEmulator, + [switch]$EmulatorOnly, + + [string]$Cmd +) + +$ErrorActionPreference = 'Stop' +$ComposeFile = Join-Path $PSScriptRoot '..' 'docker-compose.dev.yml' + +# ─── Helper Functions ───────────────────────────────────────────────────────── + +function Test-DockerAvailable { + $version = docker version --format '{{.Server.Version}}' 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Error "Docker is not running. Start Docker Desktop (or Rancher Desktop) and try again." + exit 1 + } + return $version +} + +function Test-ContainerRunning([string]$Name) { + $state = docker inspect --format '{{.State.Running}}' $Name 2>&1 + return ($LASTEXITCODE -eq 0 -and $state -eq 'true') +} + +function Test-EmulatorHealthy { + $health = docker inspect --format '{{.State.Health.Status}}' 'cosmosdb-emulator-linux' 2>&1 + return ($LASTEXITCODE -eq 0 -and $health -eq 'healthy') +} + +function Start-Service([string]$ServiceName) { + Write-Host "Starting service '$ServiceName'..." -ForegroundColor Cyan + docker compose -f $ComposeFile up -d $ServiceName + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to start service '$ServiceName'" + exit 1 + } +} + +function Wait-ForEmulator { + param([int]$TimeoutSeconds = 300) + + Write-Host "Waiting for Cosmos DB emulator to become healthy..." -ForegroundColor Cyan + $elapsed = 0 + while ($elapsed -lt $TimeoutSeconds) { + if (Test-EmulatorHealthy) { + Write-Host "Emulator healthy after ${elapsed}s" -ForegroundColor Green + return + } + Start-Sleep -Seconds 5 + $elapsed += 5 + if ($elapsed % 30 -eq 0) { + Write-Host " Still waiting... (${elapsed}s)" -ForegroundColor DarkGray + } + } + Write-Error "Emulator did not become healthy within ${TimeoutSeconds}s" + exit 1 +} + +function Ensure-DevContainer { + if (-not (Test-ContainerRunning 'cosmosdb-dev-linux')) { + Start-Service 'dev' + # Wait for PowerShell to be installed (container command installs it on first start) + Write-Host "Waiting for container to be ready (installing PowerShell if needed)..." -ForegroundColor Cyan + $elapsed = 0 + while ($elapsed -lt 120) { + $result = docker exec cosmosdb-dev-linux bash -c "command -v pwsh" 2>&1 + if ($LASTEXITCODE -eq 0) { break } + Start-Sleep -Seconds 5 + $elapsed += 5 + } + if ($elapsed -ge 120) { + Write-Error "Container did not become ready within 120s" + exit 1 + } + Write-Host "Container ready." -ForegroundColor Green + + # Restore NuGet packages + Write-Host "Restoring NuGet packages in container..." -ForegroundColor Cyan + docker exec cosmosdb-dev-linux dotnet restore --verbosity quiet + if ($LASTEXITCODE -ne 0) { + Write-Warning "NuGet restore had issues — tests may still work if packages are cached." + } + } +} + +function Ensure-Emulator { + if (-not (Test-ContainerRunning 'cosmosdb-emulator-linux')) { + Start-Service 'emulator' + } + if (-not (Test-EmulatorHealthy)) { + Wait-ForEmulator + } +} + +function Invoke-InContainer { + param( + [string]$Command, + [hashtable]$EnvVars = @{} + ) + + $envArgs = @() + foreach ($kv in $EnvVars.GetEnumerator()) { + $envArgs += '-e' + $envArgs += "$($kv.Key)=$($kv.Value)" + } + + docker exec @envArgs cosmosdb-dev-linux bash -c $Command + return $LASTEXITCODE +} + +# ─── Commands ───────────────────────────────────────────────────────────────── + +function Invoke-Start { + $dockerVersion = Test-DockerAvailable + Write-Host "Docker $dockerVersion detected" -ForegroundColor Cyan + + if ($EmulatorOnly) { + Ensure-Emulator + Write-Host "`nEmulator running at https://localhost:8081" -ForegroundColor Green + } elseif ($WithEmulator) { + Ensure-Emulator + Ensure-DevContainer + Write-Host "`nDev container + emulator running." -ForegroundColor Green + Write-Host " Dev container: cosmosdb-dev-linux" -ForegroundColor DarkGray + Write-Host " Emulator: https://localhost:8081 (host) / https://emulator:8081 (from dev container)" -ForegroundColor DarkGray + } else { + Ensure-DevContainer + Write-Host "`nDev container running: cosmosdb-dev-linux" -ForegroundColor Green + } +} + +function Invoke-Stop { + Write-Host "Stopping dev environment..." -ForegroundColor Cyan + docker compose -f $ComposeFile down + Write-Host "Done." -ForegroundColor Green +} + +function Invoke-Status { + $devRunning = Test-ContainerRunning 'cosmosdb-dev-linux' + $emulatorRunning = Test-ContainerRunning 'cosmosdb-emulator-linux' + $emulatorHealthy = if ($emulatorRunning) { Test-EmulatorHealthy } else { $false } + + Write-Host "Cross-Platform Dev Environment Status" -ForegroundColor Cyan + Write-Host "─────────────────────────────────────" -ForegroundColor DarkGray + $devStatus = if ($devRunning) { "Running" } else { "Stopped" } + $devColor = if ($devRunning) { "Green" } else { "DarkGray" } + Write-Host " Dev container (Linux): $devStatus" -ForegroundColor $devColor + + $emStatus = if ($emulatorHealthy) { "Healthy" } elseif ($emulatorRunning) { "Starting..." } else { "Stopped" } + $emColor = if ($emulatorHealthy) { "Green" } elseif ($emulatorRunning) { "Yellow" } else { "DarkGray" } + Write-Host " Emulator (Linux): $emStatus" -ForegroundColor $emColor + + Write-Host "" + Write-Host "Available test scenarios:" -ForegroundColor Cyan + Write-Host " ./scripts/dev-env.ps1 test -Platform windows -Target inmemory" -ForegroundColor DarkGray + Write-Host " ./scripts/dev-env.ps1 test -Platform windows -Target emulator-windows" -ForegroundColor DarkGray + if ($emulatorRunning -or $emulatorHealthy) { + Write-Host " ./scripts/dev-env.ps1 test -Platform windows -Target emulator-linux" -ForegroundColor White + } else { + Write-Host " ./scripts/dev-env.ps1 test -Platform windows -Target emulator-linux (needs: start -EmulatorOnly)" -ForegroundColor DarkGray + } + if ($devRunning) { + Write-Host " ./scripts/dev-env.ps1 test -Platform linux -Target inmemory" -ForegroundColor White + } else { + Write-Host " ./scripts/dev-env.ps1 test -Platform linux -Target inmemory (needs: start)" -ForegroundColor DarkGray + } + if ($devRunning -and $emulatorHealthy) { + Write-Host " ./scripts/dev-env.ps1 test -Platform linux -Target emulator-linux" -ForegroundColor White + } else { + Write-Host " ./scripts/dev-env.ps1 test -Platform linux -Target emulator-linux (needs: start -WithEmulator)" -ForegroundColor DarkGray + } +} + +function Invoke-Exec { + if (-not $Cmd) { + Write-Error "The -Cmd parameter is required for the 'exec' command. Example: ./scripts/dev-env.ps1 exec -Cmd 'dotnet build -c Release'" + exit 1 + } + + Test-DockerAvailable | Out-Null + Ensure-DevContainer + + Write-Host "Executing in Linux container:" -ForegroundColor Cyan + Write-Host " $Cmd" -ForegroundColor DarkGray + Write-Host "" + + docker exec cosmosdb-dev-linux bash -c $Cmd + exit $LASTEXITCODE +} + +function Invoke-Test { + if (-not $Platform) { + Write-Error "The -Platform parameter is required for the 'test' command. Use: -Platform windows or -Platform linux" + exit 1 + } + if (-not $Target) { + Write-Error "The -Target parameter is required for the 'test' command. Use: -Target inmemory, emulator-linux, or emulator-windows" + exit 1 + } + + # Validate combination + if ($Platform -eq 'linux' -and $Target -eq 'emulator-windows') { + Write-Error "Invalid combination: Linux platform cannot use the Windows emulator." + exit 1 + } + + $runTestsScript = './scripts/run-tests.ps1' + + # Build the run-tests.ps1 arguments + $testArgs = "-Target $Target -Project $Project -Framework $Framework" + if ($Filter) { + $testArgs += " -Filter '$Filter'" + } + + switch ("$Platform|$Target") { + # ── Scenario 1: Windows + in-memory ── + 'windows|inmemory' { + Write-Host "Scenario 1: Windows + in-memory" -ForegroundColor Cyan + $scriptPath = Join-Path $PSScriptRoot 'run-tests.ps1' + $invokeArgs = @{ Target = $Target; Project = $Project; Framework = $Framework } + if ($Filter) { $invokeArgs.Filter = $Filter } + & $scriptPath @invokeArgs + exit $LASTEXITCODE + } + + # ── Scenario 2: Windows + Windows emulator ── + 'windows|emulator-windows' { + Write-Host "Scenario 2: Windows + Windows emulator" -ForegroundColor Cyan + $scriptPath = Join-Path $PSScriptRoot 'run-tests.ps1' + $invokeArgs = @{ Target = $Target; Project = $Project; Framework = $Framework } + if ($Filter) { $invokeArgs.Filter = $Filter } + & $scriptPath @invokeArgs + exit $LASTEXITCODE + } + + # ── Scenario 3: Windows + Linux emulator ── + 'windows|emulator-linux' { + Write-Host "Scenario 3: Windows host + Linux emulator" -ForegroundColor Cyan + Test-DockerAvailable | Out-Null + Ensure-Emulator + Write-Host "" + + $scriptPath = Join-Path $PSScriptRoot 'run-tests.ps1' + $invokeArgs = @{ + Target = 'emulator-linux' + Project = $Project + Framework = $Framework + EmulatorEndpoint = 'https://localhost:8081' + } + if ($Filter) { $invokeArgs.Filter = $Filter } + & $scriptPath @invokeArgs + exit $LASTEXITCODE + } + + # ── Scenario 4: Linux + in-memory ── + 'linux|inmemory' { + Write-Host "Scenario 4: Linux container + in-memory" -ForegroundColor Cyan + Test-DockerAvailable | Out-Null + Ensure-DevContainer + Write-Host "" + + $envVars = @{ + COSMOS_TEST_TARGET = 'inmemory' + } + $cmd = "pwsh -NoProfile -Command `"& $runTestsScript -Target inmemory -Project $Project -Framework $Framework$(if ($Filter) { " -Filter '$Filter'" })`"" + $exitCode = Invoke-InContainer -Command $cmd -EnvVars $envVars + exit $exitCode + } + + # ── Scenario 5: Linux + Linux emulator ── + 'linux|emulator-linux' { + Write-Host "Scenario 5: Linux container + Linux emulator" -ForegroundColor Cyan + Test-DockerAvailable | Out-Null + Ensure-DevContainer + Ensure-Emulator + Write-Host "" + + $envVars = @{ + COSMOS_TEST_TARGET = 'emulator-linux' + COSMOS_EMULATOR_ENDPOINT = 'https://emulator:8081' + } + $cmd = "pwsh -NoProfile -Command `"& $runTestsScript -Target emulator-linux -Project $Project -Framework $Framework -EmulatorEndpoint 'https://emulator:8081'$(if ($Filter) { " -Filter '$Filter'" })`"" + $exitCode = Invoke-InContainer -Command $cmd -EnvVars $envVars + exit $exitCode + } + } +} + +# ─── Dispatch ───────────────────────────────────────────────────────────────── + +switch ($Command) { + 'start' { Invoke-Start } + 'stop' { Invoke-Stop } + 'status' { Invoke-Status } + 'exec' { Invoke-Exec } + 'test' { Invoke-Test } +} diff --git a/test-linux.ps1 b/test-linux.ps1 deleted file mode 100644 index 52147db..0000000 --- a/test-linux.ps1 +++ /dev/null @@ -1,83 +0,0 @@ -<# -.SYNOPSIS - Runs the integration test suite inside a Linux Docker container to catch - platform-specific failures (e.g. Cosmos SDK gateway query plan path) before - pushing to CI. - -.DESCRIPTION - The Cosmos SDK uses a native ServiceInterop DLL on Windows for local query plan - computation. On Linux (including CI), it falls back to a gateway HTTP endpoint, - which exercises different code paths in FakeCosmosHandler. This script reproduces - that Linux behavior locally via Docker. - - By default this script runs only the integration test classes that the - Emulator Parity workflow validates (Crud, Ttl, Batch, QueryAdvanced) — the - other integration test classes (PartitionKey, Linq, CrudHardening, Bulk) - overload the Linux Cosmos emulator under per-test container churn and are - excluded from CI parity. Pass -Filter to override. - - NuGet packages are restored offline from the host's package cache to avoid - network dependencies. - -.PARAMETER Filter - xUnit test filter expression. Defaults to the parity-clean integration - classes; pass an explicit value to run a different set - (e.g. "FullyQualifiedName~OrderBy"). - -.EXAMPLE - .\test-linux.ps1 - # Runs the parity-clean integration test classes - -.EXAMPLE - .\test-linux.ps1 -Filter "FullyQualifiedName~FakeCosmosHandlerLinqTests" - # Runs only the Linq integration tests -#> -param( - [string]$Filter = "FullyQualifiedName~FakeCosmosHandlerCrudTests|FullyQualifiedName~FakeCosmosHandlerTtlTests|FullyQualifiedName~FakeCosmosHandlerBatchTests|FullyQualifiedName~FakeCosmosHandlerQueryAdvancedTests" -) - -$ErrorActionPreference = 'Stop' - -# Verify Docker is available -$dockerVersion = docker version --format '{{.Server.Version}}' 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Error "Docker is not running. Start Rancher Desktop (or Docker Desktop) and try again." - return -} -Write-Host "Docker $dockerVersion detected" -ForegroundColor Cyan - -$repoRoot = $PSScriptRoot -$nugetCache = Join-Path $env:USERPROFILE '.nuget\packages' - -if (-not (Test-Path $nugetCache)) { - Write-Error "NuGet package cache not found at $nugetCache" - return -} - -$testProject = 'tests/CosmosDB.InMemoryEmulator.Tests.Integration' -$filterArg = if ($Filter) { "--filter '$Filter'" } else { "" } - -$testCmd = @( - "dotnet nuget add source /root/.nuget/packages --name local-cache 2>/dev/null;" - "find /src -name 'project.assets.json' -delete 2>/dev/null;" - "dotnet restore $testProject -p:TargetFrameworks=net8.0 --source /root/.nuget/packages -p:TreatWarningsAsErrors=false 2>/dev/null;" - "dotnet test $testProject --nologo -c Release -f net8.0 --no-restore -p:TreatWarningsAsErrors=false $filterArg" -) -join ' ' - -Write-Host "Running integration tests on Linux (mcr.microsoft.com/dotnet/sdk:8.0)..." -ForegroundColor Cyan -Write-Host " Filter: $Filter" -ForegroundColor DarkGray - -docker run --rm ` - -v "${repoRoot}:/src" ` - -v "${nugetCache}:/root/.nuget/packages" ` - -w /src ` - mcr.microsoft.com/dotnet/sdk:8.0 ` - bash -c $testCmd - -if ($LASTEXITCODE -ne 0) { - Write-Host "`nLinux tests failed." -ForegroundColor Red -} else { - Write-Host "`nLinux tests passed." -ForegroundColor Green -} - -exit $LASTEXITCODE From aef9db90be55614af04ccea695dd976cf3c49881 Mon Sep 17 00:00:00 2001 From: McNultyyy Date: Wed, 27 May 2026 10:30:44 +0100 Subject: [PATCH 2/2] fix: resolve Docker networking and naming issues in dev environment - Use network_mode: service:emulator for shared network namespace (replicates CI behavior where emulator is at localhost:8081) - Add Docker Compose profiles: standalone, with-emulator, emulator-only - Rename Start-Service to Start-DockerService to avoid PowerShell conflict - Fix exit code leak in status command from docker inspect - Mount host NuGet cache as fallback source for corporate proxy environments - Remove ports 10250-10256 (unnecessary with shared network namespace) - Only publish port 8081 for host access in emulator-only scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docker-compose.dev.yml | 52 ++++++++++++++++++++++++++------------- scripts/dev-env.ps1 | 56 +++++++++++++++++++++++++++++------------- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 98384cc..ebf0824 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -8,17 +8,49 @@ # docker compose -f docker-compose.dev.yml up -d dev # docker compose -f docker-compose.dev.yml up -d emulator # docker compose -f docker-compose.dev.yml up -d # both +# +# Networking: The dev container shares the emulator's network namespace +# (network_mode: "service:emulator") so the tests can reach the emulator +# at localhost:8081 — exactly like CI. When only the dev container is running +# (no emulator), it uses bridge networking by default via the dev-standalone profile. services: dev: image: mcr.microsoft.com/dotnet/sdk:8.0 container_name: cosmosdb-dev-linux + profiles: ["with-emulator"] + # Share the emulator's network namespace — tests see emulator at localhost:8081 + # just like in CI (GitHub Actions service containers share localhost). + network_mode: "service:emulator" + depends_on: + emulator: + condition: service_healthy volumes: - .:/src + - ${NUGET_PACKAGES:-${USERPROFILE:-.}/.nuget/packages}:/root/.nuget/fallback-packages:ro + - nuget-cache:/root/.nuget/packages + working_dir: /src + command: > + bash -c " + if ! command -v pwsh &>/dev/null; then + apt-get update -qq && apt-get install -y -qq powershell >/dev/null 2>&1; + fi; + sleep infinity + " + environment: + - DOTNET_CLI_TELEMETRY_OPTOUT=1 + - DOTNET_NOLOGO=1 + + # Standalone dev container (no emulator dependency) — for inmemory-only Linux testing + dev-standalone: + image: mcr.microsoft.com/dotnet/sdk:8.0 + container_name: cosmosdb-dev-linux + profiles: ["standalone"] + volumes: + - .:/src + - ${NUGET_PACKAGES:-${USERPROFILE:-.}/.nuget/packages}:/root/.nuget/fallback-packages:ro - nuget-cache:/root/.nuget/packages working_dir: /src - # Install PowerShell on first start — run-tests.ps1 requires it. - # The install is cached in the container's writable layer as long as it stays running. command: > bash -c " if ! command -v pwsh &>/dev/null; then @@ -29,21 +61,13 @@ services: environment: - DOTNET_CLI_TELEMETRY_OPTOUT=1 - DOTNET_NOLOGO=1 - networks: - - dev-net emulator: image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator container_name: cosmosdb-emulator-linux + profiles: ["with-emulator", "emulator-only"] ports: - "8081:8081" - - "10250:10250" - - "10251:10251" - - "10252:10252" - - "10253:10253" - - "10254:10254" - - "10255:10255" - - "10256:10256" environment: AZURE_COSMOS_EMULATOR_PARTITION_COUNT: 30 AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: "false" @@ -54,12 +78,6 @@ services: timeout: 5s retries: 60 start_period: 30s - networks: - - dev-net volumes: nuget-cache: - -networks: - dev-net: - driver: bridge diff --git a/scripts/dev-env.ps1 b/scripts/dev-env.ps1 index b62849f..af24f98 100644 --- a/scripts/dev-env.ps1 +++ b/scripts/dev-env.ps1 @@ -98,9 +98,13 @@ function Test-EmulatorHealthy { return ($LASTEXITCODE -eq 0 -and $health -eq 'healthy') } -function Start-Service([string]$ServiceName) { +function Start-DockerService([string]$ServiceName, [string]$Profile = '') { Write-Host "Starting service '$ServiceName'..." -ForegroundColor Cyan - docker compose -f $ComposeFile up -d $ServiceName + if ($Profile) { + docker compose -f $ComposeFile --profile $Profile up -d $ServiceName + } else { + docker compose -f $ComposeFile up -d $ServiceName + } if ($LASTEXITCODE -ne 0) { Write-Error "Failed to start service '$ServiceName'" exit 1 @@ -128,8 +132,17 @@ function Wait-ForEmulator { } function Ensure-DevContainer { + param([switch]$WithEmulatorNetwork) + if (-not (Test-ContainerRunning 'cosmosdb-dev-linux')) { - Start-Service 'dev' + if ($WithEmulatorNetwork) { + # Start with the with-emulator profile — dev shares emulator's network namespace. + # The emulator must be healthy first (depends_on condition ensures this). + Start-DockerService 'dev' 'with-emulator' + } else { + # Standalone — no emulator dependency, uses default bridge networking. + Start-DockerService 'dev-standalone' 'standalone' + } # Wait for PowerShell to be installed (container command installs it on first start) Write-Host "Waiting for container to be ready (installing PowerShell if needed)..." -ForegroundColor Cyan $elapsed = 0 @@ -145,18 +158,19 @@ function Ensure-DevContainer { } Write-Host "Container ready." -ForegroundColor Green - # Restore NuGet packages + # Configure NuGet to use the mounted host package cache as a fallback source, + # avoiding network issues (e.g., corporate proxy TLS interception). Write-Host "Restoring NuGet packages in container..." -ForegroundColor Cyan - docker exec cosmosdb-dev-linux dotnet restore --verbosity quiet - if ($LASTEXITCODE -ne 0) { - Write-Warning "NuGet restore had issues — tests may still work if packages are cached." - } + docker exec cosmosdb-dev-linux bash -c "dotnet nuget add source /root/.nuget/fallback-packages --name host-cache 2>/dev/null; dotnet restore -p:TargetFrameworks=net8.0 --source /root/.nuget/fallback-packages --verbosity quiet 2>&1 || true" + # Reset exit code — restore warnings are non-fatal + $global:LASTEXITCODE = 0 + Write-Host "Restore complete." -ForegroundColor Green } } function Ensure-Emulator { if (-not (Test-ContainerRunning 'cosmosdb-emulator-linux')) { - Start-Service 'emulator' + Start-DockerService 'emulator' 'emulator-only' } if (-not (Test-EmulatorHealthy)) { Wait-ForEmulator @@ -190,19 +204,21 @@ function Invoke-Start { Write-Host "`nEmulator running at https://localhost:8081" -ForegroundColor Green } elseif ($WithEmulator) { Ensure-Emulator - Ensure-DevContainer + Ensure-DevContainer -WithEmulatorNetwork Write-Host "`nDev container + emulator running." -ForegroundColor Green - Write-Host " Dev container: cosmosdb-dev-linux" -ForegroundColor DarkGray - Write-Host " Emulator: https://localhost:8081 (host) / https://emulator:8081 (from dev container)" -ForegroundColor DarkGray + Write-Host " Dev container: cosmosdb-dev-linux (shares emulator network — localhost:8081)" -ForegroundColor DarkGray + Write-Host " Emulator: https://localhost:8081" -ForegroundColor DarkGray } else { Ensure-DevContainer Write-Host "`nDev container running: cosmosdb-dev-linux" -ForegroundColor Green } + exit 0 } function Invoke-Stop { Write-Host "Stopping dev environment..." -ForegroundColor Cyan - docker compose -f $ComposeFile down + # Tear down all profiles + docker compose -f $ComposeFile --profile with-emulator --profile standalone --profile emulator-only down Write-Host "Done." -ForegroundColor Green } @@ -211,6 +227,9 @@ function Invoke-Status { $emulatorRunning = Test-ContainerRunning 'cosmosdb-emulator-linux' $emulatorHealthy = if ($emulatorRunning) { Test-EmulatorHealthy } else { $false } + # Reset LASTEXITCODE — docker inspect failures above are expected when containers don't exist + $global:LASTEXITCODE = 0 + Write-Host "Cross-Platform Dev Environment Status" -ForegroundColor Cyan Write-Host "─────────────────────────────────────" -ForegroundColor DarkGray $devStatus = if ($devRunning) { "Running" } else { "Stopped" } @@ -327,7 +346,7 @@ function Invoke-Test { 'linux|inmemory' { Write-Host "Scenario 4: Linux container + in-memory" -ForegroundColor Cyan Test-DockerAvailable | Out-Null - Ensure-DevContainer + Ensure-DevContainer # standalone profile — no emulator dependency Write-Host "" $envVars = @{ @@ -342,15 +361,18 @@ function Invoke-Test { 'linux|emulator-linux' { Write-Host "Scenario 5: Linux container + Linux emulator" -ForegroundColor Cyan Test-DockerAvailable | Out-Null - Ensure-DevContainer Ensure-Emulator + Ensure-DevContainer -WithEmulatorNetwork # shares emulator's network namespace Write-Host "" + # The dev container shares the emulator's network namespace (network_mode: service:emulator), + # so the emulator is reachable at localhost:8081 — same as CI. + $emulatorEndpoint = 'https://localhost:8081' $envVars = @{ COSMOS_TEST_TARGET = 'emulator-linux' - COSMOS_EMULATOR_ENDPOINT = 'https://emulator:8081' + COSMOS_EMULATOR_ENDPOINT = $emulatorEndpoint } - $cmd = "pwsh -NoProfile -Command `"& $runTestsScript -Target emulator-linux -Project $Project -Framework $Framework -EmulatorEndpoint 'https://emulator:8081'$(if ($Filter) { " -Filter '$Filter'" })`"" + $cmd = "pwsh -NoProfile -Command `"& $runTestsScript -Target emulator-linux -Project $Project -Framework $Framework -EmulatorEndpoint '$emulatorEndpoint'$(if ($Filter) { " -Filter '$Filter'" })`"" $exitCode = Invoke-InContainer -Command $cmd -EnvVars $envVars exit $exitCode }