diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7db1e4a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,70 @@ +name: Run Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: test-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + python-tests: + name: Python tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + with: + enable-cache: true + python-version: '3.13' + + - name: Install extension's bundled Python dependencies + run: uv pip install --target bundled/libs -r pyproject.toml + + - name: Install dev dependencies + run: uv sync + + - name: Run Python tests + run: uv run pytest src/test/python_tests -v + + extension-tests: + name: Extension (TypeScript) tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 10 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: '24' + cache: 'pnpm' + + - name: Install JavaScript dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm run lint + + - name: Check formatting + run: pnpm run format-check + + - name: Compile extension + run: pnpm run compile + + - name: Compile tests + run: pnpm run compile-tests + + - name: Run extension tests + # @vscode/test-electron launches a real (headless) VS Code instance, + # which needs a virtual display on Linux runners. + run: xvfb-run -a pnpm test diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad3712..afed644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,10 @@ ### 🐛 Bug fixes - Updated publisher name from `` to `david-yz-liu` +- Fixed `substitute_attr` in the bundled language server not restoring the original value when the wrapped code raised an exception ### 🔧 Internal changes - Removed unused `nox` dependency +- Improved test infrastructure: fixed and expanded Python tests, added Typescript extension tests (using Mocha + `@vscode/test-electron`), and added GitHub Actions workflow to run tests +- Removed unused glob pattern `'build/**/*.yml'` from `package.json` `format-check` command diff --git a/README.md b/README.md index f19b57a..dccf54c 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,16 @@ If you want to avoid this during testing, and you do not need breakpoints, launc ### Running tests -To run the Python tests, run `uv run pytest src/test/python_tests`. +To run the Python (language server) tests, run `uv run pytest src/test/python_tests`. + +To run the extension's TypeScript tests, run `pnpm test`. This compiles the extension and its +tests, downloads a copy of VS Code (cached under `.vscode-test/` after the first run), installs +the `ms-python.python` extension dependency into that test profile, and runs the suite in a real +(headless-capable) Extension Development Host. On Linux, this requires a display server, e.g. run +it as `xvfb-run -a pnpm test`. + +Both test suites also run automatically in CI on every pull request and push to `main`; see +`.github/workflows/test.yml`. ## Linting diff --git a/bundled/tool/lsp_utils.py b/bundled/tool/lsp_utils.py index 74ed342..b05612f 100644 --- a/bundled/tool/lsp_utils.py +++ b/bundled/tool/lsp_utils.py @@ -85,8 +85,10 @@ def substitute_attr(obj: Any, attribute: str, new_value: Any): """Manage object attributes context when using runpy.run_module().""" old_value = getattr(obj, attribute) setattr(obj, attribute, new_value) - yield - setattr(obj, attribute, old_value) + try: + yield + finally: + setattr(obj, attribute, old_value) @contextlib.contextmanager diff --git a/package.json b/package.json index 38d2e79..57b20d9 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "pnpm run compile-tests && pnpm run compile && pnpm run lint", "lint": "eslint src", - "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.yml' '.github/**/*.yml'", + "format-check": "prettier --check src/**/*.ts .github/**/*.yml", + "format": "prettier --write src/**/*.ts .github/**/*.yml", "test": "node ./out/test/runTest.js", "vsce-package": "vsce package -o python-ta.vsix" }, @@ -157,6 +158,7 @@ "devDependencies": { "@types/fs-extra": "^11.0.4", "@types/glob": "^9.0.0", + "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/vscode": "1.78.0", "@typescript-eslint/eslint-plugin": "^8.62.1", @@ -165,6 +167,7 @@ "@vscode/vsce": "^3.7.1", "eslint": "^10.3.0", "glob": "^13.0.6", + "mocha": "^11.8.0", "prettier": "^3.8.3", "ts-loader": "^9.6.2", "typescript": "^6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec41280..84b870c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@types/glob': specifier: ^9.0.0 version: 9.0.0 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 '@types/node': specifier: 20.x version: 20.19.43 @@ -54,6 +57,9 @@ importers: glob: specifier: ^13.0.6 version: 13.0.6 + mocha: + specifier: ^11.8.0 + version: 11.8.0 prettier: specifier: ^3.8.3 version: 3.9.5 @@ -184,6 +190,10 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -212,6 +222,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@secretlint/config-creator@10.2.2': resolution: {integrity: sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==} engines: {node: '>=20.0.0'} @@ -295,6 +309,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -571,6 +588,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -627,6 +648,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + browserslist@4.28.5: resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -656,6 +680,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001803: resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} @@ -674,6 +702,10 @@ packages: resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} engines: {node: '>=20.18.1'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -689,6 +721,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone-deep@4.0.1: resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} engines: {node: '>=6'} @@ -742,6 +778,10 @@ packages: supports-color: optional: true + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -773,6 +813,10 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -790,6 +834,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} @@ -806,6 +853,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -977,6 +1027,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.6: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} @@ -991,6 +1045,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -1014,6 +1072,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -1045,6 +1108,10 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -1139,10 +1206,22 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-plain-object@2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@1.3.0: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} @@ -1169,6 +1248,9 @@ packages: resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} engines: {node: '>=4'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -1279,6 +1361,10 @@ packages: lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + log-symbols@6.0.0: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} @@ -1353,6 +1439,10 @@ packages: resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1406,6 +1496,11 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mocha@11.8.0: + resolution: {integrity: sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1490,6 +1585,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -1520,6 +1618,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} @@ -1589,6 +1691,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + rc-config-loader@4.1.4: resolution: {integrity: sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==} @@ -1611,10 +1716,18 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} engines: {node: '>= 10.13.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -1678,6 +1791,9 @@ packages: engines: {node: '>=10'} hasBin: true + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -1758,6 +1874,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -1780,6 +1900,10 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + structured-source@4.0.0: resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==} @@ -2012,6 +2136,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerpool@9.3.4: + resolution: {integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2027,9 +2162,25 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yauzl@3.4.0: resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} engines: {node: '>=12'} @@ -2151,7 +2302,7 @@ snapshots: '@eslint/config-array@0.23.5': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -2187,6 +2338,15 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2218,6 +2378,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@pkgjs/parseargs@0.11.0': + optional: true + '@secretlint/config-creator@10.2.2': dependencies: '@secretlint/types': 10.2.2 @@ -2228,7 +2391,7 @@ snapshots: '@secretlint/resolver': 10.2.2 '@secretlint/types': 10.2.2 ajv: 8.20.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) rc-config-loader: 4.1.4 transitivePeerDependencies: - supports-color @@ -2237,7 +2400,7 @@ snapshots: dependencies: '@secretlint/profiler': 10.2.2 '@secretlint/types': 10.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) structured-source: 4.0.0 transitivePeerDependencies: - supports-color @@ -2250,7 +2413,7 @@ snapshots: '@textlint/module-interop': 15.7.1 '@textlint/types': 15.7.1 chalk: 5.6.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) pluralize: 8.0.0 strip-ansi: 7.2.0 table: 6.9.0 @@ -2266,7 +2429,7 @@ snapshots: '@secretlint/profiler': 10.2.2 '@secretlint/source-creator': 10.2.2 '@secretlint/types': 10.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) p-map: 7.0.5 transitivePeerDependencies: - supports-color @@ -2304,7 +2467,7 @@ snapshots: '@textlint/resolver': 15.7.1 '@textlint/types': 15.7.1 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) js-yaml: 4.3.0 lodash: 4.18.1 pluralize: 2.0.0 @@ -2342,6 +2505,8 @@ snapshots: dependencies: '@types/node': 20.19.43 + '@types/mocha@10.0.10': {} + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -2374,7 +2539,7 @@ snapshots: '@typescript-eslint/types': 8.65.0 '@typescript-eslint/typescript-estree': 8.65.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 10.6.0 typescript: 6.0.3 transitivePeerDependencies: @@ -2384,7 +2549,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -2393,7 +2558,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -2421,7 +2586,7 @@ snapshots: '@typescript-eslint/types': 8.63.0 '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) eslint: 10.6.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 @@ -2438,7 +2603,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) '@typescript-eslint/types': 8.63.0 '@typescript-eslint/visitor-keys': 8.63.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 @@ -2453,7 +2618,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@6.0.3) '@typescript-eslint/types': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 @@ -2707,6 +2872,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + argparse@2.0.1: {} astral-regex@2.0.0: {} @@ -2758,6 +2925,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-stdout@1.3.1: {} + browserslist@4.28.5: dependencies: baseline-browser-mapping: 2.10.42 @@ -2792,6 +2961,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + camelcase@6.3.0: {} + caniuse-lite@1.0.30001803: {} chalk@4.1.2: @@ -2824,6 +2995,10 @@ snapshots: undici: 7.28.0 whatwg-mimetype: 4.0.0 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + chownr@1.1.4: optional: true @@ -2835,6 +3010,12 @@ snapshots: cli-spinners@2.9.2: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone-deep@4.0.1: dependencies: is-plain-object: 2.0.4 @@ -2877,9 +3058,13 @@ snapshots: css-what@6.2.2: {} - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} decompress-response@6.0.0: dependencies: @@ -2905,6 +3090,8 @@ snapshots: detect-libc@2.1.2: optional: true + diff@7.0.0: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -2929,6 +3116,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 @@ -2943,6 +3132,8 @@ snapshots: emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -3019,7 +3210,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -3116,6 +3307,11 @@ snapshots: flatted@3.4.2: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.6: dependencies: asynckit: 0.4.0 @@ -3135,6 +3331,8 @@ snapshots: function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -3166,6 +3364,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -3197,6 +3404,8 @@ snapshots: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 @@ -3215,14 +3424,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -3277,10 +3486,16 @@ snapshots: is-number@7.0.0: {} + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + is-plain-object@2.0.4: dependencies: isobject: 3.0.1 + is-unicode-supported@0.1.0: {} + is-unicode-supported@1.3.0: {} is-unicode-supported@2.1.0: {} @@ -3301,6 +3516,12 @@ snapshots: editions: 6.22.0 textextensions: 6.11.0 + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jest-worker@27.5.1: dependencies: '@types/node': 20.19.43 @@ -3417,6 +3638,11 @@ snapshots: lodash@4.18.1: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + log-symbols@6.0.0: dependencies: chalk: 5.6.2 @@ -3479,6 +3705,10 @@ snapshots: dependencies: brace-expansion: 2.1.2 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.2 + minimist@1.2.8: optional: true @@ -3495,6 +3725,30 @@ snapshots: mkdirp-classic@0.5.3: optional: true + mocha@11.8.0: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.3(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.5.0 + he: 1.2.0 + is-path-inside: 3.0.3 + js-yaml: 4.3.0 + log-symbols: 4.1.0 + minimatch: 9.0.9 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.4 + yargs: 17.7.3 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + ms@2.1.3: {} mute-stream@0.0.8: {} @@ -3590,6 +3844,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + pako@1.0.11: {} parse-json@8.3.0: @@ -3621,6 +3877,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-scurry@2.0.2: dependencies: lru-cache: 11.5.2 @@ -3683,9 +3944,13 @@ snapshots: queue-microtask@1.2.3: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + rc-config-loader@4.1.4: dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) js-yaml: 4.3.0 json5: 2.2.3 require-from-string: 2.0.2 @@ -3729,10 +3994,14 @@ snapshots: util-deprecate: 1.0.2 optional: true + readdirp@4.1.2: {} + rechoir@0.8.0: dependencies: resolve: 1.22.12 + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-cwd@3.0.0: @@ -3782,7 +4051,7 @@ snapshots: '@secretlint/formatter': 10.2.2 '@secretlint/node': 10.2.2 '@secretlint/profiler': 10.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globby: 14.1.0 read-pkg: 9.0.1 transitivePeerDependencies: @@ -3792,6 +4061,10 @@ snapshots: semver@7.8.5: {} + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + setimmediate@1.0.5: {} shallow-clone@3.0.1: @@ -3883,6 +4156,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -3909,6 +4188,8 @@ snapshots: strip-json-comments@2.0.1: optional: true + strip-json-comments@3.1.1: {} + structured-source@4.0.0: dependencies: boundary: 2.0.0 @@ -4150,6 +4431,20 @@ snapshots: word-wrap@1.2.5: {} + workerpool@9.3.4: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + wrappy@1.0.2: optional: true @@ -4164,8 +4459,29 @@ snapshots: xmlbuilder@11.0.1: {} + y18n@5.0.8: {} + yallist@4.0.0: {} + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yauzl@3.4.0: dependencies: pend: 1.2.0 diff --git a/src/common/python.ts b/src/common/python.ts index 4c1ce89..962bd18 100644 --- a/src/common/python.ts +++ b/src/common/python.ts @@ -42,9 +42,7 @@ export async function initializePython(disposables: Disposable[]): Promise disposables.push( envsApi.onDidChangeEnvironment((e) => { onDidChangePythonInterpreterEvent.fire({ - path: e.new - ? [e.new.execInfo.run.executable] - : undefined, + path: e.new ? [e.new.execInfo.run.executable] : undefined, resource: e.uri, }); }), diff --git a/src/test/python_tests/conftest.py b/src/test/python_tests/conftest.py new file mode 100644 index 0000000..c098440 --- /dev/null +++ b/src/test/python_tests/conftest.py @@ -0,0 +1,17 @@ +"""Shared pytest setup for the Python test suite. + +Puts `bundled/libs` and `bundled/tool` on `sys.path` so test modules can +`import lsp_server`, `lsp_utils`, etc. the same way the language server +process does, without each test file repeating the path setup. +""" + +import os +import sys +from pathlib import Path + +_PROJECT_ROOT = Path(__file__).parents[3] + +for _sub_dir in ("libs", "tool"): + _dir = os.fspath(_PROJECT_ROOT / "bundled" / _sub_dir) + if _dir not in sys.path: + sys.path.insert(0, _dir) diff --git a/src/test/python_tests/lsp_test_client/session.py b/src/test/python_tests/lsp_test_client/session.py index 00c68aa..d9a42bc 100644 --- a/src/test/python_tests/lsp_test_client/session.py +++ b/src/test/python_tests/lsp_test_client/session.py @@ -56,8 +56,8 @@ def __enter__(self): shell="WITH_COVERAGE" in os.environ, ) - self._writer = JsonRpcStreamWriter(os.fdopen(self._sub.stdin.fileno(), "wb")) - self._reader = JsonRpcStreamReader(os.fdopen(self._sub.stdout.fileno(), "rb")) + self._writer = JsonRpcStreamWriter(self._sub.stdin) + self._reader = JsonRpcStreamReader(self._sub.stdout) dispatcher = { PUBLISH_DIAGNOSTICS: self._publish_diagnostics, diff --git a/src/test/python_tests/test_change_cwd.py b/src/test/python_tests/test_change_cwd.py index 0478a7a..2ac1977 100644 --- a/src/test/python_tests/test_change_cwd.py +++ b/src/test/python_tests/test_change_cwd.py @@ -2,15 +2,8 @@ import logging import os -import pathlib -import sys from unittest.mock import patch -# Ensure bundled libs and tool are importable. -_PROJECT_ROOT = pathlib.Path(__file__).parent.parent.parent.parent -sys.path.insert(0, os.fsdecode(_PROJECT_ROOT / "bundled" / "libs")) -sys.path.insert(0, os.fsdecode(_PROJECT_ROOT / "bundled" / "tool")) - import lsp_utils diff --git a/src/test/python_tests/test_data/sample1/sample.unformatted b/src/test/python_tests/test_data/sample1/sample.unformatted deleted file mode 100644 index 6c8771b..0000000 --- a/src/test/python_tests/test_data/sample1/sample.unformatted +++ /dev/null @@ -1 +0,0 @@ -import sys;print(x) \ No newline at end of file diff --git a/src/test/python_tests/test_get_cwd.py b/src/test/python_tests/test_get_cwd.py index d4e3a32..d6a384c 100644 --- a/src/test/python_tests/test_get_cwd.py +++ b/src/test/python_tests/test_get_cwd.py @@ -1,94 +1,8 @@ """Unit tests for the get_cwd() helper in lsp_server.""" import os -import pathlib -import sys import types -# --------------------------------------------------------------------------- -# Stub out bundled LSP dependencies so lsp_server can be imported without the -# full VS Code extension environment. -# --------------------------------------------------------------------------- -def _setup_mocks(): - class _MockLS: - def __init__(self, **kwargs): - pass - - def feature(self, *args, **kwargs): - return lambda f: f - - def command(self, *args, **kwargs): - return lambda f: f - - def window_log_message(self, *args, **kwargs): - pass - - def window_show_message(self, *args, **kwargs): - pass - - mock_server = types.ModuleType("pygls.server") - mock_server.LanguageServer = _MockLS - - mock_lsp_server = types.ModuleType("pygls.lsp.server") - mock_lsp_server.LanguageServer = _MockLS - - mock_lsp_mod = types.ModuleType("pygls.lsp") - mock_lsp_mod.server = mock_lsp_server - - mock_workspace = types.ModuleType("pygls.workspace") - mock_workspace.TextDocument = type("TextDocument", (), {"path": None}) - - mock_uris = types.ModuleType("pygls.uris") - mock_uris.from_fs_path = lambda p: "file://" + p - mock_uris.to_fs_path = lambda u: u.replace("file://", "") - - mock_lsp = types.ModuleType("lsprotocol.types") - for _name in [ - "TEXT_DOCUMENT_DID_OPEN", "TEXT_DOCUMENT_DID_SAVE", "TEXT_DOCUMENT_DID_CLOSE", - "TEXT_DOCUMENT_FORMATTING", "INITIALIZE", "EXIT", "SHUTDOWN", - "NOTEBOOK_DOCUMENT_DID_OPEN", "NOTEBOOK_DOCUMENT_DID_CHANGE", - "NOTEBOOK_DOCUMENT_DID_SAVE", "NOTEBOOK_DOCUMENT_DID_CLOSE", - ]: - setattr(mock_lsp, _name, _name) - for _name in [ - "Diagnostic", "DiagnosticSeverity", "DidCloseTextDocumentParams", - "DidOpenTextDocumentParams", "DidSaveTextDocumentParams", - "DocumentFormattingParams", "InitializeParams", "Position", "Range", "TextEdit", - "DidOpenNotebookDocumentParams", "DidChangeNotebookDocumentParams", - "DidSaveNotebookDocumentParams", "DidCloseNotebookDocumentParams", - "NotebookCellLanguage", "NotebookDocumentFilterWithNotebook", - "NotebookDocumentSyncOptions", "PublishDiagnosticsParams", - ]: - def _make_stub(name): - def __init__(self, *args, **kwargs): - pass - return type(name, (), {"__init__": __init__}) - setattr(mock_lsp, _name, _make_stub(_name)) - mock_lsp.MessageType = type("MessageType", (), {"Log": 4, "Error": 1, "Warning": 2, "Info": 3}) - mock_lsp.NotebookCellKind = type("NotebookCellKind", (), {"Code": 2}) - - for _mod_name, _mod in [ - ("pygls", types.ModuleType("pygls")), - ("pygls.server", mock_server), - ("pygls.lsp", mock_lsp_mod), - ("pygls.lsp.server", mock_lsp_server), - ("pygls.workspace", mock_workspace), - ("pygls.uris", mock_uris), - ("lsprotocol", types.ModuleType("lsprotocol")), - ("lsprotocol.types", mock_lsp), - ("lsp_jsonrpc", types.ModuleType("lsp_jsonrpc")), - ("lsp_utils", types.ModuleType("lsp_utils")), - ]: - if _mod_name not in sys.modules: - sys.modules[_mod_name] = _mod - - tool_dir = str(pathlib.Path(__file__).parents[3] / "bundled" / "tool") - if tool_dir not in sys.path: - sys.path.insert(0, tool_dir) - - -_setup_mocks() - -import lsp_server # noqa: E402 +import lsp_server WORKSPACE = "/home/user/myproject" diff --git a/src/test/python_tests/test_lsp_server_helpers.py b/src/test/python_tests/test_lsp_server_helpers.py new file mode 100644 index 0000000..8055f7c --- /dev/null +++ b/src/test/python_tests/test_lsp_server_helpers.py @@ -0,0 +1,204 @@ +"""Unit tests for the pure helper functions in bundled/tool/lsp_server.py. + +These exercise URI/path normalization, JSON diagnostic parsing, and the +workspace-settings lookup helpers directly, without spinning up a real LSP +session (see test_server.py / test_notebook.py for end-to-end coverage). +""" + +import json +import os +import pathlib +import types + +import pytest +from lsprotocol import types as lsp +from pygls import uris + +import lsp_server + + +@pytest.fixture(autouse=True) +def _reset_module_state(): + """Each test gets a clean slate for the module-level settings caches.""" + saved_workspace = dict(lsp_server.WORKSPACE_SETTINGS) + saved_global = dict(lsp_server.GLOBAL_SETTINGS) + lsp_server.WORKSPACE_SETTINGS.clear() + lsp_server.GLOBAL_SETTINGS.clear() + yield + lsp_server.WORKSPACE_SETTINGS.clear() + lsp_server.WORKSPACE_SETTINGS.update(saved_workspace) + lsp_server.GLOBAL_SETTINGS.clear() + lsp_server.GLOBAL_SETTINGS.update(saved_global) + + +# --------------------------------------------------------------------------- +# _get_document_path +# --------------------------------------------------------------------------- + + +def test_get_document_path_regular_file_uri(): + file_uri = "file:///home/user/project/foo.py" + document = types.SimpleNamespace(uri=file_uri) + assert lsp_server._get_document_path(document) == uris.to_fs_path(file_uri) + + +def test_get_document_path_notebook_cell_uri_strips_scheme_and_fragment(): + file_uri = "file:///home/user/project/nb.ipynb" + cell_uri = "vscode-notebook-cell:/home/user/project/nb.ipynb#cell1" + document = types.SimpleNamespace(uri=cell_uri) + assert lsp_server._get_document_path(document) == uris.to_fs_path(file_uri) + + +# --------------------------------------------------------------------------- +# _parse_json_output +# --------------------------------------------------------------------------- + + +def _make_payload(): + return [ + { + "uri": "file:///home/user/project/foo.py", + "diagnostics": [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 5}, + }, + "message": "msg", + "severity": 1, + "code": "E0001", + "source": "python-ta", + } + ], + }, + {"uri": "file:///home/user/project/bar.py", "diagnostics": []}, + ] + + +def test_parse_json_output_returns_diagnostics_for_matching_uri(): + content = json.dumps(_make_payload()) + result = lsp_server._parse_json_output(content, "file:///home/user/project/foo.py") + + assert len(result) == 1 + diagnostic = result[0] + assert isinstance(diagnostic, lsp.Diagnostic) + assert diagnostic.message == "msg" + assert diagnostic.code == "E0001" + assert diagnostic.source == "python-ta" + assert diagnostic.severity == lsp.DiagnosticSeverity.Error + + +def test_parse_json_output_ignores_leading_log_lines(): + content = "some log preamble\nmore logs\n" + json.dumps(_make_payload()) + result = lsp_server._parse_json_output(content, "file:///home/user/project/foo.py") + assert len(result) == 1 + + +def test_parse_json_output_returns_empty_list_for_unmatched_uri(): + content = json.dumps(_make_payload()) + result = lsp_server._parse_json_output(content, "file:///home/user/project/missing.py") + assert result == [] + + +def test_parse_json_output_returns_empty_list_when_no_json_array_present(): + assert lsp_server._parse_json_output("no json here", "file:///x.py") == [] + + +def test_parse_json_output_returns_empty_list_for_file_with_no_diagnostics(): + content = json.dumps(_make_payload()) + result = lsp_server._parse_json_output(content, "file:///home/user/project/bar.py") + assert result == [] + + +# --------------------------------------------------------------------------- +# _get_global_defaults +# --------------------------------------------------------------------------- + + +def test_get_global_defaults_uses_builtin_fallbacks_when_empty(): + defaults = lsp_server._get_global_defaults() + assert defaults["path"] == [] + assert defaults["args"] == [] + assert defaults["importStrategy"] == "useBundled" + assert defaults["showNotifications"] == "off" + assert defaults["configPath"] == "" + + +def test_get_global_defaults_reflects_global_settings_overrides(): + lsp_server.GLOBAL_SETTINGS.update({"importStrategy": "fromEnvironment", "args": ["--foo"]}) + defaults = lsp_server._get_global_defaults() + assert defaults["importStrategy"] == "fromEnvironment" + assert defaults["args"] == ["--foo"] + + +# --------------------------------------------------------------------------- +# _update_workspace_settings / _get_settings_by_path / _get_document_key / +# _get_settings_by_document +# --------------------------------------------------------------------------- + + +def test_update_workspace_settings_with_no_settings_uses_cwd(): + lsp_server._update_workspace_settings(None) + key = os.getcwd() + assert key in lsp_server.WORKSPACE_SETTINGS + assert lsp_server.WORKSPACE_SETTINGS[key]["workspaceFS"] == key + + +def _fs_path(path) -> str: + """Round-trips a path through a file URI, the same normalization real + document/workspace paths go through (e.g. lower-cased drive letter on + Windows), so keys built here match what lsp_server actually stores.""" + return uris.to_fs_path(uris.from_fs_path(os.fspath(path))) + + +def test_update_workspace_settings_keys_by_workspace_fs_path(tmp_path): + workspace_uri = uris.from_fs_path(os.fspath(tmp_path)) + lsp_server._update_workspace_settings( + [{"workspace": workspace_uri, "cwd": os.fspath(tmp_path)}] + ) + key = _fs_path(tmp_path) + assert key in lsp_server.WORKSPACE_SETTINGS + assert lsp_server.WORKSPACE_SETTINGS[key]["workspaceFS"] == key + + +def test_get_settings_by_path_walks_up_to_nearest_workspace(tmp_path): + workspace_uri = uris.from_fs_path(os.fspath(tmp_path)) + lsp_server._update_workspace_settings([{"workspace": workspace_uri, "cwd": "${workspaceFolder}"}]) + + nested_file = pathlib.Path(_fs_path(tmp_path)) / "src" / "pkg" / "mod.py" + settings = lsp_server._get_settings_by_path(nested_file) + assert settings["workspaceFS"] == _fs_path(tmp_path) + + +def test_get_document_key_returns_none_when_no_workspace_matches(tmp_path): + workspace_uri = uris.from_fs_path(os.fspath(tmp_path / "other-workspace")) + lsp_server._update_workspace_settings([{"workspace": workspace_uri, "cwd": "${workspaceFolder}"}]) + + document = types.SimpleNamespace(path=_fs_path(tmp_path / "unrelated" / "foo.py")) + assert lsp_server._get_document_key(document) is None + + +def test_get_document_key_finds_containing_workspace(tmp_path): + workspace_uri = uris.from_fs_path(os.fspath(tmp_path)) + lsp_server._update_workspace_settings([{"workspace": workspace_uri, "cwd": "${workspaceFolder}"}]) + + document = types.SimpleNamespace(path=_fs_path(tmp_path / "src" / "foo.py")) + assert lsp_server._get_document_key(document) == _fs_path(tmp_path) + + +def test_get_settings_by_document_none_returns_first_workspace_settings(tmp_path): + workspace_uri = uris.from_fs_path(os.fspath(tmp_path)) + lsp_server._update_workspace_settings([{"workspace": workspace_uri, "cwd": "${workspaceFolder}"}]) + + settings = lsp_server._get_settings_by_document(None) + assert settings["workspaceFS"] == _fs_path(tmp_path) + + +def test_get_settings_by_document_outside_any_workspace_synthesizes_settings(tmp_path): + workspace_uri = uris.from_fs_path(os.fspath(tmp_path / "workspace")) + lsp_server._update_workspace_settings([{"workspace": workspace_uri, "cwd": "${workspaceFolder}"}]) + + outside_file = pathlib.Path(_fs_path(tmp_path)) / "elsewhere" / "foo.py" + document = types.SimpleNamespace(path=os.fspath(outside_file)) + settings = lsp_server._get_settings_by_document(document) + assert settings["workspaceFS"] == os.fspath(outside_file.parent) diff --git a/src/test/python_tests/test_lsp_utils.py b/src/test/python_tests/test_lsp_utils.py new file mode 100644 index 0000000..29a41f0 --- /dev/null +++ b/src/test/python_tests/test_lsp_utils.py @@ -0,0 +1,118 @@ +"""Unit tests for the pure helper functions in bundled/tool/lsp_utils.py.""" + +import os +import sys + +import lsp_utils + + +def test_as_list_wraps_single_value(): + assert lsp_utils.as_list("a") == ["a"] + + +def test_as_list_wraps_non_sequence_object(): + obj = object() + assert lsp_utils.as_list(obj) == [obj] + + +def test_as_list_passes_through_list(): + original = ["a", "b"] + assert lsp_utils.as_list(original) is original + + +def test_as_list_passes_through_tuple(): + original = ("a", "b") + assert lsp_utils.as_list(original) is original + + +def test_is_same_path_true_for_equivalent_paths(tmp_path): + p1 = str(tmp_path / "foo" / ".." / "bar.py") + p2 = str(tmp_path / "bar.py") + assert lsp_utils.is_same_path(p1, p2) + + +def test_is_same_path_false_for_different_paths(tmp_path): + p1 = str(tmp_path / "foo.py") + p2 = str(tmp_path / "bar.py") + assert not lsp_utils.is_same_path(p1, p2) + + +def test_is_same_path_case_insensitive_on_windows(tmp_path): + p1 = str(tmp_path / "Foo.py") + p2 = str(tmp_path / "foo.py") + if os.name == "nt": + assert lsp_utils.is_same_path(p1, p2) + else: + assert not lsp_utils.is_same_path(p1, p2) + + +def test_is_current_interpreter_true_for_sys_executable(): + assert lsp_utils.is_current_interpreter(sys.executable) + + +def test_is_current_interpreter_false_for_other_path(): + assert not lsp_utils.is_current_interpreter("/definitely/not/python") + + +def test_is_stdlib_file_true_for_installed_site_package(): + # Despite the name, is_stdlib_file() checks site-packages directories + # (site.getsitepackages() / getusersitepackages()), not the interpreter's + # standard library directory, so a real stdlib module like os.py returns + # False here -- see test_is_stdlib_file_false_for_stdlib_module below. + import pytest as _pytest + + assert lsp_utils.is_stdlib_file(_pytest.__file__) + + +def test_is_stdlib_file_false_for_stdlib_module(): + assert not lsp_utils.is_stdlib_file(os.__file__) + + +def test_is_stdlib_file_false_for_project_file(): + assert not lsp_utils.is_stdlib_file(os.fspath(lsp_utils.__file__)) + + +def test_run_result_holds_stdout_and_stderr(): + result = lsp_utils.RunResult("out", "err") + assert result.stdout == "out" + assert result.stderr == "err" + + +def test_custom_io_round_trips_written_content(): + stream = lsp_utils.CustomIO("", encoding="utf-8") + stream.write("hello") + assert stream.get_value() == "hello" + # close() is intentionally a no-op so the buffer stays readable. + stream.close() + assert stream.get_value() == "hello" + + +def test_substitute_attr_restores_original_value_after_context(): + class Obj: + attr = "original" + + obj = Obj() + with lsp_utils.substitute_attr(obj, "attr", "temporary"): + assert obj.attr == "temporary" + assert obj.attr == "original" + + +def test_substitute_attr_restores_on_exception(): + class Obj: + attr = "original" + + obj = Obj() + try: + with lsp_utils.substitute_attr(obj, "attr", "temporary"): + raise ValueError("boom") + except ValueError: + pass + assert obj.attr == "original" + + +def test_redirect_io_swaps_and_restores_stream(): + original_stdout = sys.stdout + replacement = lsp_utils.CustomIO("", encoding="utf-8") + with lsp_utils.redirect_io("stdout", replacement): + assert sys.stdout is replacement + assert sys.stdout is original_stdout diff --git a/src/test/python_tests/test_server.py b/src/test/python_tests/test_server.py index 6db9c23..0059c79 100644 --- a/src/test/python_tests/test_server.py +++ b/src/test/python_tests/test_server.py @@ -10,7 +10,6 @@ TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" TEST_FILE_URI = utils.as_uri(str(TEST_FILE_PATH)) -SERVER_INFO = utils.get_server_info_defaults() TIMEOUT = 10 # 10 seconds @@ -45,45 +44,70 @@ def _handler(params): # wait for some time to receive all notifications done.wait(TIMEOUT) - # TODO: Add your linter specific diagnostic result here + # Note: Diagnostics come from python-ta's `pyta-lsp` JSON output, so + # `source` is "python-ta" rather than the extension/server display name. expected = { "uri": TEST_FILE_URI, "diagnostics": [ { - # "range": { - # "start": {"line": 0, "character": 0}, - # "end": {"line": 0, "character": 0}, - # }, - # "message": "Missing module docstring", - # "severity": 3, - # "code": "C0114:missing-module-docstring", - "source": SERVER_INFO["name"], + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 2, "character": 8}, + }, + "message": "Missing module docstring", + "severity": 3, + "code": "C0114", + "source": "python-ta", }, { - # "range": { - # "start": {"line": 2, "character": 6}, - # "end": { - # "line": 2, - # "character": 7, - # }, - # }, - # "message": "Undefined variable 'x'", - # "severity": 1, - # "code": "E0602:undefined-variable", - "source": SERVER_INFO["name"], + "range": { + "start": {"line": 2, "character": 0}, + "end": {"line": 2, "character": 8}, + }, + "message": "Forbidden top-level code found on line 3", + "severity": 1, + "code": "E9992", + "source": "python-ta", }, { - # "range": { - # "start": {"line": 0, "character": 0}, - # "end": { - # "line": 0, - # "character": 10, - # }, - # }, - # "message": "Unused import sys", - # "severity": 2, - # "code": "W0611:unused-import", - "source": SERVER_INFO["name"], + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 10}, + }, + "message": "You may not import module sys.", + "severity": 1, + "code": "E9999", + "source": "python-ta", + }, + { + "range": { + "start": {"line": 2, "character": 0}, + "end": {"line": 2, "character": 8}, + }, + "message": "Used input/output function print", + "severity": 1, + "code": "E9998", + "source": "python-ta", + }, + { + "range": { + "start": {"line": 2, "character": 6}, + "end": {"line": 2, "character": 7}, + }, + "message": "Undefined variable 'x'", + "severity": 1, + "code": "E0602", + "source": "python-ta", + }, + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 0, "character": 10}, + }, + "message": "The import import sys is unused, and so can be removed.", + "severity": 2, + "code": "W0611", + "source": "python-ta", }, ], } @@ -91,46 +115,42 @@ def _handler(params): assert_that(actual, is_(expected)) -def test_formatting_example(): - """Test formatting a python file.""" - FORMATTED_TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.py" - UNFORMATTED_TEST_FILE_PATH = constants.TEST_DATA / "sample1" / "sample.unformatted" - - contents = UNFORMATTED_TEST_FILE_PATH.read_text() - lines = contents.splitlines(keepends=False) +def test_linting_clears_on_close(): + """Diagnostics are cleared (empty list) when a linted file is closed.""" + contents = TEST_FILE_PATH.read_text() actual = [] - with utils.PythonFile(contents, UNFORMATTED_TEST_FILE_PATH.parent) as pf: - uri = utils.as_uri(str(pf.fullpath)) + with session.LspSession() as ls_session: + ls_session.initialize(defaults.VSCODE_DEFAULT_INITIALIZE) - with session.LspSession() as ls_session: - ls_session.initialize() - ls_session.notify_did_open( - { - "textDocument": { - "uri": uri, - "languageId": "python", - "version": 1, - "text": contents, - } - } - ) - actual = ls_session.text_document_formatting( - { - "textDocument": {"uri": uri}, - # `options` is not used by black - "options": {"tabSize": 4, "insertSpaces": True}, + opened = Event() + closed = Event() + + def _handler(params): + nonlocal actual + actual = params + if params.get("diagnostics") == []: + closed.set() + else: + opened.set() + + ls_session.set_notification_callback(session.PUBLISH_DIAGNOSTICS, _handler) + + ls_session.notify_did_open( + { + "textDocument": { + "uri": TEST_FILE_URI, + "languageId": "python", + "version": 1, + "text": contents, } - ) - - expected = [ - { - "range": { - "start": {"line": 0, "character": 0}, - "end": {"line": len(lines), "character": 0}, - }, - "newText": FORMATTED_TEST_FILE_PATH.read_text(), - } - ] + } + ) + opened.wait(TIMEOUT) - assert_that(actual, is_(expected)) + ls_session.notify_did_close( + {"textDocument": {"uri": TEST_FILE_URI}} + ) + closed.wait(TIMEOUT) + + assert_that(actual, is_({"uri": TEST_FILE_URI, "diagnostics": []})) diff --git a/src/test/runTest.ts b/src/test/runTest.ts new file mode 100644 index 0000000..4bba7cd --- /dev/null +++ b/src/test/runTest.ts @@ -0,0 +1,45 @@ +import * as cp from 'child_process'; +import * as path from 'path'; +import { downloadAndUnzipVSCode, resolveCliArgsFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test runner script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // A throwaway workspace folder so the extension activates + // (`workspaceContains:*.py`) without touching a real project. + const workspacePath = path.resolve(__dirname, '../../src/test/testFixture'); + + const vscodeExecutablePath = await downloadAndUnzipVSCode(); + const [cliPath, ...cliArgs] = resolveCliArgsFromVSCodeExecutablePath(vscodeExecutablePath); + + // package.json declares `ms-python.python` as an extension dependency, + // so it must be installed in the test profile or activation fails. + // `shell: true` is required on Windows, where the CLI entry point is a `.cmd` file. + cp.spawnSync(cliPath, [...cliArgs, '--install-extension', 'ms-python.python'], { + encoding: 'utf-8', + stdio: 'inherit', + shell: process.platform === 'win32', + }); + + // Download VS Code, unzip it and run the integration test + await runTests({ + vscodeExecutablePath, + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [workspacePath], + }); + } catch (err) { + console.error('Failed to run tests'); + console.error(err); + process.exit(1); + } +} + +main(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts new file mode 100644 index 0000000..9a5edce --- /dev/null +++ b/src/test/suite/extension.test.ts @@ -0,0 +1,20 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; + +suite('Extension activation', () => { + test('extension is present and activates', async () => { + const ext = vscode.extensions.getExtension('david-yz-liu.python-ta'); + assert.ok(ext, 'Extension not found by id david-yz-liu.python-ta'); + + await ext!.activate(); + assert.strictEqual(ext!.isActive, true); + }); + + test('registers the restart command', async () => { + const ext = vscode.extensions.getExtension('david-yz-liu.python-ta'); + await ext?.activate(); + + const commands = await vscode.commands.getCommands(true); + assert.ok(commands.includes('python-ta.restart'), 'python-ta.restart command was not registered'); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts new file mode 100644 index 0000000..e0aa176 --- /dev/null +++ b/src/test/suite/index.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import { glob } from 'glob'; +import Mocha from 'mocha'; + +// @vscode/test-electron launches a real VS Code Extension Host and expects +// this module to export a `run()` it can call once that host is up. Mocha is +// used here (rather than e.g. Jest) because it fits that contract directly — +// it's a plain library we drive ourselves with `new Mocha()` / `mocha.run()`, +// with no test process or environment of its own to reconcile with the one +// @vscode/test-electron already launched. +export async function run(): Promise { + const mocha = new Mocha({ + ui: 'tdd', + color: true, + timeout: 20000, + }); + + const testsRoot = path.resolve(__dirname, '.'); + const files = await glob('**/*.test.js', { cwd: testsRoot }); + + files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); + + return new Promise((resolve, reject) => { + try { + mocha.run((failures) => { + if (failures > 0) { + reject(new Error(`${failures} tests failed.`)); + } else { + resolve(); + } + }); + } catch (err) { + reject(err); + } + }); +} diff --git a/src/test/suite/settings.test.ts b/src/test/suite/settings.test.ts new file mode 100644 index 0000000..42287a6 --- /dev/null +++ b/src/test/suite/settings.test.ts @@ -0,0 +1,58 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { checkIfConfigurationChanged, getGlobalSettings } from '../../common/settings'; + +const NAMESPACE = 'python-ta'; + +suite('settings.getGlobalSettings', () => { + test('returns defaults matching package.json when nothing overridden', async () => { + const settings = await getGlobalSettings(NAMESPACE, false); + assert.deepStrictEqual(settings.args, []); + assert.deepStrictEqual(settings.path, []); + assert.strictEqual(settings.importStrategy, 'useBundled'); + assert.strictEqual(settings.showNotifications, 'off'); + assert.strictEqual(settings.configPath, ''); + }); +}); + +suite('settings.checkIfConfigurationChanged', () => { + test('returns true when a namespaced setting changes', async () => { + const config = vscode.workspace.getConfiguration(NAMESPACE); + const original = config.get('args'); + + const changed = new Promise((resolve) => { + const disposable = vscode.workspace.onDidChangeConfiguration((e) => { + disposable.dispose(); + resolve(e); + }); + }); + + await config.update('args', ['--foo'], vscode.ConfigurationTarget.Global); + try { + const event = await changed; + assert.strictEqual(checkIfConfigurationChanged(event, NAMESPACE), true); + } finally { + await config.update('args', original, vscode.ConfigurationTarget.Global); + } + }); + + test('returns false for changes to an unrelated configuration namespace', async () => { + const config = vscode.workspace.getConfiguration('editor'); + const original = config.get('wordWrap'); + + const changed = new Promise((resolve) => { + const disposable = vscode.workspace.onDidChangeConfiguration((e) => { + disposable.dispose(); + resolve(e); + }); + }); + + await config.update('wordWrap', 'on', vscode.ConfigurationTarget.Global); + try { + const event = await changed; + assert.strictEqual(checkIfConfigurationChanged(event, NAMESPACE), false); + } finally { + await config.update('wordWrap', original, vscode.ConfigurationTarget.Global); + } + }); +}); diff --git a/src/test/suite/setup.test.ts b/src/test/suite/setup.test.ts new file mode 100644 index 0000000..119001e --- /dev/null +++ b/src/test/suite/setup.test.ts @@ -0,0 +1,10 @@ +import * as assert from 'assert'; +import { loadServerDefaults } from '../../common/setup'; + +suite('setup.loadServerDefaults', () => { + test('reads serverInfo from package.json', () => { + const info = loadServerDefaults(); + assert.strictEqual(info.name, 'PythonTA VS Code Extension'); + assert.strictEqual(info.module, 'python-ta'); + }); +}); diff --git a/src/test/suite/utilities.test.ts b/src/test/suite/utilities.test.ts new file mode 100644 index 0000000..f658509 --- /dev/null +++ b/src/test/suite/utilities.test.ts @@ -0,0 +1,43 @@ +import * as assert from 'assert'; +import { LogLevel } from 'vscode'; +import { Trace } from 'vscode-jsonrpc/node'; +import { getLSClientTraceLevel, getProjectRoot } from '../../common/utilities'; + +suite('utilities.getLSClientTraceLevel', () => { + test('returns Off when both levels are Off', () => { + assert.strictEqual(getLSClientTraceLevel(LogLevel.Off, LogLevel.Off), Trace.Off); + }); + + test('falls back to global level when channel level is Off', () => { + assert.strictEqual(getLSClientTraceLevel(LogLevel.Off, LogLevel.Debug), Trace.Verbose); + }); + + test('falls back to channel level when global level is Off', () => { + assert.strictEqual(getLSClientTraceLevel(LogLevel.Info, LogLevel.Off), Trace.Messages); + }); + + test('uses the more verbose (numerically smaller) of the two levels', () => { + // Info (2) is more verbose than Warning (3); Debug (4) is more verbose than Info. + assert.strictEqual(getLSClientTraceLevel(LogLevel.Info, LogLevel.Warning), Trace.Messages); + assert.strictEqual(getLSClientTraceLevel(LogLevel.Debug, LogLevel.Info), Trace.Verbose); + }); + + test('maps Debug and Trace to Trace.Verbose', () => { + assert.strictEqual(getLSClientTraceLevel(LogLevel.Debug, LogLevel.Debug), Trace.Verbose); + assert.strictEqual(getLSClientTraceLevel(LogLevel.Trace, LogLevel.Trace), Trace.Verbose); + }); + + test('maps Error, Warning, and Info to Trace.Messages', () => { + assert.strictEqual(getLSClientTraceLevel(LogLevel.Error, LogLevel.Error), Trace.Messages); + assert.strictEqual(getLSClientTraceLevel(LogLevel.Warning, LogLevel.Warning), Trace.Messages); + assert.strictEqual(getLSClientTraceLevel(LogLevel.Info, LogLevel.Info), Trace.Messages); + }); +}); + +suite('utilities.getProjectRoot', () => { + test('resolves to a workspace folder without throwing', async () => { + const root = await getProjectRoot(); + assert.ok(root.uri); + assert.ok(root.name.length > 0); + }); +}); diff --git a/src/test/testFixture/sample.py b/src/test/testFixture/sample.py new file mode 100644 index 0000000..61b9bdd --- /dev/null +++ b/src/test/testFixture/sample.py @@ -0,0 +1,7 @@ +"""A minimal Python file used only to give the test workspace a *.py file +so the extension's `workspaceContains:*.py` activation event fires.""" + + +def add(a: int, b: int) -> int: + """Return the sum of a and b.""" + return a + b