diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e10620c84..31633c899 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -49,8 +49,9 @@ jobs: ui-test: strategy: matrix: - os: [ubicloud-standard-4, windows-latest, macos-latest] - fail-fast: true + os: [ubicloud-standard-4] + run: [1, 2, 3, 4, 5, 6, 7, 8] + fail-fast: false env: CODE_VERSION: "1.90.2" runs-on: ${{ matrix.os }} @@ -113,7 +114,7 @@ jobs: if: contains(matrix.os, 'ubuntu') || startsWith(matrix.os, 'ubicloud-standard') working-directory: ./rascal-vscode-extension env: - DELAY_FACTOR: 8 + DELAY_FACTOR: 15 RASCAL_LSP_DEV_DEPLOY: true _JAVA_OPTIONS: '-Xmx5G' # we have 16gb of memory, make sure LSP, REPL & DSL-LSP can start run: | @@ -122,9 +123,9 @@ jobs: - name: Upload Screenshots uses: actions/upload-artifact@v7 - if: failure() + if: failure() || cancelled() with: - name: screenshots-${{ matrix.os }} + name: screenshots-${{ matrix.os }}-${{matrix.run}} path: ./rascal-vscode-extension/uitests/screenshots/**/*.png retention-days: 5 if-no-files-found: error diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java index 89f889fe5..e9bda27c8 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/TextDocumentState.java @@ -74,11 +74,11 @@ public TextDocumentState( this.parser = parser; this.location = location; + this.lastWithoutErrors = new AtomicReference<>(); + this.last = new AtomicReference<>(); var u = new Update(initialVersion, initialContent, initialTimestamp); this.current = new AtomicReference<>(new Versioned<>(initialVersion, u)); - this.lastWithoutErrors = new AtomicReference<>(); - this.last = new AtomicReference<>(); } public ISourceLocation getLocation() { @@ -132,7 +132,7 @@ public CompletableFuture> getCurrentTreeAsync(boolean allowReco } /** - * Wait for current tree to parse. Then return the last tree that matches the allowRecoveredErrors conditation. + * Wait for current tree to parse. Then return the last tree that matches the allowRecoveredErrors condition. * @param allowRecoveredErrors if false, the result will not contain a tree with recovered errors. * @return the last parse tree, or an exception if non existed. */ @@ -198,6 +198,7 @@ public CompletableFuture>> getDiagnosticsAs } private void parse() { + logger.debug("Triggering parse for {}", location); try { parser.apply(location, content) .whenComplete((ITree t, Throwable e) -> { @@ -208,13 +209,17 @@ private void parse() { // Complete future to get the tree if (t == null) { + logger.error("Parse completed exceptionally: {}", location); treeAsync.completeExceptionally(e); } else { var tree = new Versioned<>(version, t, timestamp); + logger.error("Parse completed: {}", location); Versioned.replaceIfNewer(last, tree); if (diagnosticsList.isEmpty()) { + logger.error("Parse completed without errors: {}", location); Versioned.replaceIfNewer(lastWithoutErrors, tree); } + logger.error("TreeAsync completed: {}", location); treeAsync.complete(tree); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java index b30c31360..5b209a9a6 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java @@ -87,7 +87,8 @@ public String getName() { @Override public CompletableFuture parsing(ISourceLocation loc, String input) { - return CompletableFuture.failedFuture(new NoContributionException("parsing")); + logger.error("NoContributions::parsing()", loc); + throw new NoContributionException("parsing"); } @Override diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index c024c43ee..ee6b3073f 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -360,6 +360,7 @@ public void didClose(DidCloseTextDocumentParams params) { logger.debug("Did Close file: {}", params.getTextDocument()); var loc = Locations.toLoc(params.getTextDocument()); if (files.remove(loc) == null) { + logger.error("Received `didClose` for unknown file: {}", loc); throw new ResponseErrorException(unknownFileError(loc, params)); } facts(loc).close(loc); @@ -372,9 +373,11 @@ public void didClose(DidCloseTextDocumentParams params) { @Override public void didDeleteFiles(DeleteFilesParams params) { + logger.debug("textDocument/didDeleteFiles({})", params); exec.submit(() -> { // if a file is deleted, and we were tracking it, we remove our diagnostics for (var f : params.getFiles()) { + logger.debug("Received `didDelete` for {}", f); availableClient().publishDiagnostics(new PublishDiagnosticsParams(f.getUri(), List.of())); } }); @@ -421,7 +424,7 @@ private void handleParsingErrors(TextDocumentState file, CompletableFuture diagnostic.instantiate(columns)) .collect(Collectors.toList()); - logger.trace("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); + logger.info("Finished parsing tree, reporting new parse errors: {} for: {}", parseErrors, file.getLocation()); facts(file.getLocation()).reportParseErrors(file.getLocation(), diagnostics.version(), parseErrors); }); } @@ -694,7 +697,10 @@ private ILanguageContributions contributions(ISourceLocation doc) { .map(contributions::get) .map(ILanguageContributions.class::cast) .flatMap(Optional::ofNullable) - .orElseGet(() -> new NoContributions(extension(doc), exec)); + .orElseGet(() -> { + logger.error("No contributions for {}", doc); + return new NoContributions(extension(doc), exec); + }); } private static String extension(ISourceLocation doc) { @@ -1042,7 +1048,7 @@ public Set fileExtensions() { private void updateFileState(LanguageParameter lang, ISourceLocation f) { f = f.top(); - logger.trace("File of language {} - updating state: {}", lang.getName(), f); + logger.info("File of language {} - updating state: {}", lang.getName(), f); // Since we cannot know what happened to this file before we were called, we need to be careful about races. // It might have been closed in the meantime, so we compute the new value if the key still exists, based on the current value. var state = files.computeIfPresent(f, (loc, currentState) -> currentState.changeParser(contributions(loc)::parsing)); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java index 7e8aac0de..360df3e0e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/Versioned.java @@ -27,9 +27,14 @@ package org.rascalmpl.vscode.lsp.util; import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.PolyNull; public class Versioned { + + private static final Logger logger = LogManager.getLogger(Versioned.class); + private final int version; private final T object; private final long timestamp; @@ -66,13 +71,34 @@ public static AtomicReference> atomic(int version, T object) { } public static boolean replaceIfNewer(AtomicReference<@PolyNull Versioned> current, Versioned maybeNewer) { + logger.debug("Versioned.replaceIfNewer({}, {})", current, maybeNewer); + logger.debug("current==null: {}; maybeNewer.version: {}", current == null, maybeNewer.version()); + int i = 1; while (true) { - var old = current.get(); - if (old == null || old.version() < maybeNewer.version()) { + Versioned old = null; + logger.debug("Iteration {}: Getting `old`...", i++); + try { + old = current.get(); + } catch (Throwable t) { + logger.error("{}: {}", t.getClass(), t.getMessage()); + // t.printStackTrace(); + throw t; + } + logger.debug("Iteration {}: `old` = `{}`", i++, old); + if (old == null) { + logger.debug("old == null"); + if (current.compareAndSet(old, maybeNewer)) { + return true; + } + } else if (old.version() < maybeNewer.version()) { + logger.debug("old version ({}) < new version ({})", old.version, maybeNewer.version); if (current.compareAndSet(old, maybeNewer)) { return true; + } else { + logger.debug("compareAndSet failed: {}, {}", current, old); } } else { + logger.debug("old version ({}) >= new version ({})", old.version, maybeNewer.version); return false; } } diff --git a/rascal-vscode-extension/package-lock.json b/rascal-vscode-extension/package-lock.json index 5c081362d..75b8ad15a 100644 --- a/rascal-vscode-extension/package-lock.json +++ b/rascal-vscode-extension/package-lock.json @@ -33,7 +33,7 @@ "license-check-and-add": "4.x", "mocha": "11.x", "typescript": "5.x", - "vscode-extension-tester": "8.x" + "vscode-extension-tester": "8.22.1" }, "engines": { "node": ">=20.9.0", @@ -8799,21 +8799,21 @@ } }, "node_modules/vscode-extension-tester": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/vscode-extension-tester/-/vscode-extension-tester-8.23.0.tgz", - "integrity": "sha512-YElhRDkOjvmGFv44aCMnEnUM5RmFqWByQ8TBr7/6N9K/b5o1gu2fo9EgTef6VmrB4kyFnSrkPVAbiZBfKQA1mQ==", + "version": "8.22.1", + "resolved": "https://registry.npmjs.org/vscode-extension-tester/-/vscode-extension-tester-8.22.1.tgz", + "integrity": "sha512-euqibuaeGaQofejTKZqfHNkGSz+dmcuS687PROWjj01k48xm0XVbqc6WKpIzSRob3vYx3npy+8pFZ1dVRaW7Hw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@redhat-developer/locators": "^1.20.0", - "@redhat-developer/page-objects": "^1.20.0", + "@redhat-developer/locators": "^1.19.1", + "@redhat-developer/page-objects": "^1.19.1", "@types/selenium-webdriver": "^4.35.5", "@vscode/vsce": "^3.7.1", "c8": "^11.0.0", "commander": "^14.0.3", "compare-versions": "^6.1.1", "find-up": "8.0.0", - "fs-extra": "^11.3.4", + "fs-extra": "^11.3.3", "glob": "^13.0.6", "got": "^14.6.6", "hpagent": "^1.2.0", diff --git a/rascal-vscode-extension/package.json b/rascal-vscode-extension/package.json index e20276f32..ccf386a6e 100644 --- a/rascal-vscode-extension/package.json +++ b/rascal-vscode-extension/package.json @@ -329,6 +329,6 @@ "license-check-and-add": "4.x", "mocha": "11.x", "typescript": "5.x", - "vscode-extension-tester": "8.x" + "vscode-extension-tester": "8.22.1" } } diff --git a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts index 83c66c498..f55761fd2 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -76,40 +76,61 @@ parameterizedDescribe(function (errorRecovery: boolean) { } before(async () => { + console.log("Initializing browser"); browser = VSBrowser.instance; + console.log("Initializing driver"); driver = browser.driver; + console.log("Initializing workbench"); bench = new Workbench(); + console.log("Waiting for worbench"); await ignoreFails(browser.waitForWorkbench()); + console.log("Initializing IDE operations"); ide = new IDEOperations(browser); + console.log("Loading IDE"); await ide.load(); + console.log("Loading Pico"); await loadPico(); + console.log("Reading backup file"); picoFileBackup = await fs.readFile(TestWorkspace.picoFile); - ide = new IDEOperations(browser); - await ide.load(); + await ide.screenshot("after [before] init"); }); beforeEach(async function () { + console.log(`[begin] beforeEach(${this.test?.title})`); if (this.test?.title) { await ide.screenshot(`DSL-${errorRecovery}-` + this.test?.title); } + console.log(`[end] beforeEach(${this.test?.title})`); }); afterEach(async function () { + console.log(`[begin] afterEach(${this.test?.title})`); if (this.test?.title) { + const bb = await new Workbench().getBottomBar(); + await bb.openProblemsView(); await ide.screenshot(`DSL-${errorRecovery}-`+ this.test?.title); } await ide.cleanup(); await fs.writeFile(TestWorkspace.picoFile, picoFileBackup); + console.log(`[end] afterEach(${this.test?.title})`); }); it("has highlighting and parse errors", async function () { + console.log("Closing all editors"); await ignoreFails(new Workbench().getEditorView().closeAllEditors()); + console.log(`Opening Pico file ${TestWorkspace.picoFile}`); const editor = await ide.openModule(TestWorkspace.picoFile); + await ide.screenshot("after-opening-pico-file"); const isPicoLoading = ide.statusContains("Pico"); // we might miss this event, but we wait for it to show up await ignoreFails(driver.wait(isPicoLoading, Delays.normal, "Pico parser generator should have started")); // now wait for the Pico parser generator to disappear - await driver.wait(async () => !(await isPicoLoading()), Delays.verySlow, "Pico parser generator should have finished", 100); + await driver.wait(async () => { + console.log("Awaiting Pico parser generator finish"); + await ide.screenshot("is-pico-loading-finished"); + return !(await isPicoLoading()); + }, Delays.verySlow, "Pico parser generator should have finished", 100); + console.log("hHas syntax highilighting?"); await ide.hasSyntaxHighlighting(editor, Delays.slow); console.log("We got syntax highlighting"); try { @@ -222,8 +243,15 @@ end it("code lens works", async function() { if (errorRecovery) { this.skip(); } const editor = await ide.openModule(TestWorkspace.picoFile); - const lens = await driver.wait(() => editor.getCodeLens("Rename variables a to b."), Delays.verySlow, "Rename lens should be available"); - await lens!.click(); + await driver.wait(async () => { + const lens = await editor.getCodeLens("Rename variables a to b."); + try { + await lens!.click(); + return true; + } catch (e) { + return false; + } + }, Delays.verySlow, "Rename lens should be available"); await ide.assertLineBecomes(editor, 9, "b := 2;", "a variable should be changed to b"); }); @@ -248,9 +276,11 @@ end const editor = await ide.openModule(TestWorkspace.picoFile); await editor.moveCursor(5, 6); - ide.renameSymbol(editor, bench, "z"); - - await driver.wait(() => (editor.isDirty()), Delays.extremelySlow, "Rename should have resulted in changes in the editor"); + await ide.renameSymbol(editor, bench, "z"); + await driver.wait(async () => { + await ide.screenshot("rename-dirty-check"); + return editor.isDirty(); + }, Delays.extremelySlow, "Rename should have resulted in changes in the editor"); const editorText = await editor.getText(); expect(editorText).to.contain("z : natural"); @@ -295,6 +325,7 @@ end await driver.wait(async () => { const outgoing = await ignoreFails(new SideBarView().getContent().getSection("Callers Of")); const items = await ignoreFails(outgoing!.getVisibleItems()); + await ide.screenshot("call-hierarchy-incoming"); return items?.length === 2; }, Delays.normal, "Call hierarchy should show `multiply` and its recursive call."); @@ -303,6 +334,7 @@ end await driver.wait(async () => { const incoming = await ignoreFails(new SideBarView().getContent().getSection("Calls From")); const items = await ignoreFails(incoming!.getVisibleItems()); + await ide.screenshot("call-hierarchy-outgoing"); return items?.length === 3; }, Delays.normal, "Call hierarchy should show `multiply` and its two outgoing calls."); }); diff --git a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts index 0e147389a..cfdb28154 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/ide.test.ts @@ -196,8 +196,7 @@ describe('IDE', function () { const checkRascalStatus = ide.statusContains("Loading Rascal"); await driver.wait(async () => !(await checkRascalStatus()), Delays.extremelySlow, "Rascal evaluators have not finished loading"); - ide.renameSymbol(editor, bench, "i"); - + await ide.renameSymbol(editor, bench, "i"); await driver.wait(() => (editor.isDirty()), Delays.extremelySlow, "Rename should have resulted in changes in the editor"); const editorText = await editor.getText(); diff --git a/rascal-vscode-extension/src/test/vscode-suite/utils.ts b/rascal-vscode-extension/src/test/vscode-suite/utils.ts index a46aa8c4d..57505a89b 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/utils.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/utils.ts @@ -211,6 +211,7 @@ export class IDEOperations { } async load() { + console.log("Loading IDE..."); await ignoreFails(this.browser.waitForWorkbench(Delays.slow)); for (let t = 0; t < 5; t++) { try { @@ -218,16 +219,23 @@ export class IDEOperations { if (isWorkSpaceOpen !== undefined && isWorkSpaceOpen.length > 0) { break; } + console.log(`Opening workspace ${TestWorkspace.workspaceFile}`); await this.browser.openResources(TestWorkspace.workspaceFile); } catch (ex) { - console.debug("Error opening workspace, retrying.", ex); + console.error("Error opening workspace, retrying.", ex); } } + console.log("Waiting for workbench"); await ignoreFails(this.browser.waitForWorkbench(Delays.normal)); + console.log("Waiting for workbench again"); await ignoreFails(this.browser.waitForWorkbench(Delays.normal)); + console.log("Opening notifications center"); const center = await ignoreFails(new Workbench().openNotificationsCenter()); + console.log("Clearing all notifications"); await ignoreFails(center?.clearAllNotifications()); + console.log("Closing notifications center"); await ignoreFails(center?.close()); + console.log("Assuring debug level logging is enabled"); await assureDebugLevelLoggingIsEnabled(); } @@ -283,7 +291,7 @@ export class IDEOperations { await new Workbench().executeCommand("workbench.action.revertAndCloseActiveEditor"); } catch (ex) { const title = ignoreFails(new TextEditor().getTitle()) ?? 'unknown'; - this.screenshot(`revert of ${title} failed ` + tryCount); + await this.screenshot(`revert of ${title} failed ` + tryCount); console.log(`Revert of ${title} failed, but we ignore it`, ex); } try { @@ -299,7 +307,7 @@ export class IDEOperations { return !(await new TextEditor().isDirty()); } catch (ignored) { - this.screenshot("open editor check failed " + tryCount); + await this.screenshot("open editor check failed " + tryCount); console.log("Open editor dirty check failed: ", ignored); return false; @@ -376,9 +384,11 @@ export class IDEOperations { const renameBox = await this.hasElement(editor, By.className("rename-input"), Delays.normal, "Rename box should appear"); await renameBox.sendKeys(Key.BACK_SPACE, Key.BACK_SPACE, Key.BACK_SPACE, newName, Key.ENTER); renameSuccess = true; + console.log("Entering rename name succeeded"); } catch (e) { console.log("Rename failed to succeed, lets try again"); + console.error(e); await this.screenshot(`DSL-failed-rename-round-${tries}`); tries++; } @@ -448,6 +458,7 @@ export class IDEOperations { private screenshotSeqNumber = 0; screenshot(name: string): Promise { + console.log(`Taking screenshot ${name}`); return this.browser.takeScreenshot( `${String(this.screenshotSeqNumber++).padStart(4, '0')}-` + // Make sorting screenshots chronologically in VS Code easier name.replace(/[/\\?%*:|"<>]/g, '-'));