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..ee327677 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,32 @@ +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: Start IRIS + run: docker compose -f test-fixtures/iris/docker-compose.yml up -d --wait --wait-timeout 300 + - name: Run integration tests + run: xvfb-run npm test + - name: IRIS logs + if: failure() + run: docker compose -f test-fixtures/iris/docker-compose.yml logs + - name: Stop IRIS + if: always() + run: docker 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..6727b6a0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,8 @@ out/ dist/ *.vsix vscode*.d.ts -test-fixtures .DS_Store + +# Files created by the integration tests +test-fixtures/*/src/* +!test-fixtures/*/src/.gitkeep diff --git a/package-lock.json b/package-lock.json index e7230739..e57e65a6 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/runTest.ts b/src/test/runTest.ts index 8bd38c6a..ad90df26 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -1,4 +1,6 @@ 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"; @@ -13,26 +15,44 @@ 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"); + // The multi-root workspace whose folders connect to the IRIS container started from test-fixtures/iris + const workspace = path.resolve(extensionDevelopmentPath, "test-fixtures", "ci.code-workspace"); 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", - }); - - // Install dependent extensions - installExtension("intersystems-community.servermanager"); - installExtension("intersystems.language-server"); - - 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 }); + // Server Manager resolves the intersystems.servers entries used by the fixture workspace + cp.spawnSync(cli, [...args, "--install-extension", "intersystems-community.servermanager"], { + encoding: "utf-8", + stdio: "inherit", + }); + + // 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 launchArgs = [ + workspace, + "--user-data-dir", + userDataDir, + "--disable-workspace-trust", + "--enable-proposed-api", + "intersystems-community.vscode-objectscript", + ]; + + // 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; + try { + await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs }); + } catch (err) { + // The extension's own output channel is the best record of what it sent to the server + for (const log of fs.readdirSync(userDataDir, { recursive: true }) as string[]) { + if (log.endsWith("ObjectScript.log")) { + console.error(`\n===== ${log} =====\n${fs.readFileSync(path.join(userDataDir, log), "utf-8")}`); + } + } + throw err; + } } catch (err) { console.error("Failed to run tests", err); process.exit(1); diff --git a/src/test/suite/connection.test.ts b/src/test/suite/connection.test.ts new file mode 100644 index 00000000..e9971b52 --- /dev/null +++ b/src/test/suite/connection.test.ts @@ -0,0 +1,176 @@ +/** + * Integration tests against the IRIS container defined in test-fixtures/iris/docker-compose.yml, + * opened through the multi-root workspace test-fixtures/ci.code-workspace. Each folder connects + * to that same container by a different mechanism, and every test asserts that the extension + * used the credentials stored in settings without prompting: a prompt would leave the connection + * unestablished and the test would time out. + */ +import * as assert from "assert"; +import * as vscode from "vscode"; + +const EXTENSION_ID = "intersystems-community.vscode-objectscript"; +const SERVER_MANAGER_ID = "intersystems-community.servermanager"; + +/** Must match test-fixtures/iris/docker-compose.yml and the credentials in test-fixtures/ci.code-workspace */ +const IRIS = { host: "localhost", port: 52799, ns: "USER", username: "_SYSTEM", password: "SYS" }; +/** The /api/atelier session timeout configured by test-fixtures/iris/setup/setup.sh */ +const SESSION_TIMEOUT_MS = 10000; +/** `AtelierAPI` reports this version until the extension has successfully fetched real server info */ +const PLACEHOLDER_SERVER_VERSION = "2016.2.0"; + +/** Workspace folders that should be connected, keyed by the mechanism they use to find the server */ +const CONNECTED_FOLDERS = { + "client-conn": "host/port in objectscript.conn", + iris: "docker-compose port resolution", + "client-named": "intersystems.servers entry", +}; + +let api: any; + +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}`); +} + +function folderUri(name: string): vscode.Uri { + const folder = vscode.workspace.workspaceFolders?.find((f) => f.name === name); + assert.ok(folder, `workspace folder '${name}' is missing`); + return folder.uri; +} + +function classSource(name: string): Uint8Array { + return Buffer.from(`Class ${name}\n{\n\nClassMethod Hello() As %String\n{\n\tQuit "hello"\n}\n\n}\n`); +} + +/** Talks to the container directly, bypassing the extension, to check what actually landed on the server */ +async function restDoc(method: "GET" | "DELETE", name: string): Promise { + const response = await fetch(`http://${IRIS.host}:${IRIS.port}/api/atelier/v1/${IRIS.ns}/doc/${name}`, { + method, + headers: { Authorization: "Basic " + Buffer.from(`${IRIS.username}:${IRIS.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; +} + +/** A connection is established once the extension has fetched the server's real version */ +async function connectedServer(uri: vscode.Uri) { + const server = await api.asyncServerForUri(uri); + return server?.active && server.serverVersion !== PLACEHOLDER_SERVER_VERSION ? server : undefined; +} + +async function assertSyncsToServer(folder: string, className: string): Promise { + // 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 + const local = vscode.Uri.joinPath(folderUri(folder), "src", `${className}.cls`); + await vscode.workspace.fs.writeFile(local, classSource(className)); + const onServer = await waitFor(`${className} to appear on the server`, () => restDoc("GET", `${className}.cls`)); + assert.match(onServer, new RegExp(`^Class ${className}`)); + await vscode.workspace.fs.delete(local); + await waitFor(`${className} to be deleted from the server`, async () => !(await restDoc("GET", `${className}.cls`))); +} + +suite("Connections to an IRIS container", () => { + suiteSetup(async () => { + await vscode.extensions.getExtension(SERVER_MANAGER_ID)?.activate(); + const extension = vscode.extensions.getExtension(EXTENSION_ID); + assert.ok(extension, `${EXTENSION_ID} is not installed`); + // Hangs here (and fails on the mocha timeout) if activation blocks on the inactive folder whose + // server is unreachable, or on a credential prompt for it + api = await extension.activate(); + }); + + suiteTeardown(async () => { + await restDoc("DELETE", "CiTest.Isfs.cls").catch(() => undefined); + for (const folder of Object.keys(CONNECTED_FOLDERS)) { + const src = vscode.Uri.joinPath(folderUri(folder), "src"); + for (const [name] of await vscode.workspace.fs.readDirectory(src)) { + if (name.endsWith(".cls")) await vscode.workspace.fs.delete(vscode.Uri.joinPath(src, name)); + } + } + }); + + for (const [folder, mechanism] of Object.entries(CONNECTED_FOLDERS)) { + test(`${folder}: connects via ${mechanism} using the credentials in settings`, async () => { + const uri = folderUri(folder); + // Activation checks each *server* once, so a folder sharing its server with another folder + // is only checked once a document in it becomes active, as happens when a user opens one + await vscode.window.showTextDocument(vscode.Uri.joinPath(uri, ".vscode", "settings.json")); + const server = await waitFor(`${folder} to connect`, () => connectedServer(uri), 60000); + assert.strictEqual(server.host, IRIS.host); + assert.strictEqual(server.port, IRIS.port); + assert.strictEqual(server.namespace, IRIS.ns); + assert.strictEqual(server.username, IRIS.username); + // A password stored in plaintext in settings must be passed on to API consumers such as Language Server + assert.strictEqual(server.password, IRIS.password); + }); + } + + test("client-inactive: stays inactive and does not expose a password it was never given", () => { + const server = api.serverForUri(folderUri("client-inactive")); + assert.strictEqual(server.active, false); + assert.strictEqual(server.password, undefined); + }); + + test("server-side: lists the namespace and writes a new class through isfs", async () => { + const root = folderUri("server-side"); + const entries = await vscode.workspace.fs.readDirectory(root); + assert.ok(entries.length > 0, "namespace listing is empty"); + await vscode.workspace.fs.writeFile(vscode.Uri.joinPath(root, "CiTest", "Isfs.cls"), classSource("CiTest.Isfs")); + const onServer = await waitFor("CiTest.Isfs to appear on the server", () => restDoc("GET", "CiTest.Isfs.cls")); + assert.match(onServer, /^Class CiTest\.Isfs/); + }); + + for (const folder of Object.keys(CONNECTED_FOLDERS)) { + test(`${folder}: saving a class under src/ syncs it to the server and deleting it removes it`, async () => { + await assertSyncsToServer(folder, `CiTest.${folder.replace(/-/g, "")}`); + }); + } + + test("an expired session is re-established without prompting", async () => { + // Idle past the server's session timeout so the extension's cached cookies are rejected with a 401 + await sleep(SESSION_TIMEOUT_MS + 3000); + const entries = await vscode.workspace.fs.readDirectory(folderUri("server-side")); + assert.ok(entries.length > 0, "namespace listing is empty after session expiry"); + await assertSyncsToServer("client-conn", "CiTest.AfterExpiry"); + }); + + test("client-conn: turning objectscript.conn.active off and on is honoured", async () => { + const uri = folderUri("client-conn"); + await withConnectionOff(uri, async () => { + // It must stay off rather than being forced back on by the connection check + await sleep(5000); + assert.strictEqual(api.serverForUri(uri).active, false); + // Keep the session cookie fresh: meeting a stale one is the scenario of the next test + await vscode.workspace.fs.readDirectory(folderUri("server-side")); + }); + }); + + // A connection check that meets an expired session should recover by itself, but currently prompts + // for a password and never settles (#1861). Un-skip when #1864 lands. + test.skip("client-conn: a connection check after the session expired recovers without prompting", async () => { + await withConnectionOff(folderUri("client-conn"), () => sleep(SESSION_TIMEOUT_MS + 3000)); + }); +}); + +/** Turn the folder's connection off, run `whileOff`, turn it back on and wait for it to reconnect */ +async function withConnectionOff(uri: vscode.Uri, whileOff: () => Promise): Promise { + const configuration = vscode.workspace.getConfiguration("objectscript", uri); + const conn = configuration.get("conn"); + await configuration.update("conn", { ...conn, active: false }, vscode.ConfigurationTarget.WorkspaceFolder); + try { + await waitFor("the connection to go inactive", async () => api.serverForUri(uri).active === false); + await whileOff(); + } finally { + await configuration.update("conn", conn, vscode.ConfigurationTarget.WorkspaceFolder); + } + await waitFor("the connection to come back", () => connectedServer(uri)); +} diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts deleted file mode 100644 index e162ec0b..00000000 --- a/src/test/suite/extension.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -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(); - }); - - before(() => { - window.showInformationMessage("Start all tests."); - }); - - test("Sample test", () => { - assert.ok("All good"); - }); -}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index 5f6a2d87..9b9f64f3 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: 120000, }); 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..ad32258b --- /dev/null +++ b/test-fixtures/README.md @@ -0,0 +1,26 @@ +# Integration test fixtures + +`npm test` opens [ci.code-workspace](ci.code-workspace) in a downloaded VS Code and runs the suite in +`src/test/suite` against an IRIS container. Every workspace folder connects to that one container by a +different mechanism: + +| Folder | Mechanism | +| ----------------- | ------------------------------------------------------------ | +| `client-conn` | `objectscript.conn` with host/port/credentials | +| `iris` | `objectscript.conn.docker-compose` port resolution | +| `client-named` | `objectscript.conn.server` naming an `intersystems.servers` entry with a plaintext password | +| `client-inactive` | `"active": false`, pointing at an unreachable host | +| `server-side` | `isfs://ci:user/` | + +The container ([iris/docker-compose.yml](iris/docker-compose.yml)) publishes fixed host ports and runs +[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. + +In CI this runs from `.github/workflows/prepare-release.yml`, on PRs whose source branch starts with +`prepare-` and on manual dispatch. To run locally, with either Docker or Podman: + +```sh +podman compose -f test-fixtures/iris/docker-compose.yml up -d --wait # or: docker compose ... +npm test +podman compose -f test-fixtures/iris/docker-compose.yml down -v +``` diff --git a/test-fixtures/ci.code-workspace b/test-fixtures/ci.code-workspace new file mode 100644 index 00000000..49a59055 --- /dev/null +++ b/test-fixtures/ci.code-workspace @@ -0,0 +1,28 @@ +{ + "folders": [ + { "path": "client-conn" }, + { "path": "iris" }, + { "path": "client-named" }, + { "path": "client-inactive" }, + { "uri": "isfs://ci:user/", "name": "server-side" } + ], + "settings": { + "intersystems.servers": { + "ci": { + "webServer": { "scheme": "http", "host": "localhost", "port": 52799 }, + "superServer": { "port": 1999 }, + "username": "_SYSTEM", + "password": "SYS" + }, + "blackhole": { + "webServer": { "scheme": "http", "host": "10.255.255.1", "port": 52773 }, + "username": "_SYSTEM" + } + }, + "objectscript.syncLocalChanges": "all", + "objectscript.outputRESTTraffic": true, + "objectscript.compileOnSave": true, + "objectscript.showProposedApiPrompt": false, + "objectscript.ignoreInstallServerManager": true + } +} diff --git a/test-fixtures/client-conn/.vscode/settings.json b/test-fixtures/client-conn/.vscode/settings.json new file mode 100644 index 00000000..798ed124 --- /dev/null +++ b/test-fixtures/client-conn/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "objectscript.conn": { + "active": true, + "host": "localhost", + "port": 52799, + "ns": "USER", + "username": "_SYSTEM", + "password": "SYS" + } +} diff --git a/test-fixtures/client-conn/src/.gitkeep b/test-fixtures/client-conn/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test-fixtures/client-inactive/.vscode/settings.json b/test-fixtures/client-inactive/.vscode/settings.json new file mode 100644 index 00000000..6197d8f1 --- /dev/null +++ b/test-fixtures/client-inactive/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "objectscript.conn": { + "active": false, + "server": "blackhole", + "ns": "USER" + } +} diff --git a/test-fixtures/client-named/.vscode/settings.json b/test-fixtures/client-named/.vscode/settings.json new file mode 100644 index 00000000..70780afd --- /dev/null +++ b/test-fixtures/client-named/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "objectscript.conn": { + "active": true, + "server": "ci", + "ns": "USER" + } +} diff --git a/test-fixtures/client-named/src/.gitkeep b/test-fixtures/client-named/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test-fixtures/iris/.vscode/settings.json b/test-fixtures/iris/.vscode/settings.json new file mode 100644 index 00000000..6017f677 --- /dev/null +++ b/test-fixtures/iris/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "objectscript.conn": { + "active": true, + "ns": "USER", + "username": "_SYSTEM", + "password": "SYS", + "docker-compose": { + "service": "iris", + "file": "docker-compose.yml" + } + } +} diff --git a/test-fixtures/iris/docker-compose.yml b/test-fixtures/iris/docker-compose.yml new file mode 100644 index 00000000..25d370ce --- /dev/null +++ b/test-fixtures/iris/docker-compose.yml @@ -0,0 +1,15 @@ +services: + iris: + image: containers.intersystems.com/intersystems/iris-community:latest-cd + command: ["-a", "sh /setup/setup.sh"] + ports: + - "52799:52773" + - "1999:1972" + volumes: + - ./setup:/setup:ro + healthcheck: + test: ["CMD", "test", "-f", "/tmp/ci-setup-done"] + interval: 5s + timeout: 3s + retries: 60 + start_period: 20s diff --git a/test-fixtures/iris/setup/setup.sh b/test-fixtures/iris/setup/setup.sh new file mode 100644 index 00000000..0b42813a --- /dev/null +++ b/test-fixtures/iris/setup/setup.sh @@ -0,0 +1,16 @@ +#!/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. +set -e + +iris session IRIS -U %SYS <<'EOF' +// Predefined accounts keep their default password (SYS) but must not demand a change on first login +Write "UnExpire: ",##class(Security.Users).UnExpireUserPasswords("*"),! +// Short Atelier session timeout so the tests can exercise expired-session recovery +Do ##class(Security.Applications).Get("/api/atelier",.a) +Set a("Timeout")=10 +Write "Timeout: ",##class(Security.Applications).Modify("/api/atelier",.a),! +Halt +EOF + +touch /tmp/ci-setup-done diff --git a/test-fixtures/iris/src/.gitkeep b/test-fixtures/iris/src/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test-fixtures/test.code-workspace b/test-fixtures/test.code-workspace deleted file mode 100644 index ba637669..00000000 --- a/test-fixtures/test.code-workspace +++ /dev/null @@ -1,15 +0,0 @@ -{ - "folders": [ - { - "path": "." - }, - ], - "settings": { - "objectscript.conn": { - "active": false - }, - "objectscript.ignoreInstallServerManager": true, - "intersystems.servers": { - } - } -} diff --git a/tsconfig.json b/tsconfig.json index 72638bc8..8574559f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "lib": ["esnext", "dom"], "sourceMap": true, "rootDir": "src", + "skipLibCheck": true, }, "exclude": ["node_modules", ".vscode-test"] }