diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 981a87f..3b3b695 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,8 +78,12 @@ jobs: if-no-files-found: error retention-days: 1 integration: - name: VS Code integration + name: VS Code integration (${{ matrix.version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: [stable, minimum] defaults: run: @@ -109,15 +113,16 @@ jobs: - name: Build server run: npm run build:server - - name: Run VS Code stable smoke test + - name: Run VS Code fixture tests env: JAVA_HOME: ${{ steps.java.outputs.path }} + VSCODE_TEST_VERSION: ${{ matrix.version }} run: xvfb-run -a npm test - name: Upload VS Code logs if: failure() uses: actions/upload-artifact@v4 with: - name: vscode-logs + name: vscode-logs-${{ matrix.version }} path: client/.vscode-test/user-data/logs if-no-files-found: ignore diff --git a/client/.vscode-test.mjs b/client/.vscode-test.mjs index 7a6b0a3..fad26a8 100644 --- a/client/.vscode-test.mjs +++ b/client/.vscode-test.mjs @@ -1,10 +1,18 @@ +import { readFileSync } from 'node:fs'; import { defineConfig } from '@vscode/test-cli'; +import { minVersion } from 'semver'; -export default defineConfig({ - files: 'out/test/**/*.test.js', - version: 'stable', - workspaceFolder: 'test-fixtures/failing', +const { engines } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); +const version = process.env.VSCODE_TEST_VERSION === 'minimum' + ? minVersion(engines.vscode).version + : 'stable'; + +export default defineConfig(['failing', 'passing'].map(fixture => ({ + label: fixture, + files: fixture === 'failing' ? 'out/test/**/*.test.js' : 'out/test/smoke.test.js', + version, + workspaceFolder: `test-fixtures/${fixture}`, extensionDevelopmentPath: '.', launchArgs: ['--disable-extensions', '--disable-workspace-trust'], mocha: { ui: 'tdd', timeout: 120_000 }, -}); +}))); diff --git a/client/package-lock.json b/client/package-lock.json index 89966eb..24ffa70 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,7 +16,7 @@ "@types/jsdom": "^30.0.0", "@types/mocha": "^10.0.10", "@types/node": "^24.7.2", - "@types/vscode": "^1.82.0", + "@types/vscode": "~1.82.0", "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.0.0", @@ -24,6 +24,7 @@ "jsdom": "^30.1.1", "jsonc-parser": "^0.4.2", "minimist": "^1.2.7", + "semver": "^7.7.4", "ts-loader": "^9.5.4", "typescript": "~5.3.0", "typescript-eslint": "^8.0.0", @@ -1670,9 +1671,9 @@ "license": "MIT" }, "node_modules/@types/vscode": { - "version": "1.105.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.105.0.tgz", - "integrity": "sha512-Lotk3CTFlGZN8ray4VxJE7axIyLZZETQJVWi/lYoUVQuqfRxlQhVOfoejsD2V3dVXPSbS15ov5ZyowMAzgUqcw==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz", + "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==", "dev": true, "license": "MIT" }, @@ -11024,9 +11025,9 @@ "dev": true }, "@types/vscode": { - "version": "1.105.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.105.0.tgz", - "integrity": "sha512-Lotk3CTFlGZN8ray4VxJE7axIyLZZETQJVWi/lYoUVQuqfRxlQhVOfoejsD2V3dVXPSbS15ov5ZyowMAzgUqcw==", + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz", + "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==", "dev": true }, "@typescript-eslint/eslint-plugin": { diff --git a/client/package.json b/client/package.json index b08e611..b5891b5 100644 --- a/client/package.json +++ b/client/package.json @@ -172,7 +172,7 @@ "@types/jsdom": "^30.0.0", "@types/mocha": "^10.0.10", "@types/node": "^24.7.2", - "@types/vscode": "^1.82.0", + "@types/vscode": "~1.82.0", "@vscode/test-cli": "^0.0.15", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.0.0", @@ -180,6 +180,7 @@ "jsdom": "^30.1.1", "jsonc-parser": "^0.4.2", "minimist": "^1.2.7", + "semver": "^7.7.4", "ts-loader": "^9.5.4", "typescript": "~5.3.0", "typescript-eslint": "^8.0.0", diff --git a/client/src/test/smoke.test.ts b/client/src/test/smoke.test.ts index 19f841b..9a35ead 100644 --- a/client/src/test/smoke.test.ts +++ b/client/src/test/smoke.test.ts @@ -5,7 +5,7 @@ import type { LJDiagnostic } from '../types/diagnostics'; import type { LiquidJavaTestApi } from '../types/test-api'; suite('Bundled LiquidJava extension', () => { - test('activates, becomes ready, and verifies the failing fixture', async () => { + test('activates, becomes ready, and verifies its isolated fixture', async () => { const installed = vscode.extensions.getExtension('AlcidesFonseca.liquid-java'); assert.ok(installed, 'LiquidJava must be loaded in the Extension Host'); assert.equal(installed.packageJSON.main, './dist/extension.js'); @@ -16,17 +16,25 @@ suite('Bundled LiquidJava extension', () => { assert.notEqual(api.getState().status, 'stopped'); const workspace = vscode.workspace.workspaceFolders?.[0]; - assert.ok(workspace, 'the failing fixture must have its own workspace'); - const uri = vscode.Uri.joinPath(workspace.uri, 'src/main/java/FailingRefinement.java'); + assert.ok(workspace, 'the fixture must have its own workspace'); + const passing = path.basename(workspace.uri.fsPath) === 'passing'; + const file = passing ? 'PassingRefinement.java' : 'FailingRefinement.java'; + const uri = vscode.Uri.joinPath(workspace.uri, `src/main/java/${file}`); const subscriptions: vscode.Disposable[] = []; - const nextFixtureDiagnostics = () => new Promise((resolve) => { - const subscription = api.onDiagnostics((diagnostics) => { - if (diagnostics.some(d => d.type === 'refinement-error' && path.resolve(d.file) === uri.fsPath)) { - subscription.dispose(); - resolve(diagnostics); - } + const nextFixtureDiagnostics = () => new Promise((resolve, reject) => { + const dispose = () => { + diagnosticsSubscription.dispose(); + failureSubscription.dispose(); + }; + const diagnosticsSubscription = api.onDiagnostics((diagnostics) => { + dispose(); + resolve(diagnostics); }); - subscriptions.push(subscription); + const failureSubscription = api.onFailure(() => { + dispose(); + reject(new Error(`LiquidJava verifier crashed while checking ${file} (status: ${api.getState().status})`)); + }); + subscriptions.push(diagnosticsSubscription, failureSubscription); }); try { // settle automatic verification before testing the manual command @@ -38,12 +46,17 @@ suite('Bundled LiquidJava extension', () => { const manualDiagnostics = nextFixtureDiagnostics(); await vscode.commands.executeCommand('liquidjava.verify'); const diagnostics = await manualDiagnostics; - const error = diagnostics.find(d => d.type === 'refinement-error' && path.resolve(d.file) === uri.fsPath); - assert.ok(error); - assert.equal(error.category, 'error'); - assert.equal(error.title, 'Refinement Error'); - assert.ok(error.position, 'the diagnostic must identify the invalid assignment'); - assert.equal(api.getState().status, 'failed'); + if (passing) { + assert.deepEqual(diagnostics, [], 'correct code must emit an explicit empty result'); + assert.equal(api.getState().status, 'passed'); + } else { + const error = diagnostics.find(d => d.type === 'refinement-error' && vscode.Uri.file(path.resolve(d.file)).fsPath === uri.fsPath); + assert.ok(error, `expected a refinement error for ${file}; received ${JSON.stringify(diagnostics)}`); + assert.equal(error.category, 'error'); + assert.equal(error.title, 'Refinement Error'); + assert.ok(error.position, 'the diagnostic must identify the invalid assignment'); + assert.equal(api.getState().status, 'failed'); + } assert.deepEqual(api.getState().diagnostics, diagnostics); } finally { subscriptions.forEach(subscription => subscription.dispose()); diff --git a/client/test-fixtures/passing/src/main/java/PassingRefinement.java b/client/test-fixtures/passing/src/main/java/PassingRefinement.java new file mode 100644 index 0000000..4228fd3 --- /dev/null +++ b/client/test-fixtures/passing/src/main/java/PassingRefinement.java @@ -0,0 +1,6 @@ +import liquidjava.specification.Refinement; + +public class PassingRefinement { + @Refinement("_ > 0") + int positive = 1; +}