diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index 9960470e7..0bcd5f5a7 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -10,6 +10,7 @@ on: - tests/Fixtures/ServerParityBaselines/** - tests/Fixtures/ServerParityNormalization/** - tests/Unit/ServerParityRunnerTest.mjs + - tests/Unit/ServerParitySdkDirectionLanesTest.mjs - tests/Unit/ServerParityArtifactSummaryTest.mjs - tests/Unit/ServerParityQueryObserverTest.mjs - tests/Unit/ServerParityCodecDeadlineLoadTest.mjs @@ -66,6 +67,7 @@ on: - tests/Fixtures/ServerParityBaselines/** - tests/Fixtures/ServerParityNormalization/** - tests/Unit/ServerParityRunnerTest.mjs + - tests/Unit/ServerParitySdkDirectionLanesTest.mjs - tests/Unit/ServerParityArtifactSummaryTest.mjs - tests/Unit/ServerParityQueryObserverTest.mjs - tests/Unit/ServerParityCodecDeadlineLoadTest.mjs @@ -195,23 +197,6 @@ jobs: path: corrected-marker-adapters retention-days: 1 if-no-files-found: error - - name: Build one bounded optimized native artifact for deadline health - run: bash scripts/conformance/server-parity/codec-operations-build.sh codec-native-build - - name: Share the optimized binary without compiler caches - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: codec-native-build - path: | - codec-native-build/server - codec-native-build/build.json - codec-native-build/SHA256SUMS - retention-days: 1 - if-no-files-found: error - - name: Retire the owned optimized build container - if: always() - run: | - build_container="$RESOURCE_SCOPE-build" - docker rm -f "$build_container" >/dev/null 2>&1 || true - name: Share thin published SDK direction adapters uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -254,8 +239,36 @@ jobs: retention-days: 1 if-no-files-found: ignore + # The optimized native build only the deadline-health cells use. It runs + # beside the SDK adapters, so the database jobs, which need only the + # adapters, no longer wait for it. + codec-native-build: + runs-on: ubuntu-latest + timeout-minutes: 25 + env: + RESOURCE_SCOPE: dw-codec-native-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }} + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - name: Build one bounded optimized native artifact for deadline health + run: bash scripts/conformance/server-parity/codec-operations-build.sh codec-native-build + - name: Share the optimized binary without compiler caches + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: codec-native-build + path: | + codec-native-build/server + codec-native-build/build.json + codec-native-build/SHA256SUMS + retention-days: 1 + if-no-files-found: error + - name: Retire the owned optimized build container + if: always() + run: | + build_container="$RESOURCE_SCOPE-build" + docker rm -f "$build_container" >/dev/null 2>&1 || true + codec-deadline-health: - needs: published-sdk-adapters + needs: [published-sdk-adapters, codec-native-build] runs-on: ubuntu-latest timeout-minutes: 10 strategy: @@ -617,47 +630,8 @@ jobs: bash scripts/conformance/server-parity/legacy-marker-profile.sh mysql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh mysql http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh mysql http://parity-native-a:8080 pending-native - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 child - docker stop --time 30 "$RESOURCE_SCOPE-sdk-child-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 timer - docker stop --time 30 "$RESOURCE_SCOPE-sdk-timers-server" - # Separate queues/databases/evidence; preserve statuses even after children exit. - ( - signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 signal > parity-evidence/sdk-signals-profile.log 2>&1 || signal_status=$? - printf '%s\n' "$signal_status" > parity-evidence/sdk-signals-profile.status - ) & - ( - query_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 query > parity-evidence/sdk-queries-profile.log 2>&1 || query_status=$? - printf '%s\n' "$query_status" > parity-evidence/sdk-queries-profile.status - ) & - update_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 update || update_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-updates-server" || true - failure_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 update-failure || failure_status=$? - wait - signal_status=$(cat parity-evidence/sdk-signals-profile.status) - query_status=$(cat parity-evidence/sdk-queries-profile.status) - cat parity-evidence/sdk-signals-profile.log parity-evidence/sdk-queries-profile.log - for completed_profile in sdk-signals sdk-queries sdk-update-failures; do - docker stop --time 30 "$RESOURCE_SCOPE-$completed_profile-server" || true - done - namespace_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 namespace-signal || namespace_signal_status=$? - test "$signal_status" -eq 0 - test "$query_status" -eq 0 - test "$update_status" -eq 0 - test "$failure_status" -eq 0 - test "$namespace_signal_status" -eq 0 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-namespace-signals-server" || true - selected_run_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh mysql http://parity-native-a:8080 selected-run-signal || selected_run_signal_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-selected-run-signals-server" || true - test "$selected_run_signal_status" -eq 0 + # Every SDK direction profile, in three concurrent lanes. + bash scripts/conformance/server-parity/sdk-direction-lanes.sh mysql http://parity-native-a:8080 docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint sh \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token "$image" -ec ' @@ -1130,47 +1104,8 @@ jobs: bash scripts/conformance/server-parity/legacy-marker-profile.sh pgsql http://parity-php:8080 http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh pgsql http://parity-native-a:8080 bash scripts/conformance/server-parity/range-repair-profile.sh pgsql http://parity-native-a:8080 pending-native - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 child - docker stop --time 30 "$RESOURCE_SCOPE-sdk-child-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 timer - docker stop --time 30 "$RESOURCE_SCOPE-sdk-timers-server" - # Separate queues/databases/evidence; preserve statuses even after children exit. - ( - signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 signal > parity-evidence/sdk-signals-profile.log 2>&1 || signal_status=$? - printf '%s\n' "$signal_status" > parity-evidence/sdk-signals-profile.status - ) & - ( - query_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 query > parity-evidence/sdk-queries-profile.log 2>&1 || query_status=$? - printf '%s\n' "$query_status" > parity-evidence/sdk-queries-profile.status - ) & - update_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 update || update_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-updates-server" || true - failure_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 update-failure || failure_status=$? - wait - signal_status=$(cat parity-evidence/sdk-signals-profile.status) - query_status=$(cat parity-evidence/sdk-queries-profile.status) - cat parity-evidence/sdk-signals-profile.log parity-evidence/sdk-queries-profile.log - for completed_profile in sdk-signals sdk-queries sdk-update-failures; do - docker stop --time 30 "$RESOURCE_SCOPE-$completed_profile-server" || true - done - namespace_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 namespace-signal || namespace_signal_status=$? - test "$signal_status" -eq 0 - test "$query_status" -eq 0 - test "$update_status" -eq 0 - test "$failure_status" -eq 0 - test "$namespace_signal_status" -eq 0 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-namespace-signals-server" || true - selected_run_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh pgsql http://parity-native-a:8080 selected-run-signal || selected_run_signal_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-selected-run-signals-server" || true - test "$selected_run_signal_status" -eq 0 + # Every SDK direction profile, in three concurrent lanes. + bash scripts/conformance/server-parity/sdk-direction-lanes.sh pgsql http://parity-native-a:8080 docker run --rm --user=1000:1000 --network "$RESOURCE_SCOPE" --entrypoint sh \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token "$image" -ec ' @@ -1529,7 +1464,7 @@ jobs: -e COMPOSER_HOME=/tmp/composer -v "$PWD:/source" -w /source "$image" \ install --working-dir=scripts/conformance/server-parity --no-interaction --no-progress docker run --rm --user=1000:1000 --entrypoint node -v "$PWD:/source:ro" -w /source \ - "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs > parity-evidence/comparator-tests.txt + "$image" --test tests/Unit/ServerParityRunnerTest.mjs tests/Unit/ServerParitySdkDirectionLanesTest.mjs tests/Unit/ServerParityCooperativeContractTest.mjs tests/Unit/ServerParityChildCancellationContractTest.mjs tests/Unit/ServerParityScheduleContractTest.mjs tests/Unit/ServerParityVisibilityContractTest.mjs tests/Unit/ServerParityAdmissionContractTest.mjs tests/Unit/ServerParityRoleTokenContractTest.mjs tests/Unit/ServerParityWorkerDeregistrationContractTest.mjs tests/Unit/ServerParitySearchAttributeContractTest.mjs tests/Unit/ServerParityWorkerListingContractTest.mjs tests/Unit/ServerParityWorkerReplacementContractTest.mjs tests/Unit/ServerParityPatchDeploymentContractTest.mjs tests/Unit/ServerParityWaitingHistoryContractTest.mjs tests/Unit/ServerParityTaskRetryContractTest.mjs tests/Unit/ServerParityPublishedPatchContractTest.mjs tests/Unit/ServerParityPublishedPatchTransportTest.mjs tests/Unit/ServerParityRecordFileTest.mjs tests/Unit/ServerParityLateWorkflowCompletionContractTest.mjs tests/Unit/ServerParityStorageTestContractTest.mjs tests/Unit/ServerParitySchedulePoliciesContractTest.mjs tests/Unit/ServerParitySignalsBeforeWorkerContractTest.mjs tests/Unit/ServerParityScheduleCatalogContractTest.mjs tests/Unit/ServerParityOperatorMetricsContractTest.mjs tests/Unit/ServerParityActivityLeaseRenewalContractTest.mjs tests/Unit/ServerParityServiceCatalogContractTest.mjs tests/Unit/ServerParitySystemMetricsContractTest.mjs tests/Unit/ServerParityOperatorDashboardTypesContractTest.mjs tests/Unit/ServerParityPrometheusMetricsContractTest.mjs tests/Unit/ServerParityPayloadCleanupContractTest.mjs tests/Unit/ServerParityRepairPassContractTest.mjs tests/Unit/ServerParityClusterInfoContractTest.mjs tests/Unit/ServerParityNamespaceDeleteContractTest.mjs > parity-evidence/comparator-tests.txt docker run --rm --user=1000:1000 --cpus=1 --memory=256m --memory-swap=256m \ --network=none --entrypoint node -v "$PWD:/source:ro" -w /source \ "$image" --test tests/Unit/ServerParityCodecDeadlineLoadTest.mjs > parity-evidence/codec-load-tests.txt @@ -1635,47 +1570,8 @@ jobs: bash scripts/conformance/server-parity/legacy-marker-profile.sh sqlite http://parity-server:8080 http://parity-rust:8080 bash scripts/conformance/server-parity/range-repair-profile.sh sqlite http://parity-rust:8080 bash scripts/conformance/server-parity/range-repair-profile.sh sqlite http://parity-rust:8080 pending-native - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 child - docker stop --time 30 "$RESOURCE_SCOPE-sdk-child-directions-server" - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 timer - docker stop --time 30 "$RESOURCE_SCOPE-sdk-timers-server" - # Separate queues/databases/evidence; preserve statuses even after children exit. - ( - signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 signal > parity-evidence/sdk-signals-profile.log 2>&1 || signal_status=$? - printf '%s\n' "$signal_status" > parity-evidence/sdk-signals-profile.status - ) & - ( - query_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 query > parity-evidence/sdk-queries-profile.log 2>&1 || query_status=$? - printf '%s\n' "$query_status" > parity-evidence/sdk-queries-profile.status - ) & - update_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 update || update_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-updates-server" || true - failure_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 update-failure || failure_status=$? - wait - signal_status=$(cat parity-evidence/sdk-signals-profile.status) - query_status=$(cat parity-evidence/sdk-queries-profile.status) - cat parity-evidence/sdk-signals-profile.log parity-evidence/sdk-queries-profile.log - for completed_profile in sdk-signals sdk-queries sdk-update-failures; do - docker stop --time 30 "$RESOURCE_SCOPE-$completed_profile-server" || true - done - namespace_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 namespace-signal || namespace_signal_status=$? - test "$signal_status" -eq 0 - test "$query_status" -eq 0 - test "$update_status" -eq 0 - test "$failure_status" -eq 0 - test "$namespace_signal_status" -eq 0 - docker stop --time 30 "$RESOURCE_SCOPE-sdk-namespace-signals-server" || true - selected_run_signal_status=0 - bash scripts/conformance/server-parity/sdk-direction-profile.sh sqlite http://parity-rust:8080 selected-run-signal || selected_run_signal_status=$? - docker stop --time 30 "$RESOURCE_SCOPE-sdk-selected-run-signals-server" || true - test "$selected_run_signal_status" -eq 0 + # Every SDK direction profile, in three concurrent lanes. + bash scripts/conformance/server-parity/sdk-direction-lanes.sh sqlite http://parity-rust:8080 docker run --rm --user=1000:1000 --entrypoint php --network "$RESOURCE_SCOPE" \ -v "$PWD:/source:ro" -v "$PWD/parity-evidence:/evidence" -w /source \ -e DW_PARITY_TOKEN=parity-test-token "$image" scripts/conformance/server-parity/restart.php \ diff --git a/rust/tests/execution/activity_deadlines.rs b/rust/tests/execution/activity_deadlines.rs index ea2d4d3a9..c67b5f724 100644 --- a/rust/tests/execution/activity_deadlines.rs +++ b/rust/tests/execution/activity_deadlines.rs @@ -483,9 +483,14 @@ async fn heartbeat_renews_only_progress_deadline_and_cannot_resurrect_elapsed_au assert_eq!(after.0, initial.0, "Start-to-close deadline remains fixed"); assert!(after.1 > initial.1, "Heartbeat deadline advances"); assert_eq!(after.2, initial.2, "Lifecycle deadline remains fixed"); + // Compare instants, not text: a lease on a whole second is written + // without a fraction ("...:26Z"), which sorts after "...:26.045Z". + let instant = |value: &Value| chrono::DateTime::parse_from_rfc3339(value.as_str().unwrap()).unwrap(); assert!( - renewed.1["lease_expires_at"].as_str().unwrap() - > activity["lease_expires_at"].as_str().unwrap() + instant(&renewed.1["lease_expires_at"]) > instant(&activity["lease_expires_at"]), + "{} renews {}", + renewed.1["lease_expires_at"], + activity["lease_expires_at"] ); let prefix = run_history(&app, &started["workflow_id"], &started["run_id"]).await; database diff --git a/scripts/conformance/server-parity/sdk-direction-lanes.sh b/scripts/conformance/server-parity/sdk-direction-lanes.sh new file mode 100755 index 000000000..713a18301 --- /dev/null +++ b/scripts/conformance/server-parity/sdk-direction-lanes.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Run every published-SDK direction profile against one native server, in +# three concurrent lanes. Each profile already has its own containers, PHP and +# embedded databases, evidence and state directory, and its own workflow-ID +# prefix on the shared native server, so profiles in different lanes cannot +# see each other unless they share a task queue. Within a lane they run in order, and each profile's PHP +# server is stopped as soon as its attempt ends, so at most three run at once. +# Every profile runs even after another fails; the script fails if any did, +# after printing each log. The job's sdk-direction-cleanup steps remove every +# profile's containers afterwards, as before. +set -euo pipefail +backend="${1:?Choose mysql, pgsql or sqlite}" +native_url="${2:?Set the independently isolated native URL}" +if [[ -z "${RESOURCE_SCOPE:-}" ]]; then + echo 'Set the owning isolated network/resource scope' >&2 + exit 2 +fi +evidence=parity-evidence/sdk-direction-lanes +mkdir -p "$evidence" +# Profile variant and its tag (evidence, log and container names). Profiles +# that share a task queue on the native server must share a lane: the three +# signal profiles all use server-parity-sdk-signals, and one's workers would +# show in another's routing diagnostics. Every other profile has its own queues. +lanes=( + "update:sdk-updates update-failure:sdk-update-failures child:sdk-child-directions" + "signal:sdk-signals namespace-signal:sdk-namespace-signals selected-run-signal:sdk-selected-run-signals" + "timer:sdk-timers activity:sdk-directions query:sdk-queries" +) +run_lane() { + local lane=$1 entry variant tag status + for entry in $lane; do + variant=${entry%%:*} tag=${entry#*:} status=0 + bash scripts/conformance/server-parity/sdk-direction-profile.sh "$backend" "$native_url" "$variant" \ + > "$evidence/$tag.log" 2>&1 || status=$? + printf '%s\n' "$status" > "$evidence/$tag.status" + # The profile's status is already kept; a failed stop leaves the server to + # the final cleanup. + docker stop --time 30 "$RESOURCE_SCOPE-$tag-server" >/dev/null 2>&1 \ + || echo "::warning::The $tag PHP server was not stopped; the final cleanup removes it" >&2 + done +} +for lane in "${lanes[@]}"; do + run_lane "$lane" & +done +wait +failed=0 +for lane in "${lanes[@]}"; do + for entry in $lane; do + tag=${entry#*:} + status=$(cat "$evidence/$tag.status" 2>/dev/null || echo missing) + echo "::group::$tag (exit $status)" + cat "$evidence/$tag.log" + echo "::endgroup::" + if [[ "$status" != 0 ]]; then + echo "::error::SDK direction profile $tag exited $status" >&2 + failed=1 + fi + done +done +exit "$failed" diff --git a/scripts/conformance/server-parity/sdk-directions/README.md b/scripts/conformance/server-parity/sdk-directions/README.md index f5fb86a88..5fea6d9ea 100644 --- a/scripts/conformance/server-parity/sdk-directions/README.md +++ b/scripts/conformance/server-parity/sdk-directions/README.md @@ -34,6 +34,8 @@ bash scripts/conformance/server-parity/sdk-direction-profile.sh BACKEND NATIVE_U bash scripts/conformance/server-parity/sdk-direction-cleanup.sh BACKEND ``` +CI runs every variant through `sdk-direction-lanes.sh BACKEND NATIVE_URL`, in three concurrent lanes against one native server; each variant keeps its own databases, containers and evidence. + Set `RESOURCE_SCOPE` to the owning network and supply `MYSQL_IMAGE` or `POSTGRES_IMAGE` for that backend, as the shared workflow does. The profile starts the exact PHP reference and embedded engine in independent databases; diff --git a/tests/Unit/ServerParitySdkDirectionLanesTest.mjs b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs new file mode 100644 index 000000000..76d034787 --- /dev/null +++ b/tests/Unit/ServerParitySdkDirectionLanesTest.mjs @@ -0,0 +1,159 @@ +import assert from 'node:assert/strict'; +import {spawnSync} from 'node:child_process'; +import {chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync} from 'node:fs'; +import {tmpdir} from 'node:os'; +import {join} from 'node:path'; +import test from 'node:test'; + +const LANES = new URL('../../scripts/conformance/server-parity/sdk-direction-lanes.sh', import.meta.url).pathname; +const PROFILES = { + update: 'sdk-updates', 'update-failure': 'sdk-update-failures', child: 'sdk-child-directions', + 'namespace-signal': 'sdk-namespace-signals', timer: 'sdk-timers', activity: 'sdk-directions', + 'selected-run-signal': 'sdk-selected-run-signals', signal: 'sdk-signals', query: 'sdk-queries', +}; + +// Runs the wrapper in a scratch tree whose profile script and `docker` are +// stubs that append to one event log: a profile starts its PHP server and +// exits FAIL_ (default 0); `docker stop` exits STOP_FAIL_. +function runLanes(environment = {}) { + const root = mkdtempSync(join(tmpdir(), 'dw-sdk-lanes-')); + try { + const scripts = join(root, 'scripts/conformance/server-parity'); + const bin = join(root, 'bin'); + mkdirSync(scripts, {recursive: true}); + mkdirSync(bin); + copyFileSync(LANES, join(scripts, 'sdk-direction-lanes.sh')); + const events = join(root, 'events.log'); + writeFileSync(join(scripts, 'sdk-direction-profile.sh'), `#!/usr/bin/env bash +set -euo pipefail +variant=$3 +echo "start $variant $1 $2" >> "$EVENTS" +echo "profile output for $variant" +sleep 0.2 +echo "end $variant" >> "$EVENTS" +fail="FAIL_\${variant//-/_}" +exit "\${!fail:-0}" +`); + writeFileSync(join(bin, 'docker'), `#!/usr/bin/env bash +echo "docker $*" >> "$EVENTS" +name=\${*: -1} +tag=\${name#"$RESOURCE_SCOPE"-} +tag=\${tag%-server} +fail="STOP_FAIL_\${tag//-/_}" +exit "\${!fail:-0}" +`); + chmodSync(join(bin, 'docker'), 0o755); + const child = spawnSync('bash', ['scripts/conformance/server-parity/sdk-direction-lanes.sh', 'sqlite', 'http://native:8080'], { + cwd: root, encoding: 'utf8', timeout: 30000, + env: {...process.env, ...environment, PATH: `${bin}:${process.env.PATH}`, EVENTS: events, RESOURCE_SCOPE: 'dw-scope-1'}, + }); + const statuses = Object.fromEntries(Object.values(PROFILES).map(tag => [tag, + readFileSync(join(root, 'parity-evidence/sdk-direction-lanes', `${tag}.status`), 'utf8').trim()])); + return {child, statuses, events: readFileSync(events, 'utf8').trim().split('\n')}; + } finally { + rmSync(root, {recursive: true, force: true}); + } +} + +const started = (events, variant) => events.findIndex(event => event.startsWith(`start ${variant} `)); + +// Each profile ran once against the given backend and URL, and its PHP server +// was stopped after the profile ended and before the lane's next profile. +function assertEveryServerRetired(events) { + const running = new Set(); + let peak = 0; + for (const event of events) { + const [kind, ...rest] = event.split(' '); + if (kind === 'start') { + assert.deepEqual(rest.slice(1), ['sqlite', 'http://native:8080'], event); + running.add(PROFILES[rest[0]]); + peak = Math.max(peak, running.size); + } else if (kind === 'docker') { + assert.deepEqual(rest.slice(0, 3), ['stop', '--time', '30'], event); + const tag = rest[3].replace(/^dw-scope-1-/, '').replace(/-server$/, ''); + assert.ok(running.delete(tag), `${tag} stopped once, after it started`); + const end = events.indexOf(`end ${Object.keys(PROFILES).find(variant => PROFILES[variant] === tag)}`); + assert.ok(end >= 0 && end < events.indexOf(event), `${tag} stopped after its profile ended`); + } + } + assert.equal(running.size, 0, `servers left running: ${[...running]}`); + assert.ok(peak <= 3, `at most three PHP servers run at once, not ${peak}`); + const started = events.filter(event => event.startsWith('start ')).map(event => event.split(' ')[1]).sort(); + assert.deepEqual(started, Object.keys(PROFILES).sort(), 'all nine profiles ran once'); + assert.deepEqual(events.filter(event => event.startsWith('docker ')).map(event => event.split(' ').at(-1)).sort(), + Object.values(PROFILES).map(tag => `dw-scope-1-${tag}-server`).sort(), 'every profile server stopped once'); +} + +test('all nine profiles pass and each PHP server is stopped after its profile', () => { + const {child, statuses, events} = runLanes(); + assert.equal(child.status, 0, child.stderr); + assert.deepEqual(new Set(Object.values(statuses)), new Set(['0'])); + assertEveryServerRetired(events); + for (const [variant, tag] of Object.entries(PROFILES)) { + assert.match(child.stdout, new RegExp(`::group::${tag} \\(exit 0\\)\\nprofile output for ${variant}\\n::endgroup::`)); + } + assert.doesNotMatch(child.stderr, /::error::|::warning::/); +}); + +test('a failed profile still runs its lane and fails the wrapper only after every status and log', () => { + // The timer profile is second in its lane; the activity profile follows it. + const {child, statuses, events} = runLanes({FAIL_timer: '7'}); + assert.equal(child.status, 1); + assert.deepEqual(statuses, {...Object.fromEntries(Object.values(PROFILES).map(tag => [tag, '0'])), 'sdk-timers': '7'}); + assertEveryServerRetired(events); + assert.ok(started(events, 'activity') > events.indexOf('end timer'), 'the lane continues after a failure'); + assert.match(child.stderr, /^::error::SDK direction profile sdk-timers exited 7$/m); + assert.equal(child.stderr.match(/::error::/g).length, 1); + assert.match(child.stdout, /::group::sdk-timers \(exit 7\)\nprofile output for timer\n::endgroup::/); + assert.match(child.stdout, /::group::sdk-queries \(exit 0\)/, 'logs after the failure are still printed'); +}); + +test('a failed stop warns without changing any profile status', () => { + const {child, statuses, events} = runLanes({STOP_FAIL_sdk_updates: '1'}); + assert.equal(child.status, 0, child.stderr); + assert.equal(statuses['sdk-updates'], '0'); + assert.match(child.stderr, /^::warning::The sdk-updates PHP server was not stopped; the final cleanup removes it$/m); + assert.ok(started(events, 'update-failure') > events.indexOf('docker stop --time 30 dw-scope-1-sdk-updates-server'), + 'the lane continues after a failed stop'); +}); + +test('a failed profile and a failed stop of its server keep the profile status', () => { + const {child, statuses} = runLanes({FAIL_query: '3', STOP_FAIL_sdk_queries: '1'}); + assert.equal(child.status, 1); + assert.equal(statuses['sdk-queries'], '3'); + assert.match(child.stderr, /^::error::SDK direction profile sdk-queries exited 3$/m); +}); + +test('the wrapper refuses to run without an owning resource scope', () => { + const child = spawnSync('bash', [LANES, 'sqlite', 'http://native:8080'], { + encoding: 'utf8', env: {...process.env, RESOURCE_SCOPE: ''}, cwd: tmpdir(), + }); + assert.equal(child.status, 2); + assert.match(child.stderr, /Set the owning isolated network\/resource scope/); +}); + +// The native server is shared by every lane. Two profiles whose fixtures name +// the same task queue would see each other's workers, so they share a lane. +test('profiles that share a task queue run in the same lane', () => { + const script = readFileSync(LANES, 'utf8'); + const lanes = [...script.slice(script.indexOf('lanes=(')).split(')')[0].matchAll(/^ {2}"([^"]+)"$/gm)] + .map(match => match[1].split(' ').map(entry => entry.split(':')[1])); + assert.deepEqual(lanes.flat().sort(), Object.values(PROFILES).sort(), 'every profile in exactly one lane'); + const queues = tag => { + const directory = new URL(`../Fixtures/ServerParityProfiles/published-${tag}/`, import.meta.url); + const text = readdirSync(directory).filter(name => name.endsWith('.json')) + .map(name => readFileSync(new URL(name, directory), 'utf8')).join('\n'); + return new Set(text.match(/server-parity-sdk-[a-z-]+/g) ?? []); + }; + for (const [index, lane] of lanes.entries()) { + for (const other of lanes.slice(index + 1)) { + for (const tag of lane) { + for (const peer of other) { + const shared = [...queues(tag)].filter(queue => queues(peer).has(queue)); + assert.deepEqual(shared, [], `${tag} and ${peer} share ${shared} but run in different lanes`); + } + } + } + } + assert.ok(queues('sdk-signals').has('server-parity-sdk-signals'), 'queues are read from the fixtures'); +});