diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e7a738b9..b09954a4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,8 +65,6 @@ jobs: - name: lint run: npm run lint - run: npm run compile - - name: npm test - run: xvfb-run npm test - name: Build package run: | npx @vscode/vsce package -o ${{ steps.set-version.outputs.name }}.vsix diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..f0cfa636 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,50 @@ +name: Pre-release tests + +# Integration tests against a real IRIS container. Too slow to run on every PR, so they only run on +# release-preparation PRs (source branch "prepare-*") and on demand. +on: + pull_request: + branches: + - master + - prerelease + workflow_dispatch: + +jobs: + integration: + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'prepare-') + timeout-minutes: 30 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24 + - run: npm install + - name: Install Podman Compose + run: | + sudo apt-get update + sudo apt-get install -y podman-compose + - name: Use Podman, not Docker + # The extension picks the first compose command that works, trying Docker before Podman; drop + # the runner's Docker Compose so both the container lifecycle here and the extension use Podman. + run: sudo apt-get remove -y docker-compose-plugin || true + - name: Start IRIS + run: podman compose -f test-fixtures/iris/docker-compose.yml up -d + - name: Wait for IRIS + run: | + for i in $(seq 1 60); do + if curl -sf -o /dev/null -u _SYSTEM:SYS http://localhost:52799/api/atelier/ \ + && curl -sf -o /dev/null http://localhost:52798/api/atelier/; then + echo "IRIS is ready"; exit 0 + fi + sleep 5 + done + echo "IRIS did not become ready"; podman compose -f test-fixtures/iris/docker-compose.yml logs; exit 1 + - name: Run integration tests + run: xvfb-run npm test + - name: IRIS logs + if: failure() + run: podman compose -f test-fixtures/iris/docker-compose.yml logs + - name: Stop IRIS + if: always() + run: podman compose -f test-fixtures/iris/docker-compose.yml down -v diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4c51a316..4225cae5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -60,8 +60,6 @@ jobs: - name: lint run: npm run lint - run: npm run compile - - name: npm test - run: xvfb-run npm test - name: Build pre-release package run: | npx @vscode/vsce package --pre-release -o ${{ steps.set-version.outputs.name }}.vsix diff --git a/.gitignore b/.gitignore index 87ca2e04..ce17ea70 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,11 @@ out/ dist/ *.vsix vscode*.d.ts -test-fixtures .DS_Store + +# Files created by the integration tests +test-fixtures/*/src/* +!test-fixtures/*/src/.gitkeep + +# Generated per-launch workspace files (see src/test/runTest.ts) +test-fixtures/.generated/ diff --git a/package-lock.json b/package-lock.json index e2aa043a..2ec105df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@typescript-eslint/parser": "^8.56.0", "@vscode/debugadapter-testsupport": "^1.68.0", "@vscode/dts": "^0.4.1", - "@vscode/test-electron": "^2.4.1", + "@vscode/test-electron": "^3.1.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", @@ -1045,19 +1045,20 @@ } }, "node_modules/@vscode/test-electron": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", - "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", + "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", "dev": true, + "license": "MIT", "dependencies": { "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "jszip": "^3.10.1", - "ora": "^7.0.1", + "ora": "^8.1.0", "semver": "^7.6.2" }, "engines": { - "node": ">=16" + "node": ">=22" } }, "node_modules/@webassemblyjs/ast": { @@ -1599,26 +1600,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/baseline-browser-mapping": { "version": "2.11.20", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", @@ -1643,31 +1624,6 @@ "url": "https://bevry.me/fund" } }, - "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", - "dev": true, - "dependencies": { - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/brace-expansion": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", @@ -1732,30 +1688,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1885,15 +1817,16 @@ } }, "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1904,6 +1837,7 @@ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -3109,18 +3043,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -3186,6 +3108,19 @@ "node": ">= 0.4" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3448,26 +3383,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3834,6 +3749,7 @@ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -4348,6 +4264,49 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", @@ -4408,13 +4367,17 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/minimatch": { @@ -4706,15 +4669,16 @@ } }, "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4738,33 +4702,35 @@ } }, "node_modules/ora": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", - "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.3.0", - "cli-cursor": "^4.0.0", - "cli-spinners": "^2.9.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", - "is-unicode-supported": "^1.3.0", - "log-symbols": "^5.1.0", - "stdin-discarder": "^0.1.0", - "string-width": "^6.1.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ora/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -4773,10 +4739,11 @@ } }, "node_modules/ora/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -4785,63 +4752,51 @@ } }, "node_modules/ora/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true - }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/ora/node_modules/log-symbols": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", - "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, - "dependencies": { - "chalk": "^5.0.0", - "is-unicode-supported": "^1.1.0" - }, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ora/node_modules/string-width": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", - "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^10.2.1", - "strip-ansi": "^7.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -5244,16 +5199,17 @@ } }, "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5563,10 +5519,17 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/sisteransi": { "version": "1.0.5", @@ -5596,15 +5559,13 @@ } }, "node_modules/stdin-discarder": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", - "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, - "dependencies": { - "bl": "^5.0.0" - }, + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index ea89ee15..852d1701 100644 --- a/package.json +++ b/package.json @@ -1732,7 +1732,7 @@ "@typescript-eslint/parser": "^8.56.0", "@vscode/debugadapter-testsupport": "^1.68.0", "@vscode/dts": "^0.4.1", - "@vscode/test-electron": "^2.4.1", + "@vscode/test-electron": "^3.1.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", diff --git a/src/test/cases.ts b/src/test/cases.ts new file mode 100644 index 00000000..12cdaa5b --- /dev/null +++ b/src/test/cases.ts @@ -0,0 +1,110 @@ +/** + * The connection test matrix, shared by runTest.ts (which generates one workspace file per launch) + * and the test suite (which reads its launch back from the open workspace's file name). See + * test-fixtures/README.md for the design. Pure Node, no vscode import, so runTest can use it too. + */ + +export const SESSION_TIMEOUT_MS = 10000; + +/** The two IRIS containers in test-fixtures/iris/docker-compose.yml */ +export interface Server { + /** intersystems.servers entry key and isfs authority for the -sm cases */ + serverName: string; + /** Published host port */ + port: number; + /** docker-compose service for the clientSide-os-docker case */ + service: string; + username?: string; + password?: string; +} +export const NAMED: Server = { serverName: "named", port: 52799, service: "iris", username: "_SYSTEM", password: "SYS" }; +export const ANONYMOUS: Server = { serverName: "anonymous", port: 52798, service: "iris-anon" }; + +export type Kind = "clientSide-os-host" | "clientSide-os-docker" | "clientSide-sm" | "serverSide-sm"; +const KINDS: Kind[] = ["clientSide-os-host", "clientSide-os-docker", "clientSide-sm", "serverSide-sm"]; + +export interface Launch { + name: string; + kind: Kind; + server: Server; + /** undefined for serverSide-sm, which is always active */ + active?: boolean; +} + +/** Only these cases have a toggleable active state; docker-compose connections are always active */ +export function togglesActive(kind: Kind): boolean { + return kind === "clientSide-os-host" || kind === "clientSide-sm"; +} + +/** + * The 12 launches: clientSide-os-host and clientSide-sm × credentials × active, plus clientSide-os-docker + * and serverSide-sm × credentials (both always active). + */ +export function allLaunches(): Launch[] { + const out: Launch[] = []; + for (const kind of KINDS) { + for (const server of [NAMED, ANONYMOUS]) { + if (togglesActive(kind)) { + for (const active of [true, false]) { + out.push({ name: `${kind}-${server.serverName}-${active ? "active" : "inactive"}`, kind, server, active }); + } + } else { + out.push({ name: `${kind}-${server.serverName}`, kind, server }); + } + } + } + return out; +} + +export function parse(name: string): Launch { + const launch = allLaunches().find((l) => l.name === name); + if (!launch) { throw new Error(`Unknown case '${name}'`); } + return launch; +} + +/** The .code-workspace contents for a launch. Folder paths are relative to test-fixtures/.generated/. */ +export function workspaceFile(l: Launch): object { + const { kind, server, active } = l; + const credentials = server.username ? { username: server.username, password: server.password } : {}; + const activeConn = active ? { active: true } : {}; + const entry = { + [server.serverName]: { + webServer: { scheme: "http", host: "localhost", port: server.port, pathPrefix: "" }, + ...credentials, + }, + }; + switch (kind) { + case "clientSide-os-host": + return { + folders: [{ path: "../client" }], + settings: { + "objectscript.conn": { https: false, host: "localhost", port: server.port, ns: "USER", ...credentials, ...activeConn }, + }, + }; + case "clientSide-os-docker": + // A docker-compose connection resolves its port only when objectscript.conn.active is truthy; + // the extension bails on an inactive connection before it ever runs compose. + return { + folders: [{ path: "../client" }], + settings: { + "objectscript.conn": { + "docker-compose": { file: "../iris/docker-compose.yml", service: server.service }, + ns: "USER", active: true, ...credentials, + }, + }, + }; + case "clientSide-sm": + return { + folders: [{ path: "../client" }], + settings: { + "objectscript.conn": { server: server.serverName, ns: "USER", ...activeConn }, + "intersystems.servers": entry, + }, + }; + case "serverSide-sm": + return { + folders: [{ uri: `isfs://${server.serverName}:USER/` }], + settings: { "intersystems.servers": entry }, + }; + } +} diff --git a/src/test/runTest.ts b/src/test/runTest.ts index 8bd38c6a..62086aae 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -1,7 +1,10 @@ import * as cp from "child_process"; +import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, runTests } from "@vscode/test-electron"; +import { allLaunches, workspaceFile } from "./cases"; async function main() { try { @@ -13,26 +16,60 @@ async function main() { // Passed to --extensionTestsPath const extensionTestsPath = path.resolve(__dirname, "./suite/index"); - // The path to the workspace file - const workspace = path.resolve("test-fixtures", "test.code-workspace"); + // One workspace file per launch, generated so the active-flip check can rewrite it without + // dirtying the repo. Folders point at the committed test-fixtures/client and .../iris. + const generated = path.resolve(extensionDevelopmentPath, "test-fixtures", ".generated"); + fs.rmSync(generated, { recursive: true, force: true }); + fs.mkdirSync(generated, { recursive: true }); + const launches = allLaunches(); + for (const l of launches) { + fs.writeFileSync(path.join(generated, `${l.name}.code-workspace`), JSON.stringify(workspaceFile(l), null, " ")); + } const vscodeExecutablePath = await downloadAndUnzipVSCode("stable"); const [cli, ...args] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); - const installExtension = (extId) => - cp.spawnSync(cli, [...args, "--install-extension", extId], { - encoding: "utf-8", - stdio: "inherit", - }); + // Server Manager resolves the intersystems.servers entries and getServerSpec used by the fixtures + cp.spawnSync(cli, [...args, "--install-extension", "intersystems-community.servermanager"], { + encoding: "utf-8", + stdio: "inherit", + }); - // Install dependent extensions - installExtension("intersystems-community.servermanager"); - installExtension("intersystems.language-server"); + // Inherited from a VS Code extension host (e.g. a terminal spawned by an extension); would make + // the downloaded VS Code run as plain Node and try to execute the workspace file as a script + delete process.env.ELECTRON_RUN_AS_NODE; - const launchArgs = ["-n", workspace, "--enable-proposed-api", "intersystems-community.vscode-objectscript"]; - - // Download VS Code, unzip it and run the integration test - await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs }); + // Optional substring to run a subset of cases locally, e.g. `npm test -- os-host` + const filter = process.argv[2]; + const failed: string[] = []; + for (const l of filter ? launches.filter((l) => l.name.includes(filter)) : launches) { + // A fresh user-data-dir so cached connection state from a previous run can't mask activation bugs + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "vscode-objectscript-test-")); + const workspace = path.join(generated, `${l.name}.code-workspace`); + const launchArgs = [ + workspace, + "--user-data-dir", + userDataDir, + "--disable-workspace-trust", + "--enable-proposed-api", + "intersystems-community.vscode-objectscript", + ]; + console.log(`\n===== ${l.name} =====`); + try { + await runTests({ vscodeExecutablePath, extensionDevelopmentPath, extensionTestsPath, launchArgs }); + } catch (err) { + failed.push(l.name); + // The two extensions' log channels are the best record of what they sent to the servers + for (const log of fs.readdirSync(userDataDir, { recursive: true }) as string[]) { + if (/intersystems-community\.[^/\\]+[/\\][^/\\]+\.log$/.test(log)) { + console.error(`\n===== ${log} =====\n${fs.readFileSync(path.join(userDataDir, log), "utf-8")}`); + } + } + } + } + if (failed.length) { + throw new Error(`Failed cases: ${failed.join(", ")}`); + } } catch (err) { console.error("Failed to run tests", err); process.exit(1); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index e162ec0b..ad931b07 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -1,25 +1,181 @@ +/** + * Integration tests against the IRIS containers in test-fixtures/iris/docker-compose.yml. runTest.ts + * generates one workspace file per launch (see test-fixtures/README.md) and opens them one at a time; + * this suite reads its launch back from the open workspace's file name and runs every check that applies. + * Every check asserts that the extension used the credentials in settings without prompting: a prompt + * would leave the connection unestablished and the test would time out. + */ import * as assert from "assert"; -import { before } from "mocha"; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import { window, extensions } from "vscode"; -import { extensionId, smExtensionId } from "../../extension"; - -suite("Extension Test Suite", () => { - suiteSetup(async function () { - // make sure extension is activated - const serverManager = extensions.getExtension(smExtensionId); - await serverManager?.activate(); - const ext = extensions.getExtension(extensionId); - await ext?.activate(); +import * as path from "path"; +import * as vscode from "vscode"; +import { parse, SESSION_TIMEOUT_MS, togglesActive } from "../cases"; + +const EXTENSION_ID = "intersystems-community.vscode-objectscript"; +const SERVER_MANAGER_ID = "intersystems-community.servermanager"; + +const CASE = path.basename(vscode.workspace.workspaceFile!.fsPath, ".code-workspace"); +const { kind, server, active } = parse(CASE); +const FOLDER = vscode.workspace.workspaceFolders![0]; +const isServerSide = kind === "serverSide-sm"; +const canToggle = togglesActive(kind); +/** docker-compose and serverSide are always active; os-host and sm follow objectscript.conn.active */ +const configuredActive = canToggle ? active === true : true; +/** getServerSpec key: the entry for the -sm cases, the folder name for the -os- cases (the Current node) */ +const specName = kind.endsWith("-sm") ? server.serverName : FOLDER.name; + +let osApi: any; +let smApi: any; +let counter = 0; +const created = new Set(); + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitFor(label: string, probe: () => Promise, timeoutMs = 30000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const result = await probe(); + if (result) return result; + await sleep(1000); + } + throw new Error(`Timed out after ${timeoutMs} ms waiting for ${label}`); +} + +/** Talks to the container directly, bypassing both extensions, to check what actually landed on the server */ +async function restDoc(method: "GET" | "DELETE", name: string): Promise { + const response = await fetch(`http://localhost:${server.port}/api/atelier/v1/USER/doc/${name}`, { + method, + headers: server.password + ? { Authorization: "Basic " + Buffer.from(`${server.username}:${server.password}`).toString("base64") } + : {}, }); + if (response.status === 404) return undefined; + assert.ok(response.ok, `${method} ${name} failed with HTTP ${response.status}`); + const { result } = await response.json(); + return Array.isArray(result.content) ? result.content.join("\n") : undefined; +} + +/** Check 1: the extension resolves the folder as configured, without prompting */ +async function checkResolves(expectActive: boolean): Promise { + const deadline = Date.now() + 30000; + let conn = await osApi.asyncServerForUri(FOLDER.uri); + while (conn?.active !== expectActive && Date.now() < deadline) { + await sleep(500); + conn = await osApi.asyncServerForUri(FOLDER.uri); + } + assert.strictEqual(conn.active, expectActive, `expected active=${expectActive}`); + assert.strictEqual(conn.host, "localhost"); + assert.strictEqual(conn.port, server.port); + assert.strictEqual(conn.namespace, "USER"); + assert.strictEqual(conn.username || "", server.username || ""); + // A password stored in plaintext in settings must reach API consumers such as Language Server + assert.strictEqual(conn.password, server.password); +} + +/** + * Check 2: a saved class reaches the server iff the connection is active. With verifyDelete, an active + * connection also propagates the local delete back to the server; the flip check omits that, because a + * folder that was inactive at activation time does not wire up delete-sync until the window reloads. + */ +async function roundTrip(expectActive: boolean, verifyDelete = true): Promise { + const className = `CiTest.${CASE.replace(/[^A-Za-z0-9]/g, "")}${counter++}`; + const doc = `${className}.cls`; + const file = isServerSide + ? vscode.Uri.joinPath(FOLDER.uri, `${className.replace(/\./g, "/")}.cls`) + : // Written straight into the pre-existing src/ folder: creating a directory tree and a file in it at + // once can lose the file's watcher event on Linux, which is not what this is testing + vscode.Uri.joinPath(FOLDER.uri, "src", `${className}.cls`); + const source = `Class ${className}\n{\n\nClassMethod Hello() As %String\n{\n\tQuit "hello"\n}\n\n}\n`; + created.add(doc); + await vscode.workspace.fs.writeFile(file, Buffer.from(source)); + if (expectActive) { + const onServer = await waitFor(`${doc} on the server`, () => restDoc("GET", doc)); + assert.match(onServer, new RegExp(`^Class ${className}`)); + await vscode.workspace.fs.delete(file); + if (verifyDelete) { + await waitFor(`${doc} deleted from the server`, async () => !(await restDoc("GET", doc))); + created.delete(doc); + } + } else { + // Give any erroneous sync time to happen before asserting it did not + await sleep(5000); + assert.strictEqual(await restDoc("GET", doc), undefined, "inactive connection must not reach the server"); + await vscode.workspace.fs.delete(file); + created.delete(doc); + } +} + +/** Rewrite objectscript.conn.active in the generated (gitignored) workspace file */ +async function applyActive(value: boolean): Promise { + const cfg = vscode.workspace.getConfiguration("objectscript"); + await cfg.update("conn", { ...(cfg.get("conn") as object), active: value }, vscode.ConfigurationTarget.Workspace); +} - before(() => { - window.showInformationMessage("Start all tests."); +/** Check 5: Server Manager resolves the spec as configured, without prompting */ +async function checkSpec(): Promise { + const spec = await smApi.getServerSpec(specName); + assert.ok(spec?.auth, `no spec for '${specName}'`); + assert.strictEqual(spec.webServer.scheme, "http"); + assert.strictEqual(spec.webServer.host, "localhost"); + assert.strictEqual(spec.webServer.port, server.port); + assert.strictEqual(spec.webServer.pathPrefix, ""); + assert.strictEqual(spec.username || "", server.username || ""); + assert.strictEqual(spec.password, server.password); + assert.strictEqual(spec.auth.resolved(), server.password !== undefined); +} + +suite(CASE, () => { + suiteSetup(async () => { + // The released Server Manager gets installed alongside; check 5 resolves specs through its API + smApi = await vscode.extensions.getExtension(SERVER_MANAGER_ID)?.activate(); + const extension = vscode.extensions.getExtension(EXTENSION_ID); + assert.ok(extension, `${EXTENSION_ID} is not installed`); + // The build under test must be the one that ends up running, not a Marketplace copy + assert.strictEqual(extension.extensionPath, path.resolve(__dirname, "../../..")); + // Hangs here (and fails on the mocha timeout) if activation blocks on a credential prompt + osApi = await extension.activate(); + }); + + suiteTeardown(async () => { + for (const doc of created) await restDoc("DELETE", doc).catch(() => undefined); + }); + + // Checks 1 and 2 + test("resolves and round-trips as configured", async () => { + await checkResolves(configuredActive); + await roundTrip(configuredActive); }); - test("Sample test", () => { - assert.ok("All good"); + // Check 4 + if (isServerSide) { + test("lists the namespace through the folder", async () => { + const entries = await vscode.workspace.fs.readDirectory(FOLDER.uri); + assert.ok(entries.length > 0, "namespace listing is empty"); + }); + } + + // Check 5 + test("Server Manager resolves the spec", () => checkSpec()); + + // Checks 1 and 2 again, after idling past the session timeout so a cached cookie must be renewed. + // Skips the delete round-trip: this proves the save reconnects, and older releases don't re-wire + // delete-sync after a session lapses (fixed on the dev build, so re-verifying it here would be flaky). + test("still resolves and round-trips after the session times out", async () => { + await sleep(SESSION_TIMEOUT_MS + 3000); + await checkResolves(configuredActive); + await roundTrip(configuredActive, false); }); + + // Check 3, last, so the connection it establishes can't leak a session into the idle check above + if (canToggle) { + test("flipping objectscript.conn.active is honored", async () => { + try { + await applyActive(!configuredActive); + await checkResolves(!configuredActive); + await roundTrip(!configuredActive, false); + } finally { + await applyActive(configuredActive); + await checkResolves(configuredActive); + } + }); + } }); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 5f6a2d87..12ee5b3b 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -2,24 +2,19 @@ import * as path from "path"; import * as Mocha from "mocha"; import { glob } from "glob"; -export function run(): Promise { - // Create the mocha test +export async function run(): Promise { const mocha = new Mocha({ ui: "tdd", color: true, + // Tests wait on a real IRIS container and on the extension's own retry/timeout logic + timeout: 180000, }); const testsRoot = path.resolve(__dirname, ".."); + const files = await glob("**/**.test.js", { cwd: testsRoot }); + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); - return glob("**/**.test.js", { cwd: testsRoot }).then((files) => { - // Add files to the test suite - files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); - - // Run the mocha test - mocha.run((failures) => { - if (failures > 0) { - throw new Error(`${failures} tests failed.`); - } - }); + return new Promise((resolve, reject) => { + mocha.run((failures) => (failures > 0 ? reject(new Error(`${failures} tests failed.`)) : resolve())); }); } diff --git a/test-fixtures/README.md b/test-fixtures/README.md new file mode 100644 index 00000000..be5a384c --- /dev/null +++ b/test-fixtures/README.md @@ -0,0 +1,69 @@ +# Integration test fixtures + +`npm test` generates one `.code-workspace` per case into `.generated/`, opens each in a downloaded +VS Code with the released Server Manager installed alongside, and runs the suite in `src/test/suite` +against two IRIS containers ([iris/docker-compose.yml](iris/docker-compose.yml)): + +| Container | Port | `/api/atelier` authentication | +| ----------- | ----- | ------------------------------- | +| `iris` | 52799 | password only (`_SYSTEM`/`SYS`) | +| `iris-anon` | 52798 | unauthenticated only | + +Both run [iris/setup/setup.sh](iris/setup/setup.sh) after IRIS starts, which sets a 10-second +`/api/atelier` session timeout so expired-session recovery can be tested. + +## Cases + +One `.code-workspace` per case, one folder per workspace. `-named` connects to `iris`, `-anonymous` +to `iris-anon`. + +| Case | `folders[·]` | `objectscript.conn` (folder) | `intersystems.servers` (workspace) | +| ---------------------- | -------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `clientSide-os-host` | `{ path }` | `{ https, host, port, ns, ...credentials, ...active }` | `{}` | +| `clientSide-os-docker` | `{ path }` | `{ docker-compose, ns, active: true, ...credentials }` | `{}` | +| `clientSide-sm` | `{ path }` | `{ server: , ns, ...active }` | `{ : { webServer: { scheme, host, port, pathPrefix }, ...credentials } }` | +| `serverSide-sm` | `{ uri: "isfs://:/" }` | `{}` | `{ : { webServer: { scheme, host, port, pathPrefix }, ...credentials } }` | + +### Variants + +Each case is launched once per applicable combination; the variant names are appended to the case name. + +`...credentials` — where the case's address lives (`objectscript.conn` for `*-os-*`, the entry for `*-sm`): + +- `-named`: `{ username, password }` +- `-anonymous`: `{}`; the server must allow unauthenticated access + +The containers run under Podman (`podman compose -f test-fixtures/iris/docker-compose.yml up`), so `clientSide-os-docker` resolves through Podman too. + +`...active` — `clientSide-os-host` and `clientSide-sm` only. A `docker-compose` connection must set `active: true` (the extension skips resolving an inactive connection), so it has no inactive variant: + +- `-active`: `{ active: true }` +- `-inactive`: `{}` (default `false`) + +2 × 2 × 2 + 2 × 2 = 12 launches. Both repos run all of them, each installing the other extension's Marketplace release. + +### Checks + +Every case performs the following tests once and then repeats them past the session timeout. + +1. resolves — `asyncServerForUri` reports `active`, host, port, ns, username, password as configured +2. round trip, depending on `active` (`serverSide-` and `clientSide-os-docker` are always active): + - active: save class → on server (direct REST) → delete → gone + - inactive: save class → never reaches the server +3. If the case toggles `active` (`clientSide-os-host`, `clientSide-sm`): flip `active` and redo 1–2 +4. If `serverSide-`: `readDirectory` on the folder root is non-empty +5. Server Manager's `getServerSpec` reports `webServer` fields, username, password as configured; `auth.resolved()` iff `-named`: + - If `*-sm`: `getServerSpec()` + - If `*-os-*`: `getServerSpec()`, the Servers view's Current node +6. Server Manager repo only: `makeRESTRequest("GET", spec)` on the result of Step 5 → 200 with `USER` listed, as the Servers view does + +## Running + +In CI this runs from [.github/workflows/prepare-release.yml](../.github/workflows/prepare-release.yml), +on PRs whose source branch starts with `prepare-` and on manual dispatch. To run locally with Podman: + +```sh +podman compose -f test-fixtures/iris/docker-compose.yml up -d --wait +npm test +podman compose -f test-fixtures/iris/docker-compose.yml down -v +``` diff --git a/test-fixtures/client/src/.gitkeep b/test-fixtures/client/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test-fixtures/iris/docker-compose.yml b/test-fixtures/iris/docker-compose.yml new file mode 100644 index 00000000..3650e719 --- /dev/null +++ b/test-fixtures/iris/docker-compose.yml @@ -0,0 +1,23 @@ +x-iris: &iris + image: containers.intersystems.com/intersystems/iris-community:latest-cd + volumes: + - ./setup:/setup:ro + healthcheck: + test: ["CMD", "test", "-f", "/tmp/ci-setup-done"] + interval: 5s + timeout: 3s + retries: 60 + start_period: 20s + +services: + iris: + <<: *iris + command: ["-a", "sh /setup/setup.sh"] + ports: + - "52799:52773" + # Same image, but /api/atelier accepts only unauthenticated access + iris-anon: + <<: *iris + command: ["-a", "sh /setup/setup.sh anon"] + ports: + - "52798:52773" diff --git a/test-fixtures/iris/setup/setup.sh b/test-fixtures/iris/setup/setup.sh new file mode 100644 index 00000000..20056432 --- /dev/null +++ b/test-fixtures/iris/setup/setup.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Runs inside the container after IRIS starts (see docker-compose.yml). +# Configures the instance for the integration tests, then drops a marker file the healthcheck waits for. +# With "anon" as the argument, /api/atelier accepts only unauthenticated access; otherwise only passwords. +set -e +AUTHE=32 +[ "$1" = anon ] && AUTHE=64 + +iris session IRIS -U %SYS <