From 2490d0cdad09625263720e90fc64747cde2eb7eb Mon Sep 17 00:00:00 2001 From: MichaelXt Date: Mon, 10 Feb 2025 13:56:13 -0800 Subject: [PATCH] Implement Godot-in-the-loop test suite and fix debugger errors (#788) Fixes for get variables issues 1. Same reference but different variable override fix, which resulted in variables being lost ** Now different GodotVariable instances are used for different variables with same reference ** const replacement = {value: rawObject, sub_values: sub_values } as GodotVariable; 2. 'Signal' type handling crash and string representation fix ** value.sub_values()?.map 3. Empty scopes return fix ** if (this.ongoing_inspections.length === 0 && stackVars.remaining == 0) 4. Various splice from findIndex fixes (where findIndex can return -1) 5. Added variables tests ** updated vscode types to version 1.96 to use `onDidChangeActiveStackItem` for breakpoint hit detection in tests 1 & 3 should fix https://github.com/godotengine/godot-vscode-plugin/issues/779 --- .github/workflows/ci.yml | 27 ++ .gitignore | 2 + .vscode-test.js | 2 + .vscode/extensions.json | 5 + .vscode/launch.json | 7 +- package-lock.json | 207 +++++----- package.json | 10 +- src/debugger/godot4/debug_session.ts | 62 ++- .../godot4/debugger_variables.test.ts | 359 ++++++++++++++++++ src/debugger/godot4/helpers.ts | 62 +-- src/debugger/godot4/server_controller.ts | 18 +- src/debugger/godot4/variables/variants.ts | 2 +- src/debugger/scene_tree_provider.ts | 5 +- src/formatter/formatter.test.ts | 4 + src/scene_tools/types.ts | 5 +- .../.vscode/launch.json | 40 ++ .../test-dap-project-godot4/BuiltInTypes.gd | 39 ++ .../test-dap-project-godot4/BuiltInTypes.tscn | 11 + .../test-dap-project-godot4/ExtensiveVars.gd | 38 ++ .../ExtensiveVars.tscn | 12 + .../ExtensiveVars_Label.gd | 14 + .../test-dap-project-godot4/GlobalScript.gd | 3 + .../test-dap-project-godot4/Node1.gd | 8 + .../test-dap-project-godot4/NodeVars.gd | 9 + .../test-dap-project-godot4/NodeVars.tscn | 17 + .../test-dap-project-godot4/ScopeVars.gd | 8 + .../test-dap-project-godot4/ScopeVars.tscn | 11 + .../test-dap-project-godot4/TestClassA.gd | 5 + .../test-dap-project-godot4/project.godot | 19 + tsconfig.json | 3 +- 30 files changed, 854 insertions(+), 160 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 src/debugger/godot4/debugger_variables.test.ts create mode 100644 test_projects/test-dap-project-godot4/.vscode/launch.json create mode 100644 test_projects/test-dap-project-godot4/BuiltInTypes.gd create mode 100644 test_projects/test-dap-project-godot4/BuiltInTypes.tscn create mode 100644 test_projects/test-dap-project-godot4/ExtensiveVars.gd create mode 100644 test_projects/test-dap-project-godot4/ExtensiveVars.tscn create mode 100644 test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd create mode 100644 test_projects/test-dap-project-godot4/GlobalScript.gd create mode 100644 test_projects/test-dap-project-godot4/Node1.gd create mode 100644 test_projects/test-dap-project-godot4/NodeVars.gd create mode 100644 test_projects/test-dap-project-godot4/NodeVars.tscn create mode 100644 test_projects/test-dap-project-godot4/ScopeVars.gd create mode 100644 test_projects/test-dap-project-godot4/ScopeVars.tscn create mode 100644 test_projects/test-dap-project-godot4/TestClassA.gd create mode 100644 test_projects/test-dap-project-godot4/project.godot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c437473..5b0e98e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ jobs: test: name: Test strategy: + fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -17,9 +18,35 @@ jobs: with: node-version: 16.x + - name: Install Godot (Ubuntu) + if: matrix.os == 'ubuntu-latest' + run: | + wget https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_linux.x86_64.zip + unzip Godot_v4.3-stable_linux.x86_64.zip + sudo mv Godot_v4.3-stable_linux.x86_64 /usr/local/bin/godot + chmod +x /usr/local/bin/godot + + - name: Install Godot (macOS) + if: matrix.os == 'macos-latest' + run: | + curl -L -o Godot.zip https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_macos.universal.zip + unzip Godot.zip + sudo mv Godot.app /Applications/Godot.app + sudo ln -s /Applications/Godot.app/Contents/MacOS/Godot /usr/local/bin/godot + + - name: Install Godot (Windows) + if: matrix.os == 'windows-latest' + run: | + Invoke-WebRequest -Uri "https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_win64.exe.zip" -OutFile "Godot.zip" + Expand-Archive -Path "Godot.zip" -DestinationPath "C:\Godot43" + "C:\Godot43\Godot_v4.3-stable_win64.exe %*" | Out-File -Encoding ascii -FilePath ([Environment]::SystemDirectory+"\godot.cmd") + - name: Install dependencies run: npm install + - name: Godot init project + run: godot --import test_projects/test-dap-project-godot4/project.godot --headless + - name: Run headless test uses: coactions/setup-xvfb@v1 with: diff --git a/.gitignore b/.gitignore index 6b78833..cb97664 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ node_modules .vscode-test workspace.code-workspace .history +.godot +*.tmp \ No newline at end of file diff --git a/.vscode-test.js b/.vscode-test.js index a96fa20..d2759de 100644 --- a/.vscode-test.js +++ b/.vscode-test.js @@ -5,5 +5,7 @@ module.exports = defineConfig( // version: '1.84.0', label: 'unitTests', files: 'out/**/*.test.js', + launchArgs: ['--disable-extensions'], + workspaceFolder: './test_projects/test-dap-project-godot4', } ); diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..5f7e489 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-vscode.extension-test-runner" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 25c1217..4d288b0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,6 +5,7 @@ { "version": "0.2.0", "configurations": [ + { "name": "Run Extension", "type": "extensionHost", @@ -22,7 +23,7 @@ ], "preLaunchTask": "npm: watch", "env": { - "VSCODE_DEBUG_MODE": true + "VSCODE_DEBUG_MODE": "true" } }, { @@ -33,7 +34,7 @@ "args": [ "--profile=temp", "--extensionDevelopmentPath=${workspaceFolder}", - "${workspaceFolder}/workspace.code-workspace" + "${workspaceFolder}/test_projects/test-dap-project-godot4" ], "outFiles": [ "${workspaceFolder}/out/**/*.js" @@ -44,7 +45,7 @@ ], "preLaunchTask": "npm: watch", "env": { - "VSCODE_DEBUG_MODE": true + "VSCODE_DEBUG_MODE": "true" } }, ] diff --git a/package-lock.json b/package-lock.json index a2cccc6..5b1954a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "2.3.0", "license": "MIT", "dependencies": { - "@vscode/debugadapter": "^1.64.0", - "@vscode/debugprotocol": "^1.64.0", + "@vscode/debugadapter": "^1.68.0", + "@vscode/debugprotocol": "^1.68.0", "await-notify": "^1.0.1", "global": "^4.4.0", "marked": "^4.0.11", @@ -25,11 +25,12 @@ }, "devDependencies": { "@types/chai": "^4.3.11", + "@types/chai-subset": "^1.3.5", "@types/marked": "^4.0.8", "@types/mocha": "^10.0.6", "@types/node": "^18.15.0", "@types/prismjs": "^1.16.8", - "@types/vscode": "^1.80.0", + "@types/vscode": "^1.96.0", "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/eslint-plugin-tslint": "^5.57.1", @@ -38,6 +39,7 @@ "@vscode/test-electron": "^2.3.8", "@vscode/vsce": "^2.29.0", "chai": "^4.3.10", + "chai-subset": "^1.6.0", "esbuild": "^0.17.15", "eslint": "^8.37.0", "mocha": "^10.2.0", @@ -47,7 +49,7 @@ "typescript": "^5.2.2" }, "engines": { - "vscode": "^1.80.0" + "vscode": "^1.96.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1067,6 +1069,15 @@ "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", "dev": true }, + "node_modules/@types/chai-subset": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", + "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", @@ -1104,9 +1115,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.82.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz", - "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==", + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz", + "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", "dev": true }, "node_modules/@types/ws": { @@ -1414,20 +1425,20 @@ } }, "node_modules/@vscode/debugadapter": { - "version": "1.64.0", - "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.64.0.tgz", - "integrity": "sha512-XygE985qmNCzJExDnam4bErK6FG9Ck8S5TRPDNESwkt7i3OXqw5a3vYb7Dteyhz9YMEf7hwhFoT46Mjc45nJUg==", + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.68.0.tgz", + "integrity": "sha512-D6gk5Fw2y4FV8oYmltoXpj+VAZexxJFopN/mcZ6YcgzQE9dgq2L45Aj3GLxScJOD6GeLILcxJIaA8l3v11esGg==", "dependencies": { - "@vscode/debugprotocol": "1.64.0" + "@vscode/debugprotocol": "1.68.0" }, "engines": { "node": ">=14" } }, "node_modules/@vscode/debugprotocol": { - "version": "1.64.0", - "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.64.0.tgz", - "integrity": "sha512-Zhf3KvB+J04M4HPE2yCvEILGVtPixXUQMLBvx4QcAtjhc5lnwlZbbt80LCsZO2B+2BH8RMgVXk3QQ5DEzEne2Q==" + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz", + "integrity": "sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg==" }, "node_modules/@vscode/test-cli": { "version": "0.0.4", @@ -1892,9 +1903,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -2192,9 +2203,9 @@ } }, "node_modules/chai": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", - "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", @@ -2203,12 +2214,21 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" } }, + "node_modules/chai-subset": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chai-subset/-/chai-subset-1.6.0.tgz", + "integrity": "sha512-K3d+KmqdS5XKW5DWPd5sgNffL3uxdDe+6GdnJh3AYPhwnBGRY5urfvfcbRtWIvvpz+KxkL9FeBB6MZewLUNwug==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2498,12 +2518,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4466,32 +4486,31 @@ "optional": true }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -4499,10 +4518,6 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha/node_modules/argparse": { @@ -4521,9 +4536,9 @@ } }, "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -4541,6 +4556,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mocha/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4563,9 +4598,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -4574,12 +4609,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4608,9 +4637,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/mute-stream": { @@ -4619,18 +4648,6 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -5247,9 +5264,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -5635,9 +5652,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -5774,9 +5791,9 @@ } }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "engines": { "node": ">=4" @@ -5951,9 +5968,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -6222,9 +6239,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" diff --git a/package.json b/package.json index c26dae3..b264186 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "author": "The Godot Engine community", "publisher": "geequlim", "engines": { - "vscode": "^1.80.0" + "vscode": "^1.96.0" }, "categories": [ "Programming Languages", @@ -866,11 +866,12 @@ }, "devDependencies": { "@types/chai": "^4.3.11", + "@types/chai-subset": "^1.3.5", "@types/marked": "^4.0.8", "@types/mocha": "^10.0.6", "@types/node": "^18.15.0", "@types/prismjs": "^1.16.8", - "@types/vscode": "^1.80.0", + "@types/vscode": "^1.96.0", "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/eslint-plugin-tslint": "^5.57.1", @@ -879,6 +880,7 @@ "@vscode/test-electron": "^2.3.8", "@vscode/vsce": "^2.29.0", "chai": "^4.3.10", + "chai-subset": "^1.6.0", "esbuild": "^0.17.15", "eslint": "^8.37.0", "mocha": "^10.2.0", @@ -888,8 +890,8 @@ "typescript": "^5.2.2" }, "dependencies": { - "@vscode/debugadapter": "^1.64.0", - "@vscode/debugprotocol": "^1.64.0", + "@vscode/debugadapter": "^1.68.0", + "@vscode/debugprotocol": "^1.68.0", "await-notify": "^1.0.1", "global": "^4.4.0", "marked": "^4.0.11", diff --git a/src/debugger/godot4/debug_session.ts b/src/debugger/godot4/debug_session.ts index 15876ca..0e71156 100644 --- a/src/debugger/godot4/debug_session.ts +++ b/src/debugger/godot4/debug_session.ts @@ -17,7 +17,7 @@ import { AttachRequestArguments, LaunchRequestArguments } from "../debugger"; import { SceneTreeProvider } from "../scene_tree_provider"; import { is_variable_built_in_type, parse_variable } from "./helpers"; import { ServerController } from "./server_controller"; -import { ObjectId } from "./variables/variants"; +import { ObjectId, RawObject } from "./variables/variants"; const log = createLogger("debugger.session", { output: "Godot Debugger" }); @@ -55,6 +55,7 @@ export class GodotDebugSession extends LoggingDebugSession { response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments, ) { + log.info("initializeRequest", args); response.body = response.body || {}; response.body.supportsConfigurationDoneRequest = true; @@ -83,6 +84,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) { + log.info("launchRequest", args); await this.configuration_done.wait(1000); this.mode = "launch"; @@ -95,6 +97,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected async attachRequest(response: DebugProtocol.AttachResponse, args: AttachRequestArguments) { + log.info("attachRequest", args); await this.configuration_done.wait(1000); this.mode = "attach"; @@ -109,11 +112,13 @@ export class GodotDebugSession extends LoggingDebugSession { response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments, ) { + log.info("configurationDoneRequest", args); this.configuration_done.notify(); this.sendResponse(response); } protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments) { + log.info("continueRequest", args); if (!this.exception) { response.body = { allThreadsContinued: true }; this.controller.continue(); @@ -122,6 +127,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected async evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments) { + log.info("evaluateRequest", args); await debug.activeDebugSession.customRequest("scopes", { frameId: 0 }); if (this.all_scopes) { @@ -149,6 +155,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments) { + log.info("nextRequest", args); if (!this.exception) { this.controller.next(); this.sendResponse(response); @@ -156,6 +163,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected pauseRequest(response: DebugProtocol.PauseResponse, args: DebugProtocol.PauseArguments) { + log.info("pauseRequest", args); if (!this.exception) { this.controller.break(); this.sendResponse(response); @@ -163,6 +171,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected async scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments) { + log.info("scopesRequest", args); this.controller.request_stack_frame_vars(args.frameId); await this.got_scope.wait(2000); @@ -180,6 +189,7 @@ export class GodotDebugSession extends LoggingDebugSession { response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments, ) { + log.info("setBreakPointsRequest", args); const path = (args.source.path as string).replace(/\\/g, "/"); const client_lines = args.lines || []; @@ -216,6 +226,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments) { + log.info("stackTraceRequest", args); if (this.debug_data.last_frame) { response.body = { totalFrames: this.debug_data.last_frames.length, @@ -234,6 +245,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected stepInRequest(response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments) { + log.info("stepInRequest", args); if (!this.exception) { this.controller.step(); this.sendResponse(response); @@ -241,6 +253,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected stepOutRequest(response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments) { + log.info("stepOutRequest", args); if (!this.exception) { this.controller.step_out(); this.sendResponse(response); @@ -248,6 +261,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected terminateRequest(response: DebugProtocol.TerminateResponse, args: DebugProtocol.TerminateArguments) { + log.info("terminateRequest", args); if (this.mode === "launch") { this.controller.stop(); this.sendEvent(new TerminatedEvent()); @@ -256,6 +270,7 @@ export class GodotDebugSession extends LoggingDebugSession { } protected threadsRequest(response: DebugProtocol.ThreadsResponse) { + log.info("threadsRequest"); response.body = { threads: [new Thread(0, "thread_1")] }; this.sendResponse(response); } @@ -264,6 +279,7 @@ export class GodotDebugSession extends LoggingDebugSession { response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, ) { + log.info("variablesRequest", args); if (!this.all_scopes) { response.body = { variables: [], @@ -347,48 +363,62 @@ export class GodotDebugSession extends LoggingDebugSession { this.add_to_inspections(); - if (this.ongoing_inspections.length === 0) { + if (this.ongoing_inspections.length === 0 && stackVars.remaining == 0) { + // in case if stackVars are empty, the this.ongoing_inspections will be empty also + // godot 4.3 generates empty stackVars with remaining > 0 on a breakpoint stop + // godot will continue sending `stack_frame_vars` until all `stackVars.remaining` are sent + // at this moment `stack_frame_vars` will call `set_scopes` again with cumulated stackVars + // TODO: godot won't send the recursive variable, see related https://github.com/godotengine/godot/issues/76019 + // in that case the vscode extension fails to call this.got_scope.notify(); + // hence the extension needs to be refactored to handle missing `stack_frame_vars` messages this.previous_inspections = []; this.got_scope.notify(); } } - public set_inspection(id: bigint, replacement: GodotVariable) { + public set_inspection(id: bigint, rawObject: RawObject, sub_values: GodotVariable[]) { const variables = this.all_scopes.filter((va) => va && va.value instanceof ObjectId && va.value.id === id); for (const va of variables) { const index = this.all_scopes.findIndex((va_id) => va_id === va); + if (index < 0) { + continue; + } const old = this.all_scopes.splice(index, 1); + // GodotVariable instance will be different for different variables, even if the referenced object id is the same: + const replacement = {value: rawObject, sub_values: sub_values } as GodotVariable; replacement.name = old[0].name; replacement.scope_path = old[0].scope_path; this.append_variable(replacement, index); } - this.ongoing_inspections.splice( - this.ongoing_inspections.findIndex((va_id) => va_id === id), - 1, - ); + const ongoing_inspections_index = this.ongoing_inspections.findIndex((va_id) => va_id === id); + if (ongoing_inspections_index >= 0) { + this.ongoing_inspections.splice(ongoing_inspections_index, 1); + } + this.previous_inspections.push(id); // this.add_to_inspections(); if (this.ongoing_inspections.length === 0) { + // the `ongoing_inspections` is not empty, until all scopes are fully resolved + // once last inspection is resolved: Notify that we got full scope this.previous_inspections = []; this.got_scope.notify(); } } private add_to_inspections() { - for (const va of this.all_scopes) { - if (va && va.value instanceof ObjectId) { - if ( - !this.ongoing_inspections.includes(va.value.id) && - !this.previous_inspections.includes(va.value.id) - ) { - this.controller.request_inspect_object(va.value.id); - this.ongoing_inspections.push(va.value.id); - } + const scopes_to_check = this.all_scopes.filter((va) => va && va.value instanceof ObjectId); + for (const va of scopes_to_check) { + if ( + !this.ongoing_inspections.includes(va.value.id) && + !this.previous_inspections.includes(va.value.id) + ) { + this.controller.request_inspect_object(va.value.id); + this.ongoing_inspections.push(va.value.id); } } } diff --git a/src/debugger/godot4/debugger_variables.test.ts b/src/debugger/godot4/debugger_variables.test.ts new file mode 100644 index 0000000..c2c2a10 --- /dev/null +++ b/src/debugger/godot4/debugger_variables.test.ts @@ -0,0 +1,359 @@ +import { promises as fs } from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; +import { DebugProtocol } from "@vscode/debugprotocol"; +import chai from "chai"; +import chaiSubset from "chai-subset"; + +import { promisify } from "util"; +import { execFile } from "child_process"; +const execFileAsync = promisify(execFile); + +chai.use(chaiSubset); +const { expect } = chai; + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Given a path to a script, returns an object where each key is the name of a + * breakpoint (delimited by `breakpoint::`) and each value is the line number + * where the breakpoint appears in the script. + * + * @param scriptPath The path to the script to scan. + * @returns An object of breakpoint names to line numbers. + */ +async function getBreakpointLocations(scriptPath: string): Promise<{ [key: string]: vscode.Location }> { + const script_content = await fs.readFile(scriptPath, "utf-8"); + const breakpoints: { [key: string]: vscode.Location } = {}; + const breakpointRegex = /\b(breakpoint::.*)\b/g; + let match: RegExpExecArray | null; + while ((match = breakpointRegex.exec(script_content)) !== null) { + const breakpointName = match[1]; + const line = match.index ? script_content.substring(0, match.index).split("\n").length : 1; + breakpoints[breakpointName] = new vscode.Location(vscode.Uri.file(scriptPath), new vscode.Position(line - 1, 0)); + } + return breakpoints; +} + +async function waitForActiveStackItemChange(ms: number = 10000): Promise { + const res = await new Promise((resolve, reject) => { + const debugListener = vscode.debug.onDidChangeActiveStackItem((event) => { + debugListener.dispose(); + resolve(vscode.debug.activeStackItem); + }); + + // Timeout fallback in case stack item never changes + setTimeout(() => { + debugListener.dispose(); + console.warn(); + reject(new Error(`The ActiveStackItem eventwas not changed within the timeout period of '${ms}'`)); + }, ms); + }); + + return res; +} + +async function getStackFrames(threadId: number = 1): Promise { + // Ensure there is an active debug session + if (!vscode.debug.activeDebugSession) { + throw new Error("No active debug session found"); + } + + // corresponds to file://./debug_session.ts stackTraceRequest(...) + const stackTraceResponse = await vscode.debug.activeDebugSession.customRequest("stackTrace", { + threadId: threadId, + }); + + // Extract and return the stack frames + return stackTraceResponse.stackFrames || []; +} + +async function waitForBreakpoint(breakpoint: vscode.SourceBreakpoint, timeoutMs: number, ctx?: Mocha.Context): Promise { + const t0 = performance.now(); + console.log(fmt(`Waiting for breakpoint ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}`)); + const res = await waitForActiveStackItemChange(timeoutMs); + const t1 = performance.now(); + console.log(fmt(`Waiting for breakpoint completed ${breakpoint.location.uri.path}:${breakpoint.location.range.start.line}, enabled: ${breakpoint.enabled}, took ${t1 - t0}ms`)); + const stackFrames = await getStackFrames(); + if (stackFrames[0].source.path !== breakpoint.location.uri.fsPath || stackFrames[0].line != breakpoint.location.range.start.line+1) { + throw new Error(`Wrong breakpoint was hit. Expected: ${breakpoint.location.uri.fsPath}:${breakpoint.location.range.start.line+1}, Got: ${stackFrames[0].source.path}:${stackFrames[0].line}`); + } +} + +enum VariableScope { + Locals = 1, + Members = 2, + Globals = 3 +} + +async function getVariablesForScope(scope: VariableScope): Promise { + // corresponds to file://./debug_session.ts protected async variablesRequest + const variablesResponse = await vscode.debug.activeDebugSession?.customRequest("variables", { + variablesReference: scope + }); + return variablesResponse?.variables || []; +} + +async function evaluateRequest(scope: VariableScope, expression: string, context = "watch", frameId = 0): Promise { + // corresponds to file://./debug_session.ts protected async evaluateRequest + const evaluateResponse: DebugProtocol.EvaluateResponse = await vscode.debug.activeDebugSession?.customRequest("evaluate", { + context, + expression, + frameId + }); + return evaluateResponse.body; +} + +function formatMs(ms: number): string { + const seconds = Math.floor((ms / 1000) % 60); + const minutes = Math.floor((ms / (1000 * 60)) % 60); + return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}.${(Math.round(ms) % 1000).toString().padStart(3, "0")}`; +} + +function formatMessage(this: Mocha.Context, msg: string): string { + return `[${formatMs(performance.now()-this.testStart)}] ${msg}`; +} + +var fmt: (msg: string) => string; // formatMessage bound to Mocha.Context + +async function startDebugging(scene: "ScopeVars.tscn" | "ExtensiveVars.tscn" | "BuiltInTypes.tscn" = "ScopeVars.tscn"): Promise { + const t0 = performance.now(); + const debugConfig: vscode.DebugConfiguration = { + type: "godot", + request: "launch", + name: "Godot Debug", + scene: scene, + additional_options: "--headless" + }; + console.log(fmt(`Starting debugger for scene ${scene}`)); + const res = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], debugConfig); + const t1 = performance.now(); + console.log(fmt(`Starting debugger for scene ${scene} completed, took ${t1 - t0}ms`)); + if (!res) { + throw new Error(`Failed to start debugging for scene ${scene}`); + } +} + +suite("DAP Integration Tests - Variable Scopes", () => { + // workspaceFolder should match `.vscode-test.js`::workspaceFolder + const workspaceFolder = path.resolve(__dirname, "../../../test_projects/test-dap-project-godot4"); + + suiteSetup(async function() { + this.timeout(20000); // enough time to do `godot --import` + console.log("Environment Variables:"); + for (const [key, value] of Object.entries(process.env)) { + console.log(`${key}: ${value}`); + } + + // init the godot project by importing it in godot engine: + const config = vscode.workspace.getConfiguration("godotTools"); + var godot4_path = config.get("editorPath.godot4"); + // get the path for currently opened project in vscode test instance: + var project_path = vscode.workspace.workspaceFolders?.[0].uri.fsPath; + console.log("Executing", [godot4_path, "--headless", "--import", project_path]); + const exec_res = await execFileAsync(godot4_path, ["--headless", "--import", project_path], {shell: true, cwd: project_path}); + if (exec_res.stderr !== "") { + throw new Error(exec_res.stderr); + } + console.log(exec_res.stdout); + }); + + setup(async function() { + console.log(`➤ Test '${this?.currentTest.title}' starting`); + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + if (vscode.debug.breakpoints) { + await vscode.debug.removeBreakpoints(vscode.debug.breakpoints); + } + this.testStart = performance.now(); + fmt = formatMessage.bind(this); + }); + + + teardown(async function() { + await sleep(1000); + if (vscode.debug.activeDebugSession !== undefined) { + console.log("Closing debug session"); + await vscode.debug.stopDebugging(); + } + console.log(`⬛ Test '${this.currentTest.title}' result: ${this.currentTest.state}, duration: ${performance.now() - this.testStart}ms`); + }); + + + test("sample test", async function() { + // await sleep(1000); + await startDebugging("ScopeVars.tscn"); + }); + + + test("should return correct scopes", async function() { + const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd")); + const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]); + vscode.debug.addBreakpoints([breakpoint]); + + await startDebugging("ScopeVars.tscn"); + await waitForBreakpoint(breakpoint, 2000); + + // TODO: current DAP needs a delay before it will return variables + console.log("Sleeping for 2 seconds"); + await sleep(2000); + + // corresponds to file://./debug_session.ts async scopesRequest + const res_scopes = await vscode.debug.activeDebugSession.customRequest("scopes", {frameId: 1}); + + expect(res_scopes).to.exist; + expect(res_scopes.scopes).to.exist; + + const scopes = res_scopes.scopes; + expect(scopes.length).to.equal(3, "Expected 3 scopes"); + expect(scopes[0].name).to.equal(VariableScope[VariableScope.Locals], "Expected Locals scope"); + expect(scopes[0].variablesReference).to.equal(VariableScope.Locals, "Expected Locals variablesReference"); + expect(scopes[1].name).to.equal(VariableScope[VariableScope.Members], "Expected Members scope"); + expect(scopes[1].variablesReference).to.equal(VariableScope.Members, "Expected Members variablesReference"); + expect(scopes[2].name).to.equal(VariableScope[VariableScope.Globals], "Expected Globals scope"); + expect(scopes[2].variablesReference).to.equal(VariableScope.Globals, "Expected Globals variablesReference"); + + await sleep(1000); + await vscode.debug.stopDebugging(); + })?.timeout(5000); + + test("should return global variables", async function() { + const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd")); + const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]); + vscode.debug.addBreakpoints([breakpoint]); + + await startDebugging("ScopeVars.tscn"); + await waitForBreakpoint(breakpoint, 2000); + + // TODO: current DAP needs a delay before it will return variables + console.log("Sleeping for 2 seconds"); + await sleep(2000); + + const variables = await getVariablesForScope(VariableScope.Globals); + expect(variables).to.containSubset([{name: "GlobalScript"}]); + + await sleep(1000); + await vscode.debug.stopDebugging(); + })?.timeout(7000); + + test("should return local variables", async function() { + const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd")); + const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]); + vscode.debug.addBreakpoints([breakpoint]); + + await startDebugging("ScopeVars.tscn"); + await waitForBreakpoint(breakpoint, 2000); + + // TODO: current DAP needs a delay before it will return variables + console.log("Sleeping for 2 seconds"); + await sleep(2000); + + const variables = await getVariablesForScope(VariableScope.Locals); + expect(variables.length).to.equal(2); + expect(variables).to.containSubset([{name: "local1"}]); + expect(variables).to.containSubset([{name: "local2"}]); + + await sleep(1000); + await vscode.debug.stopDebugging(); + })?.timeout(5000); + + test("should return member variables", async function() { + const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ScopeVars.gd")); + const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ScopeVars::_ready"]); + vscode.debug.addBreakpoints([breakpoint]); + + await startDebugging("ScopeVars.tscn"); + await waitForBreakpoint(breakpoint, 2000); + + // TODO: current DAP needs a delay before it will return variables + console.log("Sleeping for 2 seconds"); + await sleep(2000); + + const variables = await getVariablesForScope(VariableScope.Members); + expect(variables.length).to.equal(2); + expect(variables).to.containSubset([{name: "self"}]); + expect(variables).to.containSubset([{name: "member1"}]); + + await sleep(1000); + await vscode.debug.stopDebugging(); + })?.timeout(5000); + + test("should retrieve all built-in types correctly", async function() { + const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "BuiltInTypes.gd")); + const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::BuiltInTypes::_ready"]); + vscode.debug.addBreakpoints([breakpoint]); + + await startDebugging("BuiltInTypes.tscn"); + await waitForBreakpoint(breakpoint, 2000); + + // TODO: current DAP needs a delay before it will return variables + console.log("Sleeping for 2 seconds"); + await sleep(2000); + + const variables = await getVariablesForScope(VariableScope.Locals); + + expect(variables).to.containSubset([{ name: "int_var", value: "42" }]); + expect(variables).to.containSubset([{ name: "float_var", value: "3.14" }]); + expect(variables).to.containSubset([{ name: "bool_var", value: "true" }]); + expect(variables).to.containSubset([{ name: "string_var", value: "Hello, Godot!" }]); + expect(variables).to.containSubset([{ name: "nil_var", value: "null" }]); + expect(variables).to.containSubset([{ name: "vector2", value: "Vector2(10, 20)" }]); + expect(variables).to.containSubset([{ name: "vector3", value: "Vector3(1, 2, 3)" }]); + expect(variables).to.containSubset([{ name: "rect2", value: "Rect2((0, 0) - (100, 50))" }]); + expect(variables).to.containSubset([{ name: "quaternion", value: "Quat(0, 0, 0, 1)" }]); + // expect(variables).to.containSubset([{ name: "simple_array", value: "[1, 2, 3]" }]); + expect(variables).to.containSubset([{ name: "simple_array", value: "Array[3]" }]); + // expect(variables).to.containSubset([{ name: "nested_dict.nested_key", value: `"Nested Value"` }]); + // expect(variables).to.containSubset([{ name: "nested_dict.sub_dict.sub_key", value: "99" }]); + expect(variables).to.containSubset([{ name: "nested_dict", value: "Dictionary[2]" }]); + // expect(variables).to.containSubset([{ name: "byte_array", value: "[0, 1, 2, 255]" }]); + expect(variables).to.containSubset([{ name: "byte_array", value: "Array[4]" }]); + // expect(variables).to.containSubset([{ name: "int32_array", value: "[100, 200, 300]" }]); + expect(variables).to.containSubset([{ name: "int32_array", value: "Array[3]" }]); + expect(variables).to.containSubset([{ name: "color_var", value: "Color(1, 0, 0, 1)" }]); + expect(variables).to.containSubset([{ name: "aabb_var", value: "AABB((0, 0, 0), (1, 1, 1))" }]); + expect(variables).to.containSubset([{ name: "plane_var", value: "Plane(0, 1, 0, -5)" }]); + expect(variables).to.containSubset([{ name: "callable_var", value: "Callable()" }]); + expect(variables).to.containSubset([{ name: "signal_var" }]); + const signal_var = variables.find(v => v.name === "signal_var"); + expect(signal_var.value).to.match(/Signal\(member_signal\, <\d+>\)/, "Should be in format of 'Signal(member_signal, <28236055815>)'"); + + await sleep(1000); + await vscode.debug.stopDebugging(); + })?.timeout(5000); + + test("should retrieve all complex variables correctly", async function() { + const breakpointLocations = await getBreakpointLocations(path.join(workspaceFolder, "ExtensiveVars.gd")); + const breakpoint = new vscode.SourceBreakpoint(breakpointLocations["breakpoint::ExtensiveVars::_ready"]); + vscode.debug.addBreakpoints([breakpoint]); + + await startDebugging("ExtensiveVars.tscn"); + await waitForBreakpoint(breakpoint, 2000); + + // TODO: current DAP needs a delay before it will return variables + console.log("Sleeping for 2 seconds"); + await sleep(2000); + + const memberVariables = await getVariablesForScope(VariableScope.Members); + + expect(memberVariables.length).to.equal(3); + expect(memberVariables).to.containSubset([{name: "self"}]); + expect(memberVariables).to.containSubset([{name: "self_var"}]); + const self = memberVariables.find(v => v.name === "self"); + const self_var = memberVariables.find(v => v.name === "self_var"); + expect(self.value).to.deep.equal(self_var.value); + + const localVariables = await getVariablesForScope(VariableScope.Locals); + expect(localVariables.length).to.equal(4); + expect(localVariables).to.containSubset([ + { name: "local_label", value: "Label" }, + { name: "local_self_var_through_label", value: "Node2D" }, + { name: "local_classA", value: "RefCounted" }, + { name: "local_classB", value: "RefCounted" } + ]); + + await sleep(1000); + await vscode.debug.stopDebugging(); + })?.timeout(15000); +}); diff --git a/src/debugger/godot4/helpers.ts b/src/debugger/godot4/helpers.ts index 066a985..dea7cd7 100644 --- a/src/debugger/godot4/helpers.ts +++ b/src/debugger/godot4/helpers.ts @@ -36,38 +36,40 @@ export function is_variable_built_in_type(va: GodotVariable) { return ["number", "bigint", "boolean", "string"].some(x => x == type); } -export function build_sub_values(va: GodotVariable) { - const value = va.value; - +export function get_sub_values(value: any) { let subValues: GodotVariable[] = undefined; - if (value && Array.isArray(value)) { - subValues = value.map((va, i) => { - return { name: `${i}`, value: va } as GodotVariable; - }); - } else if (value instanceof Map) { - subValues = Array.from(value.keys()).map((va) => { - if (typeof va["stringify_value"] === "function") { - return { - name: `${va.type_name()}${va.stringify_value()}`, - value: value.get(va), - } as GodotVariable; - } else { - return { - name: `${va}`, - value: value.get(va), - } as GodotVariable; - } - }); - } else if (value && typeof value["sub_values"] === "function") { - subValues = value.sub_values().map((sva) => { - return { name: sva.name, value: sva.value } as GodotVariable; - }); + if (value) { + if (Array.isArray(value)) { + subValues = value.map((va, i) => { + return { name: `${i}`, value: va } as GodotVariable; + }); + } else if (value instanceof Map) { + subValues = Array.from(value.keys()).map((va) => { + if (typeof va["stringify_value"] === "function") { + return { + name: `${va.type_name()}${va.stringify_value()}`, + value: value.get(va), + } as GodotVariable; + } else { + return { + name: `${va}`, + value: value.get(va), + } as GodotVariable; + } + }); + } else if (typeof value["sub_values"] === "function") { + subValues = value.sub_values()?.map((sva) => { + return { name: sva.name, value: sva.value } as GodotVariable; + }); + } } - va.sub_values = subValues; + for (let i = 0; i < subValues?.length; i++) { + subValues[i].sub_values = get_sub_values(subValues[i].value); + } - subValues?.forEach(build_sub_values); + return subValues; } export function parse_variable(va: GodotVariable, i?: number) { @@ -103,7 +105,11 @@ export function parse_variable(va: GodotVariable, i?: number) { array_type = "named"; reference = i ? i : 0; } else { - rendered_value = `${value.type_name()}${value.stringify_value()}`; + try { + rendered_value = `${value.type_name()}${value.stringify_value()}`; + } catch (e) { + rendered_value = `${value}`; + } reference = i ? i : 0; } } diff --git a/src/debugger/godot4/server_controller.ts b/src/debugger/godot4/server_controller.ts index 4eff0fa..0fc2997 100644 --- a/src/debugger/godot4/server_controller.ts +++ b/src/debugger/godot4/server_controller.ts @@ -19,7 +19,7 @@ import { killSubProcesses, subProcess } from "../../utils/subspawn"; import { GodotStackFrame, GodotStackVars, GodotVariable } from "../debug_runtime"; import { AttachRequestArguments, LaunchRequestArguments, pinnedScene } from "../debugger"; import { GodotDebugSession } from "./debug_session"; -import { build_sub_values, parse_next_scene_node, split_buffers } from "./helpers"; +import { get_sub_values, parse_next_scene_node, split_buffers } from "./helpers"; import { VariantDecoder } from "./variables/variant_decoder"; import { VariantEncoder } from "./variables/variant_encoder"; import { RawObject } from "./variables/variants"; @@ -395,13 +395,15 @@ export class ServerController { for (const prop of properties) { rawObject.set(prop[0], prop[5]); } - const inspectedVariable = { name: "", value: rawObject }; - build_sub_values(inspectedVariable); - if (this.session.inspect_callbacks.has(BigInt(id))) { - this.session.inspect_callbacks.get(BigInt(id))(inspectedVariable.name, inspectedVariable); + const sub_values = get_sub_values(rawObject); + + const inspect_callback = this.session.inspect_callbacks.get(BigInt(id)); + if (inspect_callback !== undefined) { + const inspectedVariable = { name: "", value: rawObject, sub_values: sub_values } as GodotVariable; + inspect_callback(inspectedVariable.name, inspectedVariable); this.session.inspect_callbacks.delete(BigInt(id)); } - this.session.set_inspection(id, inspectedVariable); + this.session.set_inspection(id, rawObject, sub_values); break; } case "stack_dump": { @@ -641,8 +643,8 @@ export class ServerController { throw new Error("More stack frame variables were sent than expected."); } - const variable: GodotVariable = { name, value, type }; - build_sub_values(variable); + const sub_values = get_sub_values(value); + const variable = { name, value, type, sub_values } as GodotVariable; const scopeName = ["locals", "members", "globals"][scope]; this.partialStackVars[scopeName].push(variable); diff --git a/src/debugger/godot4/variables/variants.ts b/src/debugger/godot4/variables/variants.ts index e222085..1cb3da1 100644 --- a/src/debugger/godot4/variables/variants.ts +++ b/src/debugger/godot4/variables/variants.ts @@ -471,7 +471,7 @@ export class Signal implements GDObject { constructor(public name: string, public oid: ObjectId) {} public stringify_value(): string { - return `${this.name}() ${this.oid.stringify_value()}`; + return `(${this.name}, ${this.oid.stringify_value()})`; } public sub_values(): GodotVariable[] { diff --git a/src/debugger/scene_tree_provider.ts b/src/debugger/scene_tree_provider.ts index b09052a..330127e 100644 --- a/src/debugger/scene_tree_provider.ts +++ b/src/debugger/scene_tree_provider.ts @@ -5,6 +5,7 @@ import { ProviderResult, TreeItem, TreeItemCollapsibleState, + Uri } from "vscode"; import path = require("path"); import { get_extension_uri } from "../utils"; @@ -81,8 +82,8 @@ export class SceneNode extends TreeItem { const iconName = class_name + ".svg"; this.iconPath = { - light: path.join(iconDir, "light", iconName), - dark: path.join(iconDir, "dark", iconName), + light: Uri.file(path.join(iconDir, "light", iconName)), + dark: Uri.file(path.join(iconDir, "dark", iconName)), }; } } diff --git a/src/formatter/formatter.test.ts b/src/formatter/formatter.test.ts index 12087c6..801ed1b 100644 --- a/src/formatter/formatter.test.ts +++ b/src/formatter/formatter.test.ts @@ -138,6 +138,10 @@ function parse_test_file(content: string): Test[] { suite("GDScript Formatter Tests", () => { const testFiles = fs.readdirSync(snapshotsFolderPath, { withFileTypes: true, recursive: true }); + teardown(async () => { + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + }); + for (const file of testFiles.filter((f) => f.isFile())) { if (["in.gd", "out.gd"].includes(file.name) || !file.name.endsWith(".gd")) { continue; diff --git a/src/scene_tools/types.ts b/src/scene_tools/types.ts index c43c415..39d568f 100644 --- a/src/scene_tools/types.ts +++ b/src/scene_tools/types.ts @@ -2,6 +2,7 @@ import { TreeItem, TreeItemCollapsibleState, MarkdownString, + Uri } from "vscode"; import * as path from "path"; import { get_extension_uri } from "../utils"; @@ -31,8 +32,8 @@ export class SceneNode extends TreeItem { const iconName = className + ".svg"; this.iconPath = { - light: path.join(iconDir, "light", iconName), - dark: path.join(iconDir, "dark", iconName), + light: Uri.file(path.join(iconDir, "light", iconName)), + dark: Uri.file(path.join(iconDir, "dark", iconName)), }; } diff --git a/test_projects/test-dap-project-godot4/.vscode/launch.json b/test_projects/test-dap-project-godot4/.vscode/launch.json new file mode 100644 index 0000000..debd1cb --- /dev/null +++ b/test_projects/test-dap-project-godot4/.vscode/launch.json @@ -0,0 +1,40 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "GDScript: Launch ScopeVars.tscn", + "type": "godot", + "request": "launch", + "project": "${workspaceFolder}", + "scene": "ScopeVars.tscn" + // "debug_collisions": false, + // "debug_paths": false, + // "debug_navigation": false, + // "additional_options": "" + }, + { + "name": "GDScript: Launch ExtensiveVars.tscn", + "type": "godot", + "request": "launch", + "project": "${workspaceFolder}", + "scene": "ExtensiveVars.tscn" + }, + { + "name": "GDScript: Launch BuiltInTypes.tscn", + "type": "godot", + "request": "launch", + "project": "${workspaceFolder}", + "scene": "BuiltInTypes.tscn" + }, + { + "name": "GDScript: Launch NodeVars.tscn", + "type": "godot", + "request": "launch", + "project": "${workspaceFolder}", + "scene": "NodeVars.tscn" + } + ] +} \ No newline at end of file diff --git a/test_projects/test-dap-project-godot4/BuiltInTypes.gd b/test_projects/test-dap-project-godot4/BuiltInTypes.gd new file mode 100644 index 0000000..47b2124 --- /dev/null +++ b/test_projects/test-dap-project-godot4/BuiltInTypes.gd @@ -0,0 +1,39 @@ +extends Node + +signal member_signal +signal member_signal_with_parameters(my_param1: String) + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + var int_var = 42 + var float_var = 3.14 + var bool_var = true + var string_var = "Hello, Godot!" + var nil_var = null + var vector2 = Vector2(10, 20) + var vector3 = Vector3(1, 2, 3) + var rect2 = Rect2(0, 0, 100, 50) + var quaternion = Quaternion(0, 0, 0, 1) + var simple_array = [1, 2, 3] + var nested_dict = { + "nested_key": "Nested Value", + "sub_dict": {"sub_key": 99} + } + var byte_array = PackedByteArray([0, 1, 2, 255]) + var int32_array = PackedInt32Array([100, 200, 300]) + var color_var = Color(1, 0, 0, 1) # Red color + var aabb_var = AABB(Vector3(0, 0, 0), Vector3(1, 1, 1)) + var plane_var = Plane(Vector3(0, 1, 0), -5) + + var callable_var = self.my_callable_func + + var signal_var = member_signal + member_signal.connect(singal_connected_func) + + print("breakpoint::BuiltInTypes::_ready") + +func my_callable_func(): + pass + +func singal_connected_func(): + pass diff --git a/test_projects/test-dap-project-godot4/BuiltInTypes.tscn b/test_projects/test-dap-project-godot4/BuiltInTypes.tscn new file mode 100644 index 0000000..806d3f7 --- /dev/null +++ b/test_projects/test-dap-project-godot4/BuiltInTypes.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3 uid="uid://d0ovhv6f38jj4"] + +[ext_resource type="Script" path="res://BuiltInTypes.gd" id="1_2dpge"] + +[node name="BuiltInTypes" type="Node"] +script = ExtResource("1_2dpge") + +[node name="Label" type="Label" parent="."] +offset_right = 40.0 +offset_bottom = 23.0 +text = "Built-in types" diff --git a/test_projects/test-dap-project-godot4/ExtensiveVars.gd b/test_projects/test-dap-project-godot4/ExtensiveVars.gd new file mode 100644 index 0000000..59e8e7e --- /dev/null +++ b/test_projects/test-dap-project-godot4/ExtensiveVars.gd @@ -0,0 +1,38 @@ +extends Node2D + +var self_var := self +@onready var label: ExtensiveVars_Label = $Label + +class ClassA: + var member_classB + var member_self := self + +class ClassB: + var member_classA + +func _ready() -> void: + var local_label := label + var local_self_var_through_label := label.parent_var + + var local_classA = ClassA.new() + var local_classB = ClassB.new() + local_classA.member_classB = local_classB + local_classB.member_classA = local_classA + + # Circular reference. + # Note: that causes the godot engine to omit this variable, since stack_frame_var cannot be completed and sent + # https://github.com/godotengine/godot/issues/76019 + # var dict = {} + # dict["self_ref"] = dict + + print("breakpoint::ExtensiveVars::_ready") + +func _process(delta: float) -> void: + var local_label := label + var local_self_var_through_label := label.parent_var + + var local_classA = ClassA.new() + var local_classB = ClassB.new() + local_classA.member_classB = local_classB + local_classB.member_classA = local_classA + pass diff --git a/test_projects/test-dap-project-godot4/ExtensiveVars.tscn b/test_projects/test-dap-project-godot4/ExtensiveVars.tscn new file mode 100644 index 0000000..9d74c2d --- /dev/null +++ b/test_projects/test-dap-project-godot4/ExtensiveVars.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=3 format=3 uid="uid://bsonfthpqa3dx"] + +[ext_resource type="Script" path="res://ExtensiveVars.gd" id="1_fnilr"] +[ext_resource type="Script" path="res://ExtensiveVars_Label.gd" id="2_jijf2"] + +[node name="ExtensiveVars" type="Node2D"] +script = ExtResource("1_fnilr") + +[node name="Label" type="Label" parent="."] +text = "Extensive Vars scene" +script = ExtResource("2_jijf2") +metadata/_edit_use_anchors_ = true diff --git a/test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd b/test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd new file mode 100644 index 0000000..485bf64 --- /dev/null +++ b/test_projects/test-dap-project-godot4/ExtensiveVars_Label.gd @@ -0,0 +1,14 @@ +extends Label + +class_name ExtensiveVars_Label + +@onready var parent_var: Node2D = $".." + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + pass diff --git a/test_projects/test-dap-project-godot4/GlobalScript.gd b/test_projects/test-dap-project-godot4/GlobalScript.gd new file mode 100644 index 0000000..432237c --- /dev/null +++ b/test_projects/test-dap-project-godot4/GlobalScript.gd @@ -0,0 +1,3 @@ +extends Node + +var globalMember := "global member" diff --git a/test_projects/test-dap-project-godot4/Node1.gd b/test_projects/test-dap-project-godot4/Node1.gd new file mode 100644 index 0000000..6c33416 --- /dev/null +++ b/test_projects/test-dap-project-godot4/Node1.gd @@ -0,0 +1,8 @@ +extends Node + +@onready var parent_node: Node2D = $".." +@onready var sibling_node2: Node = $"../node2" + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. diff --git a/test_projects/test-dap-project-godot4/NodeVars.gd b/test_projects/test-dap-project-godot4/NodeVars.gd new file mode 100644 index 0000000..64cb3c3 --- /dev/null +++ b/test_projects/test-dap-project-godot4/NodeVars.gd @@ -0,0 +1,9 @@ +extends Node2D + +@onready var node_1: Node = $node1 +@onready var node_2: Node = $node2 + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + print("breakpoint::NodeVars::_ready") + pass diff --git a/test_projects/test-dap-project-godot4/NodeVars.tscn b/test_projects/test-dap-project-godot4/NodeVars.tscn new file mode 100644 index 0000000..4c5f4a2 --- /dev/null +++ b/test_projects/test-dap-project-godot4/NodeVars.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=3 format=3 uid="uid://xrjtth0d2nc5"] + +[ext_resource type="Script" path="res://NodeVars.gd" id="1_6eeca"] +[ext_resource type="Script" path="res://Node1.gd" id="2_bl41t"] + +[node name="NodeVars" type="Node2D"] +script = ExtResource("1_6eeca") + +[node name="node1" type="Node" parent="."] +script = ExtResource("2_bl41t") + +[node name="node2" type="Node" parent="."] + +[node name="Label" type="Label" parent="."] +offset_right = 40.0 +offset_bottom = 23.0 +text = "NodeVars" diff --git a/test_projects/test-dap-project-godot4/ScopeVars.gd b/test_projects/test-dap-project-godot4/ScopeVars.gd new file mode 100644 index 0000000..c2c7142 --- /dev/null +++ b/test_projects/test-dap-project-godot4/ScopeVars.gd @@ -0,0 +1,8 @@ +extends Node + +var member1 := TestClassA.new() + +func _ready() -> void: + var local1 := TestClassA.new() + var local2 = GlobalScript.globalMember + print("breakpoint::ScopeVars::_ready") diff --git a/test_projects/test-dap-project-godot4/ScopeVars.tscn b/test_projects/test-dap-project-godot4/ScopeVars.tscn new file mode 100644 index 0000000..b9be0ec --- /dev/null +++ b/test_projects/test-dap-project-godot4/ScopeVars.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3 uid="uid://g5gqewj2i2xs"] + +[ext_resource type="Script" path="res://ScopeVars.gd" id="1_wtcpp"] + +[node name="RootNode" type="Node"] +script = ExtResource("1_wtcpp") + +[node name="Label" type="Label" parent="."] +offset_right = 40.0 +offset_bottom = 23.0 +text = "Godot test project" diff --git a/test_projects/test-dap-project-godot4/TestClassA.gd b/test_projects/test-dap-project-godot4/TestClassA.gd new file mode 100644 index 0000000..7b9976d --- /dev/null +++ b/test_projects/test-dap-project-godot4/TestClassA.gd @@ -0,0 +1,5 @@ +class_name TestClassA + +var testclassa_member1 := "member1" + +var testclassa_member2: Node diff --git a/test_projects/test-dap-project-godot4/project.godot b/test_projects/test-dap-project-godot4/project.godot new file mode 100644 index 0000000..a4c2b29 --- /dev/null +++ b/test_projects/test-dap-project-godot4/project.godot @@ -0,0 +1,19 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Test DAP project godot4" +run/main_scene="res://ScopeVars.tscn" +config/features=PackedStringArray("4.3", "Forward Plus") + +[autoload] + +GlobalScript="*res://GlobalScript.gd" diff --git a/tsconfig.json b/tsconfig.json index 97fb729..9c92560 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "rootDir": "src", "strict": false, "skipLibCheck": true, - "allowJs": true + "allowJs": true, + "strictBindCallApply": true }, "exclude": [ "node_modules",