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
This commit is contained in:
MichaelXt
2025-02-10 13:56:13 -08:00
committed by GitHub
parent 7c70ac2753
commit 2490d0cdad
30 changed files with 854 additions and 160 deletions

View File

@@ -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:

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ node_modules
.vscode-test
workspace.code-workspace
.history
.godot
*.tmp

View File

@@ -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',
}
);

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-vscode.extension-test-runner"
]
}

7
.vscode/launch.json vendored
View File

@@ -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"
}
},
]

207
package-lock.json generated
View File

@@ -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"

View File

@@ -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",

View File

@@ -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);
}
}
}

View File

@@ -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<vscode.DebugThread | vscode.DebugStackFrame | undefined> {
const res = await new Promise<vscode.DebugThread | vscode.DebugStackFrame | undefined>((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<DebugProtocol.StackFrame[]> {
// 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<void> {
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<DebugProtocol.Variable[]> {
// 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<any> {
// 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<void> {
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<string>("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);
});

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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[] {

View File

@@ -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)),
};
}
}

View File

@@ -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;

View File

@@ -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)),
};
}

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
extends Node
var globalMember := "global member"

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

View File

@@ -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")

View File

@@ -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"

View File

@@ -0,0 +1,5 @@
class_name TestClassA
var testclassa_member1 := "member1"
var testclassa_member2: Node

View File

@@ -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"

View File

@@ -14,7 +14,8 @@
"rootDir": "src",
"strict": false,
"skipLibCheck": true,
"allowJs": true
"allowJs": true,
"strictBindCallApply": true
},
"exclude": [
"node_modules",