diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3958606c..23226a4d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Latte Extension Dev", - "image": "mcr.microsoft.com/devcontainers/javascript-node:20", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22", "customizations": { "vscode": { "extensions": [ @@ -16,4 +16,3 @@ }, "postCreateCommand": "npm install" } - \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 225cb0d9..f4995301 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Java uses: actions/setup-java@v4 with: - java-version: 20 + java-version: 21 distribution: temurin - name: Setup Node.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6f0c39f..b116ada8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,6 @@ permissions: jobs: checks: name: Checks - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-latest defaults: @@ -66,9 +65,13 @@ jobs: grep -Fx 'extension/media/webview.js' /tmp/liquid-java-vsix-files.txt integration: - name: VS Code integration - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + name: VS Code integration (${{ matrix.version }}) + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + version: ${{ (github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')) && fromJSON('["stable", "minimum"]') || fromJSON('["stable"]') }} defaults: run: @@ -98,15 +101,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 7a6b0a39..fad26a89 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 74e5fa05..149cac54 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,13 +15,14 @@ "@eslint/js": "^9.39.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", "eslint": "^9.39.0", "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", @@ -30,6 +31,7 @@ "webpack-cli": "^5.1.4" }, "engines": { + "node": ">=22", "vscode": "^1.82.0" } }, @@ -1082,9 +1084,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" }, @@ -9126,9 +9128,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 a4fbcce4..02ec2853 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ }, "homepage": "https://liquid-java.github.io", "engines": { + "node": ">=22", "vscode": "^1.82.0" }, "languages": [ @@ -168,13 +169,14 @@ "@eslint/js": "^9.39.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", "eslint": "^9.39.0", "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/extension.ts b/client/src/extension.ts index b3e52e0e..a6493cc7 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -19,7 +19,7 @@ import type { LiquidJavaTestApi } from "./types/test-api"; * @param context The extension context */ export async function activate(context: vscode.ExtensionContext): Promise { - context.subscriptions.push(extension.diagnosticsEmitter); + context.subscriptions.push(extension.diagnosticsEmitter, extension.failureEmitter); registerLogger(context); extension.logger!.client.info("Activating LiquidJava extension..."); @@ -42,6 +42,7 @@ export async function activate(context: vscode.ExtensionContext): Promise ({ status: extension.status, diagnostics: [...(extension.diagnostics ?? [])], diff --git a/client/src/services/diagnostics.ts b/client/src/services/diagnostics.ts index 4e022154..46314573 100644 --- a/client/src/services/diagnostics.ts +++ b/client/src/services/diagnostics.ts @@ -33,6 +33,7 @@ export function handleLJFailure() { if (extension.context) extension.webview?.sendMessage({ type: "context", context: extension.context, errorAtCursor: extension.errorAtCursor }); updateStatusBar("crashed"); + extension.failureEmitter.fire(); } /** diff --git a/client/src/state.ts b/client/src/state.ts index 56f6593f..f31dcdcd 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -26,6 +26,7 @@ export class ExtensionState { file?: string; diagnostics?: LJDiagnostic[]; readonly diagnosticsEmitter = new vscode.EventEmitter(); + readonly failureEmitter = new vscode.EventEmitter(); stateMachine?: LJStateMachine | null; context?: LJContext; currentSelection?: Range; diff --git a/client/src/test/smoke.test.ts b/client/src/test/smoke.test.ts index 19f841b4..9a35ead7 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/src/types/test-api.ts b/client/src/types/test-api.ts index 07a8aa71..1ceba3d4 100644 --- a/client/src/types/test-api.ts +++ b/client/src/types/test-api.ts @@ -10,6 +10,7 @@ export interface WebviewMessage { export interface LiquidJavaTestApi { readonly ready: Promise; readonly onDiagnostics: vscode.Event; + readonly onFailure: vscode.Event; getState(): { status: ExtensionStatus | undefined; diagnostics: LJDiagnostic[] }; readonly onWebviewMessage: vscode.Event; } 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 00000000..4228fd3e --- /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; +} diff --git a/server/pom.xml b/server/pom.xml index 71d12960..31b050c1 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -36,10 +36,6 @@ org.apache.maven.plugins maven-compiler-plugin ${pVersion.compiler} - - 20 - 20 - org.apache.maven.plugins @@ -102,8 +98,7 @@ 0.24.0 UTF-8 - 20 - 20 + 20 1.3.0 10.4.2