diff --git a/.github/skills/uitest/SKILL.md b/.github/skills/uitest/SKILL.md index 2746e160..bd42b686 100644 --- a/.github/skills/uitest/SKILL.md +++ b/.github/skills/uitest/SKILL.md @@ -77,3 +77,6 @@ The repository workflow `.github/workflows/e2e-autotest.yml` builds a branch VSI Use workflow dispatch when the user asks to validate in CI or across platforms. The `test_plan` input can target a single plan; leave it empty to run all plans. +For `java-gradle`, `java-gradle-java25`, and `java-gradle-java27`, CI uses one Gradle setup and the existing environment-binding mechanism. Java 21/25 helpers read `GRADLE_CI_GRADLE`, their `JAVA21_HOME`/`JAVA25_HOME`, and `AUTOTEST_STAGING_DIR`; the Java 27 helper retains its `JAVA27_CI_GRADLE` contract. CI does not rewrite terminal commands or override the IDE's Gradle home. Actual version, program-output and bytecode assertions remain unchanged. Local helper invocations retain their existing behavior. + +For Java 21/25, the same binding exports `ORG_GRADLE_PROJECT_javaRelease` before VS Code starts. Local runs must set that project property as documented in the fixture README. These plans require BSP import, actual Gradle `build`/`run` tasks, and the exact configuration marker in Gradle for Java output; terminal success alone is insufficient. All three Gradle plans reuse the shared artifact collector: terminal diagnostics are staged outside AutoTest's output and archived under `diagnostics/ci/terminal/`, while Gradle/Java output-channel and extension-host logs are archived under `logs/ide/logs/`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61f25264..b32cbda1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,4 +25,6 @@ jobs: - run: npm ci + - run: npm test + - run: npm run build diff --git a/.github/workflows/e2e-autotest.yml b/.github/workflows/e2e-autotest.yml index 8380cd53..74283b7c 100644 --- a/.github/workflows/e2e-autotest.yml +++ b/.github/workflows/e2e-autotest.yml @@ -179,12 +179,6 @@ jobs: "JAVA27_HOME=$target" >> $env:GITHUB_ENV Write-Host "Java 27 project toolchain: $target" - - name: Setup Gradle for Java 27 - if: contains(matrix.plan, 'java27') && contains(matrix.plan, 'gradle') - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - with: - gradle-version: '9.8.1' - - name: Setup Java 25 (for java25 test plans) if: contains(matrix.plan, 'java25') uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 @@ -196,6 +190,7 @@ jobs: if: contains(matrix.plan, 'java25') shell: pwsh run: | + "JAVA25_HOME=$env:JAVA_HOME" >> $env:GITHUB_ENV $planFile = "test-plans/${{ matrix.plan }}.yaml" if ($IsWindows) { $target = "C:\Program Files\Java\jdk-25" @@ -219,22 +214,49 @@ jobs: distribution: temurin java-version: 21 - - name: Bind Gradle Java 27 toolchain - if: matrix.plan == 'java-gradle-java27' + - name: Configure JDK 21 for Gradle import tests + if: matrix.plan == 'java-gradle' || matrix.plan == 'java-gradle-java25' shell: pwsh + run: | + "JAVA21_HOME=$env:JAVA_HOME" >> $env:GITHUB_ENV + if ("${{ matrix.plan }}" -eq "java-gradle") { + $planFile = "test-plans/${{ matrix.plan }}.yaml" + # JSON escaping also handles Windows installation paths in double-quoted YAML. + $jdkPath = ($env:JAVA_HOME | ConvertTo-Json -Compress).Trim('"') + (Get-Content $planFile -Raw).Replace('C:\\Program Files\\Java\\jdk-21', $jdkPath) | Set-Content $planFile + } + + - name: Setup Gradle for import tests + if: matrix.plan == 'java-gradle' || matrix.plan == 'java-gradle-java25' || matrix.plan == 'java-gradle-java27' + uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + with: + gradle-version: ${{ matrix.plan == 'java-gradle' && '8.5' || '9.8.1' }} + + - name: Bind Gradle for import tests + if: matrix.plan == 'java-gradle' || matrix.plan == 'java-gradle-java25' || matrix.plan == 'java-gradle-java27' + shell: pwsh + env: + GRADLE_TEST_PLAN: ${{ matrix.plan }} run: | $ciGradle = (Get-Command gradle -CommandType Application -ErrorAction Stop | Select-Object -First 1).Source - $versionLog = Join-Path $env:AUTOTEST_STAGING_DIR "gradle-version.log" - $PSNativeCommandUseErrorActionPreference = $false - $gradleVersion = & $ciGradle --version 2>&1 | Out-String - $versionExit = $LASTEXITCODE - "Command: $ciGradle --version`n$gradleVersion" | Set-Content -LiteralPath $versionLog - if ($versionExit -ne 0) { throw "CI Gradle version probe exited with $versionExit; see diagnostics/ci/gradle-version.log in this case's results artifact." } - if ($gradleVersion -notmatch '(?m)^Gradle 9\.8\.1\s*$') { - throw "CI did not select the provisioned Gradle 9.8.1: $ciGradle; see $versionLog" + $release = if ($env:GRADLE_TEST_PLAN -eq "java-gradle") { "21" } elseif ($env:GRADLE_TEST_PLAN -eq "java-gradle-java25") { "25" } else { "27" } + if ($release -eq "27") { + $versionLog = Join-Path $env:AUTOTEST_STAGING_DIR "gradle-version.log" + $PSNativeCommandUseErrorActionPreference = $false + $gradleVersion = & $ciGradle --version 2>&1 | Out-String + $versionExit = $LASTEXITCODE + "Command: $ciGradle --version`n$gradleVersion" | Set-Content -LiteralPath $versionLog + if ($versionExit -ne 0) { throw "CI Gradle version probe exited with $versionExit; see diagnostics/ci/gradle-version.log in this case's results artifact." } + if ($gradleVersion -notmatch '(?m)^Gradle 9\.8\.1\s*$') { + throw "CI did not select the provisioned Gradle 9.8.1: $ciGradle; see $versionLog" + } + "JAVA27_CI_GRADLE=$ciGradle" >> $env:GITHUB_ENV + } else { + "ORG_GRADLE_PROJECT_javaRelease=$release" >> $env:GITHUB_ENV } - "JAVA27_CI_GRADLE=$ciGradle" >> $env:GITHUB_ENV - Write-Host "Bound Gradle 9.8.1: $ciGradle" + "GRADLE_CI_GRADLE=$ciGradle" >> $env:GITHUB_ENV + "GRADLE_CI_JAVA_HOME=$([Environment]::GetEnvironmentVariable("JAVA${release}_HOME"))" >> $env:GITHUB_ENV + Write-Host "Bound CI Gradle: $ciGradle" - name: Setup virtual display (Linux) if: runner.os == 'Linux' @@ -429,8 +451,8 @@ jobs: run: | $identity = [ordered]@{ javaHome = $env:JAVA_HOME - projectJavaHome = $env:JAVA27_HOME - gradle = $env:JAVA27_CI_GRADLE + projectJavaHome = $(if ($env:GRADLE_CI_JAVA_HOME) { $env:GRADLE_CI_JAVA_HOME } else { $env:JAVA27_HOME }) + gradle = $env:GRADLE_CI_GRADLE runnerOs = $env:RUNNER_OS runnerArch = $env:RUNNER_ARCH path = $env:PATH diff --git a/.vscodeignore b/.vscodeignore index dad98cb8..4069824b 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -18,3 +18,4 @@ scripts test-fixtures/** test-plans/** test-results/** +tests/** diff --git a/README.md b/README.md index 5393758f..72e47920 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ At the same time, [π¦ CheckStyle](https://marketplace.visualstudio.com/items?i ## Questions & Issues +See the [Java runtime requirements and configuration](https://github.com/microsoft/vscode-java-pack/wiki/Java-Runtime-Requirements) guide for JDK setup details. + Each extension mentioned above is a separate open-source project and has its own repository. To make things easier, simply [π open an issue in this repository](https://github.com/Microsoft/vscode-java-pack/issues). The new issue will be triaged and redirected. ## Data/Telemetry diff --git a/package.json b/package.json index 3c5a5a6e..063d503d 100644 --- a/package.json +++ b/package.json @@ -338,6 +338,7 @@ "compile": "webpack --config webpack.config.js", "watch": "webpack --config webpack.config.js --watch", "build": "webpack --config webpack.config.js --mode=\"production\"", + "test": "node --test", "update-tpn": "node scripts/update-third-party-notice.js" }, "devDependencies": { diff --git a/src/beginner-tips/assets/BeginnerTips.tsx b/src/beginner-tips/assets/BeginnerTips.tsx index e4d39928..c2671618 100644 --- a/src/beginner-tips/assets/BeginnerTips.tsx +++ b/src/beginner-tips/assets/BeginnerTips.tsx @@ -10,7 +10,7 @@ import DebuggingPanel from "./tabs/DebuggingPanel"; import FaqPanel from "./tabs/FaqPanel"; import QuickStartPanel from "./tabs/QuickStartPanel"; -export default function BeginnerTips() { +export default function BeginnerTips({ requiredJdkVersion }: { requiredJdkVersion: number }) { return (
{"\"java.configuration.runtimes\": [{"}
@@ -50,12 +51,15 @@ export default function FaqPanel() {
Why do I see the JDK errors?
- JDK {REQUIRED_JDK_VERSION}+ is required to run the Java language support (redhat.java) extension! You see this error because we failed to find one on your machine. The Configure Java Runtime guide can help you understand how JDK path is searched and provides download links if you need to install one.
+ JDK {requiredJdkVersion}+ is required to run the installed Java language support (redhat.java) extension. A compatible bundled runtime can satisfy this requirement; otherwise, a compatible local JDK is needed. To set the language-server launch JDK explicitly, configure java.jdt.ls.java.home. Runtime selection and startup diagnostics are provided by redhat.java; open the Java logs for details.
+
+
+ The Configure Java Runtime page configures project JDKs, not the language-server launch JDK. Your projects can use older JDKs independently of the tooling requirement.
Can I run my Java 8 project with JDK 1.8?
- Yes. The JDK {REQUIRED_JDK_VERSION} requirement is just for running the Java language support (redhat.java) extension itself. You can still configure a different runtime JDK for your project via the user setting "java.configuration.runtimes". The extension will pick a matching JDK to compile/run your project according to the compiler version specified by the project build file.
+ Yes. The JDK {requiredJdkVersion} requirement is just for running the Java language support (redhat.java) extension itself. You can still configure a different runtime JDK for your project via the user setting "java.configuration.runtimes". The extension will pick a matching JDK to compile/run your project according to the compiler version specified by the project build file.
{runtimeSampleCode}
diff --git a/src/beginner-tips/index.ts b/src/beginner-tips/index.ts
index 5ac6b427..f9d9e42e 100644
--- a/src/beginner-tips/index.ts
+++ b/src/beginner-tips/index.ts
@@ -5,6 +5,7 @@ import * as path from "path";
import * as vscode from "vscode";
import { sendInfo } from "vscode-extension-telemetry-wrapper";
import { getNonce, webviewCmdLinkHandler } from "../utils";
+import { getRequiredJdkVersion } from "../java-runtime/utils/upstreamApi";
const WEBVIEW_ID = "java.gettingStarted";
const WEBVIEW_TITLE = "Tips for Beginners";
@@ -122,7 +123,7 @@ class BeginnerTipsPage {
-
+
diff --git a/src/extension.ts b/src/extension.ts
index 43f65356..42f91c08 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -13,7 +13,7 @@ import { JavaExtGuideViewSerializer } from "./ext-guide";
import { initFormatterSettingsEditorProvider } from "./formatter-settings";
import { initRemoteProfileProvider } from "./formatter-settings/RemoteProfileProvider";
import { InstallJdkViewSerializer } from "./install-jdk";
-import { JavaRuntimeViewSerializer, validateJavaRuntime } from "./java-runtime";
+import { JavaRuntimeViewSerializer } from "./java-runtime";
import { HelpViewType, showReleaseNotesOnStart } from "./misc";
import { OverviewViewSerializer } from "./overview";
import { CodeActionProvider } from "./providers/CodeActionProvider";
@@ -76,12 +76,6 @@ async function initializeExtension(_operationId: string, context: vscode.Extensi
showReleaseNotesOnStart(context);
});
}
-
- if (!await validateJavaRuntime()) {
- scheduleAction("showJdkState", true, true).then(() => {
- vscode.commands.executeCommand("java.runtime");
- });
- }
}
async function presentFirstView(context: vscode.ExtensionContext) {
diff --git a/src/java-runtime/assets/ProjectJDKPanel.tsx b/src/java-runtime/assets/ProjectJDKPanel.tsx
index 3c67aeb0..6949780a 100644
--- a/src/java-runtime/assets/ProjectJDKPanel.tsx
+++ b/src/java-runtime/assets/ProjectJDKPanel.tsx
@@ -13,15 +13,22 @@ import { useState } from "react";
import { encodeCommandUriWithTelemetry, ProjectType } from "../../utils/webview";
import { JavaRuntimeEntry, ProjectRuntimeEntry } from "../types";
import { DefaultJDKSelector } from "./components/DefaultJDKSelector";
+import { JDKActions } from "./components/JDKActions";
import { ProjectTypeHint } from "./components/ProjectTypeHint";
import { onWillListRuntimes, openBuildScript } from "./vscode.api";
interface Props {
jdkEntries: JavaRuntimeEntry[];
projectRuntimes: ProjectRuntimeEntry[];
+ javaDotHome?: string;
+ toolingJreVersion?: number;
+ requiredJdkVersion: number;
+ javaHomeError?: string;
+ javaHomeWarning?: string;
+ projectJdkError?: string;
}
-export function ProjectJDKPanel({ jdkEntries, projectRuntimes }: Props) {
+export function ProjectJDKPanel({ jdkEntries, projectRuntimes, javaDotHome, toolingJreVersion, requiredJdkVersion, javaHomeError, javaHomeWarning, projectJdkError }: Props) {
const [showHintFor, setShowHintFor] = useState<"Maven" | "Gradle" | "Others" | undefined>();
const projectTypeHint = (projectType: ProjectType) => {
@@ -68,10 +75,21 @@ export function ProjectJDKPanel({ jdkEntries, projectRuntimes }: Props) {
));
const downloadJDKCommand = encodeCommandUriWithTelemetry("java.runtime", "download", "java.installJdk");
+ const toolingSettingsCommand = encodeCommandUriWithTelemetry("java.runtime", "toolingSettings", "workbench.action.openSettings", ["java.jdt.ls.java.home"]);
return (
Configure Runtime for Projects
+ {javaDotHome ? (
+ Language server runtime reported by redhat.java: {javaDotHome} (Java {toolingJreVersion}).
+ ) : (
+ Language server runtime information is not available yet. Use Refresh after redhat.java starts, or open the Java logs for details.
+ )}
+ JDK {requiredJdkVersion}+ is required for the language server only; your projects can use older JDKs. Open language-server runtime settings.
+ {javaHomeError && {javaHomeError} Open Java Logs.
}
+ {javaHomeWarning && {javaHomeWarning} Open Java Logs.
}
+ {projectJdkError && {projectJdkError} Open Java Logs.
}
+ {!javaDotHome && }
{projectEntries.length > 0 && Manage Java runtime for your projects. If you don't have a valid Java runtime, you can download one.
}
{
projectEntries.length > 0 ?
@@ -87,10 +105,14 @@ export function ProjectJDKPanel({ jdkEntries, projectRuntimes }: Props) {
:
- No project detected yet. Please refresh later if Java extension is importing your projects.
- Refresh
+ {projectJdkError ?
+ Project information could not be fully loaded. Please refresh to try again.
+ :
+ No project detected yet. Please refresh later if Java extension is importing your projects.
+ }
}
+ Refresh
);
diff --git a/src/java-runtime/assets/ToolingJDKPanel.tsx b/src/java-runtime/assets/ToolingJDKPanel.tsx
index 947ed590..99ddd74d 100644
--- a/src/java-runtime/assets/ToolingJDKPanel.tsx
+++ b/src/java-runtime/assets/ToolingJDKPanel.tsx
@@ -3,44 +3,31 @@
import "@vscode-elements/elements/dist/vscode-button/index.js";
-import { useState } from "react";
-import { JavaRuntimeEntry } from "../types";
-import { onWillBrowseForJDK, onWillRunCommandFromWebview } from './vscode.api';
+import { JavaRuntimeData, ToolingRuntimeInfo } from "../types";
+import { JDKActions } from "./components/JDKActions";
+import { onWillListRuntimes } from './vscode.api';
-const REQUIRED_JDK_VERSION = 17;
-
-interface Props {
- jdkEntries?: JavaRuntimeEntry[];
- javaDotHome?: string;
- javaHomeError?: any;
-}
-
-export function ToolingJDKPanel({ javaHomeError }: Props) {
- const [isDirty, setIsDirty] = useState(false);
-
- const onClickBrowseJDKButton = () => {
- onWillBrowseForJDK();
- setIsDirty(true);
- };
-
- const onClickInstallButton = () => {
- onWillRunCommandFromWebview("java.runtime", "download", "java.installJdk");
- };
+type Props = ToolingRuntimeInfo & Pick;
+export function ToolingJDKPanel({ javaHomeError, javaHomeWarning, javaDotHome, toolingJreVersion, requiredJdkVersion, projectJdkError }: Props) {
return (
Configure Runtime for Language Server
- Java Language Server requires a JDK {REQUIRED_JDK_VERSION}+ to launch itself.
+ Java Language Server requires a JDK {requiredJdkVersion}+ to launch itself. Your projects can use older JDKs.
+ {javaDotHome ? (
+ Language server runtime reported by redhat.java: {javaDotHome} (Java {toolingJreVersion}).
+ ) : (
+ Language server runtime information has not been reported by redhat.java.
+ )}
{javaHomeError && ({javaHomeError}
)}
+ {javaHomeWarning && ({javaHomeWarning}
)}
+ {projectJdkError && ({projectJdkError}
)}
+
+ Open Java Logs for details. Runtime selection and startup diagnostics are provided by redhat.java.
-
- Locate an Existing JDK
- {isDirty && Reload }
-
-
+
+ Refresh
);
}
diff --git a/src/java-runtime/assets/components/JDKActions.tsx b/src/java-runtime/assets/components/JDKActions.tsx
new file mode 100644
index 00000000..ab52a6ef
--- /dev/null
+++ b/src/java-runtime/assets/components/JDKActions.tsx
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+import "@vscode-elements/elements/dist/vscode-button/index.js";
+
+import { useState } from "react";
+import { onWillBrowseForJDK, onWillRunCommandFromWebview } from "../vscode.api";
+
+export function JDKActions() {
+ const [isDirty, setIsDirty] = useState(false);
+
+ const onClickBrowseJDKButton = () => {
+ onWillBrowseForJDK();
+ setIsDirty(true);
+ };
+
+ const onClickInstallButton = () => {
+ onWillRunCommandFromWebview("java.runtime", "download", "java.installJdk");
+ };
+
+ return (
+ <>
+
+ Locate an Existing JDK
+ {isDirty && Reload }
+
+
+ >
+ );
+}
diff --git a/src/java-runtime/assets/index.ts b/src/java-runtime/assets/index.ts
index a71ce3d0..e87a41a3 100644
--- a/src/java-runtime/assets/index.ts
+++ b/src/java-runtime/assets/index.ts
@@ -7,11 +7,12 @@ import "./style.scss";
import { ProjectJDKPanel } from "./ProjectJDKPanel";
import { onWillListRuntimes } from "./vscode.api";
import { ToolingJDKPanel } from "./ToolingJDKPanel";
+import { JavaRuntimeData } from "../types";
const container = document.getElementById("content")!;
const root = createRoot(container);
-const onInitialize = (event: any) => {
+const onInitialize = (event: MessageEvent<{command: string; args: JavaRuntimeData}>) => {
const { data } = event;
if (data.command === "showJavaRuntimeEntries") {
showJavaRuntimeEntries(data.args);
@@ -21,20 +22,19 @@ const onInitialize = (event: any) => {
window.addEventListener("message", onInitialize);
onWillListRuntimes();
-function showJavaRuntimeEntries(args: any) {
- if (args.javaHomeError) {
- // TODO: remove after tooling JDK is embedded
- const props = {
- jdkEntries: args.javaRuntimes,
- javaHomeError: args.javaHomeError,
- javaDotHome: args.javaDotHome
- };
- root.render(createElement(ToolingJDKPanel, props));
+function showJavaRuntimeEntries(args: JavaRuntimeData) {
+ if (args.javaHomeError && args.projectRuntimes.length === 0) {
+ root.render(createElement(ToolingJDKPanel, args));
} else {
- const props = {
+ root.render(createElement(ProjectJDKPanel, {
jdkEntries: args.javaRuntimes,
projectRuntimes: args.projectRuntimes,
- }
- root.render(createElement(ProjectJDKPanel, props));
+ javaDotHome: args.javaDotHome,
+ toolingJreVersion: args.toolingJreVersion,
+ requiredJdkVersion: args.requiredJdkVersion,
+ javaHomeError: args.javaHomeError,
+ javaHomeWarning: args.javaHomeWarning,
+ projectJdkError: args.projectJdkError
+ }));
}
}
diff --git a/src/java-runtime/index.ts b/src/java-runtime/index.ts
index e3c17e9a..83a08772 100644
--- a/src/java-runtime/index.ts
+++ b/src/java-runtime/index.ts
@@ -2,15 +2,13 @@
// Licensed under the MIT license.
import { findRuntimes, getRuntime, IJavaRuntime } from "jdk-utils";
-import * as _ from "lodash";
import * as path from "path";
import * as vscode from "vscode";
import { getExtensionContext, getNonce } from "../utils";
import { getProjectNameFromUri, getProjectType } from "../utils/jdt";
-import { ProjectType } from "../utils/webview";
-import { JavaRuntimeEntry, ProjectRuntimeEntry } from "./types";
+import { JavaRuntimeData, JavaRuntimeEntry, ProjectRuntimeEntry } from "./types";
import { sourceLevelDisplayName } from "./utils/misc";
-import { REQUIRED_JDK_VERSION, resolveRequirements } from "./utils/upstreamApi";
+import { getRequiredJdkVersion, getToolingRuntimeInfo } from "./utils/upstreamApi";
let javaRuntimeView: vscode.WebviewPanel | undefined;
let javaHomes: IJavaRuntime[];
@@ -47,9 +45,7 @@ async function initializeJavaRuntimeView(context: vscode.ExtensionContext, webvi
context.subscriptions.push(webviewPanel.webview.onDidReceiveMessage(async (e) => {
switch (e.command) {
case "onWillListRuntimes": {
- findJavaRuntimeEntries().then(data => {
- showJavaRuntimeEntries(data);
- });
+ await refreshRuntimes();
break;
}
case "updateJavaHome": {
@@ -70,9 +66,7 @@ async function initializeJavaRuntimeView(context: vscode.ExtensionContext, webvi
});
}
await vscode.workspace.getConfiguration("java").update("configuration.runtimes", runtimes, vscode.ConfigurationTarget.Global);
- findJavaRuntimeEntries().then(data => {
- showJavaRuntimeEntries(data);
- });
+ await refreshRuntimes();
break;
}
case "setDefaultRuntime": {
@@ -100,9 +94,7 @@ async function initializeJavaRuntimeView(context: vscode.ExtensionContext, webvi
}
}
await vscode.workspace.getConfiguration("java").update("configuration.runtimes", runtimes, vscode.ConfigurationTarget.Global);
- findJavaRuntimeEntries().then(data => {
- showJavaRuntimeEntries(data);
- });
+ await refreshRuntimes();
break;
}
case "openBuildScript": {
@@ -121,10 +113,15 @@ async function initializeJavaRuntimeView(context: vscode.ExtensionContext, webvi
});
if (javaHomeUri) {
const javaHome = javaHomeUri[0].fsPath;
- if (await getRuntime(javaHome)) {
- await vscode.workspace.getConfiguration("java").update("jdt.ls.java.home", javaHome, vscode.ConfigurationTarget.Global);
+ const runtime = await getRuntime(javaHome, {checkJavac: true, withVersion: true});
+ const requiredJdkVersion = getRequiredJdkVersion();
+ if (!runtime?.hasJavac) {
+ await vscode.window.showWarningMessage(`${javaHome} is not a valid JDK home directory.`);
+ } else if (!runtime.version || runtime.version.major < requiredJdkVersion) {
+ await vscode.window.showWarningMessage(`Java ${requiredJdkVersion} or more recent is required to launch the Java Language Server. "${javaHome}" doesn't meet the requirement.`);
} else {
- await vscode.window.showWarningMessage(`${javaHome} is not a valid Java runtime home directory.`);
+ await vscode.workspace.getConfiguration("java").update("jdt.ls.java.home", javaHome, vscode.ConfigurationTarget.Global);
+ await refreshRuntimes();
}
}
break;
@@ -139,24 +136,35 @@ async function initializeJavaRuntimeView(context: vscode.ExtensionContext, webvi
}
}));
- function showJavaRuntimeEntries(args: any) {
- webviewPanel.webview.postMessage({
- command: "showJavaRuntimeEntries",
- args: args,
- });
+ let disposed = false;
+ async function refreshRuntimes() {
+ try {
+ const args = await findJavaRuntimeEntries();
+ if (!disposed) {
+ await webviewPanel.webview.postMessage({command: "showJavaRuntimeEntries", args});
+ }
+ } catch (error) {
+ console.warn(error);
+ if (!disposed) {
+ const message = error instanceof Error ? error.message : String(error);
+ await vscode.window.showErrorMessage(`Unable to refresh Configure Java Runtime: ${message}`);
+ }
+ }
}
- // refresh webview with latest source levels when classpath (project info) changes
+ let classpathListener: vscode.Disposable | undefined;
const javaExt = vscode.extensions.getExtension("redhat.java");
- if (javaExt?.isActive && javaExt?.exports?.onDidClasspathUpdate) {
+ if (javaExt?.isActive && javaExt.exports?.onDidClasspathUpdate) {
const onDidClasspathUpdate: vscode.Event = javaExt.exports.onDidClasspathUpdate;
- const listener = onDidClasspathUpdate((_e: vscode.Uri) => {
- findJavaRuntimeEntries().then(data => {
- showJavaRuntimeEntries(data);
- });
+ classpathListener = onDidClasspathUpdate(() => {
+ void refreshRuntimes();
});
- context.subscriptions.push(webviewPanel.onDidDispose(() => listener.dispose()));
}
+
+ context.subscriptions.push(webviewPanel.onDidDispose(() => {
+ disposed = true;
+ classpathListener?.dispose();
+ }));
}
function getHtmlForWebview(webviewPanel: vscode.WebviewPanel, scriptPath: string) {
@@ -197,138 +205,128 @@ export class JavaRuntimeViewSerializer implements vscode.WebviewPanelSerializer
}
}
-export async function validateJavaRuntime() {
- // TODO:
- // option a) should check Java LS exported API for java_home
- // * option b) use the same way to check java_home as vscode-java
+export async function findJavaRuntimeEntries(): Promise {
+ const toolingRuntimeInfo = getToolingRuntimeInfo();
+ const errors: string[] = [];
try {
- const runtime = await resolveRequirements();
- if (runtime.tooling_jre_version >= REQUIRED_JDK_VERSION && runtime.tooling_jre) {
- return true;
+ if (!javaHomes) {
+ const runtimes: IJavaRuntime[] = await findRuntimes({ checkJavac: true, withVersion: true });
+ javaHomes = runtimes.filter(r => r.hasJavac);
}
} catch (error) {
+ recordInventoryError(errors, "Unable to list installed project JDKs", error);
}
-
- return false;
-}
-
-export async function findJavaRuntimeEntries(): Promise<{
- javaRuntimes?: JavaRuntimeEntry[],
- projectRuntimes?: ProjectRuntimeEntry[],
- javaDotHome?: string;
- javaHomeError?: string;
-}> {
- if (!javaHomes) {
- const runtimes: IJavaRuntime[] = await findRuntimes({ checkJavac: true, withVersion: true });
- javaHomes = runtimes.filter(r => r.hasJavac);
- }
- const javaRuntimes: JavaRuntimeEntry[] = javaHomes.map(elem => ({
+ const javaRuntimes: JavaRuntimeEntry[] = (javaHomes ?? []).map(elem => ({
name: elem.homedir,
fspath: elem.homedir,
majorVersion: elem.version?.major || 0,
type: "from jdk-utils"
})).sort((a, b) => b.majorVersion - a.majorVersion);
- let javaDotHome;
- let javaHomeError;
- try {
- const runtime = await resolveRequirements();
- javaDotHome = runtime.tooling_jre;
- const javaVersion = runtime.tooling_jre_version;
- if (!javaVersion || javaVersion < REQUIRED_JDK_VERSION) {
- javaHomeError = `Java ${REQUIRED_JDK_VERSION} or more recent is required by the Java language support (redhat.java) extension. Preferred JDK "${javaDotHome}" (version ${javaVersion}) doesn't meet the requirement. Please specify or install a recent JDK.`;
- }
- } catch (error) {
- javaHomeError = (error as Error).message;
- }
-
- let projectRuntimes = await getProjectRuntimesFromPM();
- if (_.isEmpty(projectRuntimes)) {
- projectRuntimes = await getProjectRuntimesFromLS();
+ const pmResult = await getProjectRuntimesFromPM();
+ let projectRuntimes = pmResult.entries;
+ errors.push(...pmResult.errors);
+ if (projectRuntimes.length === 0) {
+ const lsResult = await getProjectRuntimesFromLS();
+ projectRuntimes = lsResult.entries;
+ errors.push(...lsResult.errors);
}
return {
javaRuntimes,
projectRuntimes,
- javaDotHome,
- javaHomeError
+ projectJdkError: errors.length > 0 ? errors.join("\n") : undefined,
+ ...toolingRuntimeInfo
};
}
-async function getProjectRuntimesFromPM(): Promise {
- const ret: ProjectRuntimeEntry[] = [];
+interface ProjectRuntimeResult {
+ entries: ProjectRuntimeEntry[];
+ errors: string[];
+}
+
+function recordInventoryError(errors: string[], context: string, error: unknown) {
+ const message = error instanceof Error ? error.message : String(error);
+ const diagnostic = `${context}: ${message}`;
+ console.warn(diagnostic, error);
+ errors.push(diagnostic);
+}
+
+async function addProjectRuntime(result: ProjectRuntimeResult, rootPath: string, name?: string) {
+ try {
+ const runtimeSpec = await getRuntimeSpec(rootPath);
+ const projectType = getProjectType(vscode.Uri.parse(rootPath).fsPath, runtimeSpec.natureIds);
+ result.entries.push({
+ name: name || getProjectNameFromUri(rootPath),
+ rootPath,
+ projectType,
+ ...runtimeSpec
+ });
+ } catch (error) {
+ recordInventoryError(result.errors, `Unable to read runtime information for project "${name || rootPath}" (${rootPath})`, error);
+ }
+}
+
+async function getProjectRuntimesFromPM(): Promise {
+ const result: ProjectRuntimeResult = { entries: [], errors: [] };
const projectManagerExt = vscode.extensions.getExtension("vscjava.vscode-java-dependency");
if (vscode.workspace.workspaceFolders && projectManagerExt && projectManagerExt.isActive) {
- let projects: any[] = [];
for (const wf of vscode.workspace.workspaceFolders) {
+ const folderUri = wf.uri.toString();
try {
- projects = await vscode.commands.executeCommand("java.execute.workspaceCommand", "java.project.list", wf.uri.toString()) || [];
+ const projects = await vscode.commands.executeCommand<{ uri: string; displayName?: string; name: string }[]>(
+ "java.execute.workspaceCommand", "java.project.list", folderUri
+ ) || [];
+ for (const project of projects) {
+ await addProjectRuntime(result, project.uri, project.displayName || project.name);
+ }
} catch (error) {
- console.error(error);
- }
-
- for (const project of projects) {
- const runtimeSpec = await getRuntimeSpec(project.uri);
- const projectType: ProjectType = getProjectType(vscode.Uri.parse(project.uri).fsPath, runtimeSpec.natureIds);
- ret.push({
- name: project.displayName || project.name,
- rootPath: project.uri,
- projectType,
- ...runtimeSpec
- });
+ recordInventoryError(result.errors, `Unable to list projects from Project Manager for workspace "${folderUri}"`, error);
}
}
}
- return ret;
+ return result;
}
-async function getProjectRuntimesFromLS(): Promise {
- const ret: ProjectRuntimeEntry[] = [];
+async function getProjectRuntimesFromLS(): Promise {
+ const result: ProjectRuntimeResult = { entries: [], errors: [] };
const javaExt = vscode.extensions.getExtension("redhat.java");
if (javaExt && javaExt.isActive) {
- let projects: string[] = [];
try {
- projects = await vscode.commands.executeCommand("java.execute.workspaceCommand", "java.project.getAll") || [];
+ const projects = await vscode.commands.executeCommand(
+ "java.execute.workspaceCommand", "java.project.getAll"
+ ) || [];
+ for (const projectRoot of projects) {
+ await addProjectRuntime(result, projectRoot);
+ }
} catch (error) {
- // LS not ready
- }
-
- for (const projectRoot of projects) {
- const runtimeSpec = await getRuntimeSpec(projectRoot);
- const projectType: ProjectType = await getProjectType(vscode.Uri.parse(projectRoot).fsPath, runtimeSpec.natureIds);
- ret.push({
- name: getProjectNameFromUri(projectRoot),
- rootPath: projectRoot,
- projectType: projectType,
- ...runtimeSpec
- });
+ recordInventoryError(result.errors, "Unable to list projects from the Java language server", error);
}
}
- return ret;
+ return result;
}
async function getRuntimeSpec(projectRootUri: string) {
- let natureIds;
- let runtimePath;
- let sourceLevel;
const javaExt = vscode.extensions.getExtension("redhat.java");
- if (javaExt && javaExt.isActive) {
- const NATURE_IDS = "org.eclipse.jdt.ls.core.natureIds";
- const SOURCE_LEVEL_KEY = "org.eclipse.jdt.core.compiler.source";
- const VM_INSTALL_PATH = "org.eclipse.jdt.ls.core.vm.location";
- try {
- const settings: any = await javaExt.exports.getProjectSettings(projectRootUri, [NATURE_IDS, SOURCE_LEVEL_KEY, VM_INSTALL_PATH]);
- natureIds = settings[NATURE_IDS];
- runtimePath = settings[VM_INSTALL_PATH];
- sourceLevel = settings[SOURCE_LEVEL_KEY];
- } catch (error) {
- console.warn(error);
- }
+ if (!javaExt?.isActive || typeof javaExt.exports?.getProjectSettings !== "function") {
+ throw new Error("Project settings are not available from redhat.java.");
+ }
+ const NATURE_IDS = "org.eclipse.jdt.ls.core.natureIds";
+ const SOURCE_LEVEL_KEY = "org.eclipse.jdt.core.compiler.source";
+ const VM_INSTALL_PATH = "org.eclipse.jdt.ls.core.vm.location";
+ const settings: Record | undefined = await javaExt.exports.getProjectSettings(
+ projectRootUri, [NATURE_IDS, SOURCE_LEVEL_KEY, VM_INSTALL_PATH]
+ );
+ const natureIds = settings?.[NATURE_IDS];
+ if (!Array.isArray(natureIds) || !natureIds.every((natureId: unknown) => typeof natureId === "string")) {
+ throw new Error("Project type information is unavailable.");
}
+ const runtimePath = settings?.[VM_INSTALL_PATH];
+ const sourceLevel = settings?.[SOURCE_LEVEL_KEY];
return {
natureIds,
- runtimePath,
- sourceLevel
+ runtimePath: typeof runtimePath === "string" ? runtimePath : undefined,
+ sourceLevel: typeof sourceLevel === "string" ? sourceLevel : undefined
};
}
diff --git a/src/java-runtime/types.ts b/src/java-runtime/types.ts
index 0ae5bd32..9f8d2ffb 100644
--- a/src/java-runtime/types.ts
+++ b/src/java-runtime/types.ts
@@ -21,7 +21,21 @@ export interface JdkData {
export interface ProjectRuntimeEntry {
name: string;
rootPath: string;
- runtimePath: string;
- sourceLevel: string;
+ runtimePath?: string;
+ sourceLevel?: string;
projectType: ProjectType;
}
+
+export interface ToolingRuntimeInfo {
+ requiredJdkVersion: number;
+ javaDotHome?: string;
+ toolingJreVersion?: number;
+ javaHomeError?: string;
+ javaHomeWarning?: string;
+}
+
+export interface JavaRuntimeData extends ToolingRuntimeInfo {
+ javaRuntimes: JavaRuntimeEntry[];
+ projectRuntimes: ProjectRuntimeEntry[];
+ projectJdkError?: string;
+}
diff --git a/src/java-runtime/utils/upstreamApi.ts b/src/java-runtime/utils/upstreamApi.ts
index a8ed888d..e53dc336 100644
--- a/src/java-runtime/utils/upstreamApi.ts
+++ b/src/java-runtime/utils/upstreamApi.ts
@@ -1,197 +1,83 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
-// based on https://github.com/redhat-developer/vscode-java/blob/c4cdbed1190fc705c364179dab525645acf03898/src/requirements.ts
-
-import * as fse from "fs-extra";
-import { findRuntimes, getRuntime, getSources, IJavaRuntime, JAVAC_FILENAME, JAVA_FILENAME } from 'jdk-utils';
+import { compareVersions } from "compare-versions";
import * as path from "path";
import * as vscode from "vscode";
-import { env, workspace } from 'vscode';
+import { ToolingRuntimeInfo } from "../types";
const expandHomeDir = require("expand-home-dir");
-export const REQUIRED_JDK_VERSION = 17;
-export async function resolveRequirements(): Promise<{
+export interface RequirementsData {
tooling_jre: string | undefined; // Used to launch Java extension.
tooling_jre_version: number;
java_home: string | undefined; // Used as default project JDK.
java_version: number;
-}> {
- const javaExtPath: string | undefined = vscode.extensions.getExtension("redhat.java")?.extensionPath
- let toolingJre: string | undefined = await findEmbeddedJRE(javaExtPath);
- let toolingJreVersion: number = await getMajorVersion(toolingJre);
- return new Promise(async (resolve, reject) => {
- let source: string;
- const javaPreferences = checkJavaPreferences();
- let preferenceName = javaPreferences.preference;
- let javaVersion: number = 0;
- let javaHome = javaPreferences.javaHome;
- if (javaHome) { // java.jdt.ls.java.home or java.home setting has highest priority.
- source = `java.home variable defined in ${env.appName} settings`;
- javaHome = expandHomeDir(javaHome);
- if (!await fse.pathExists(javaHome!)) {
- invalidJavaHome(reject, `The ${source} points to a missing or inaccessible folder (${javaHome})`);
- } else if (!await fse.pathExists(path.resolve(javaHome!, 'bin', JAVAC_FILENAME))) {
- let msg: string;
- if (await fse.pathExists(path.resolve(javaHome!, JAVAC_FILENAME))) {
- msg = `'bin' should be removed from the ${source} (${javaHome})`;
- } else {
- msg = `The ${source} (${javaHome}) does not point to a JDK.`;
- }
- invalidJavaHome(reject, msg);
- }
- javaVersion = await getMajorVersion(javaHome);
- if (preferenceName === "java.jdt.ls.java.home" || !toolingJre) {
- toolingJre = javaHome;
- toolingJreVersion = javaVersion;
- }
- }
-
- // java.home not specified, search valid JDKs from env.JAVA_HOME, env.PATH, SDKMAN, jEnv, jabba, Common directories
- const javaRuntimes = await findRuntimes({checkJavac: true, withVersion: true, withTags: true});
- if (!toolingJre) { // universal version
- // as latest version as possible.
- sortJdksByVersion(javaRuntimes);
- const validJdks = javaRuntimes.filter(r => r.version && r.version.major >= REQUIRED_JDK_VERSION);
- if (validJdks.length > 0) {
- sortJdksBySource(validJdks);
- javaHome = validJdks[0].homedir;
- javaVersion = validJdks[0].version?.major ?? 0;
- toolingJre = javaHome;
- toolingJreVersion = javaVersion;
- }
- } else { // pick a default project JDK/JRE
- /**
- * For legacy users, we implicitly following the order below to
- * set a default project JDK during initialization:
- * java.home > env.JDK_HOME > env.JAVA_HOME > env.PATH
- *
- * We'll keep it for compatibility.
- */
- if (javaHome && (await getRuntime(javaHome) !== undefined)) {
- const runtime = await getRuntime(javaHome, {withVersion: true});
- if (runtime) {
- javaHome = runtime.homedir;
- javaVersion = runtime.version?.major ?? 0;
- }
- } else if (javaRuntimes.length) {
- sortJdksBySource(javaRuntimes);
- javaHome = javaRuntimes[0].homedir;
- javaVersion = javaRuntimes[0].version?.major ?? 0;
- } else if (javaHome = (await findDefaultRuntimeFromSettings() ?? "")) {
- javaVersion = await getMajorVersion(javaHome);
- } else {
- /**
- * Originally it was:
- * invalidJavaHome(reject, "Please download and install a JDK to compile your project. You can configure your projects with different JDKs by the setting ['java.configuration.runtimes'](https://github.com/redhat-developer/vscode-java/wiki/JDK-Requirements#java.configuration.runtimes)");
- *
- * here we focus on tooling jre, so we swallow the error.
- *
- */
- }
- }
-
-
- if (!toolingJre || toolingJreVersion < REQUIRED_JDK_VERSION) {
- // For universal version, we still require users to install a qualified JDK to run Java extension.
- invalidJavaHome(reject, `Java ${REQUIRED_JDK_VERSION} or more recent is required to run the Java extension. Please download and install a recent JDK. You can still compile your projects with older JDKs by configuring ['java.configuration.runtimes'](https://github.com/redhat-developer/vscode-java/wiki/JDK-Requirements#java.configuration.runtimes)`);
- }
-
- resolve({
- tooling_jre: toolingJre, // Used to launch Java extension.
- tooling_jre_version: toolingJreVersion,
- java_home: javaHome, // Used as default project JDK.
- java_version: javaVersion,
- });
- });
}
-async function findEmbeddedJRE(javaExtPath?: string): Promise {
- if (!javaExtPath) {
- return undefined;
+export function getRequiredJdkVersion(): number {
+ const javaExt = vscode.extensions.getExtension("redhat.java");
+ // Installation guidance uses the current release's minimum when the extension is absent.
+ if (!javaExt || compareVersions(javaExt.packageJSON.version, "1.57.0") >= 0) {
+ return 25;
}
- const jreHome = path.join(javaExtPath, "jre");
- if (fse.existsSync(jreHome) && fse.statSync(jreHome).isDirectory()) {
- const candidates = fse.readdirSync(jreHome);
- for (const candidate of candidates) {
- if (fse.existsSync(path.join(jreHome, candidate, "bin", JAVA_FILENAME))) {
- return path.join(jreHome, candidate);
- }
+ const version = javaExt.packageJSON.version;
+ if (compareVersions(version, "1.36.0") >= 0 &&
+ vscode.workspace.getConfiguration().get("java.jdt.ls.javac.enabled") === "on") {
+ if (compareVersions(version, "1.46.0") >= 0) {
+ return 25;
}
- }
-
- return;
-}
-
-async function findDefaultRuntimeFromSettings(): Promise {
- const runtimes = workspace.getConfiguration().get("java.configuration.runtimes");
- if (Array.isArray(runtimes) && runtimes.length) {
- let candidate: string | undefined;
- for (const runtime of runtimes) {
- if (!runtime || typeof runtime !== 'object' || !runtime.path) {
- continue;
- }
-
- const jr = await getRuntime(runtime.path);
- if (jr) {
- candidate = jr.homedir;
- }
-
- if (runtime.default) {
- break;
- }
+ if (compareVersions(version, "1.41.0") >= 0) {
+ return 24;
}
-
- return candidate;
+ return 23;
}
-
- return undefined;
-}
-
-function sortJdksBySource(jdks: IJavaRuntime[]) {
- const rankedJdks = jdks as Array;
- const sources = ["JDK_HOME", "JAVA_HOME", "PATH"];
- for (const [index, source] of sources.entries()) {
- for (const jdk of rankedJdks) {
- if (jdk.rank === undefined && getSources(jdk).includes(source)) {
- jdk.rank = index;
- }
- }
+ if (compareVersions(version, "1.39.0") >= 0) {
+ return 21;
}
- rankedJdks.filter(jdk => jdk.rank === undefined).forEach(jdk => jdk.rank = sources.length);
- rankedJdks.sort((a, b) => a.rank - b.rank);
+ return 17;
}
-/**
- * Sort by major version in descend order.
- */
-function sortJdksByVersion(jdks: IJavaRuntime[]) {
- jdks.sort((a, b) => (b.version?.major ?? 0) - (a.version?.major ?? 0));
-}
-
-
-function checkJavaPreferences(){
- let preference: string = 'java.jdt.ls.java.home';
- let javaHome = workspace.getConfiguration().get('java.jdt.ls.java.home');
- if (!javaHome) { // Read java.home from the deprecated "java.home" setting.
- preference = 'java.home';
- javaHome = workspace.getConfiguration().get('java.home');
+export function getToolingRuntimeInfo(): ToolingRuntimeInfo {
+ const requiredJdkVersion = getRequiredJdkVersion();
+ const info: ToolingRuntimeInfo = { requiredJdkVersion };
+ const javaExt = vscode.extensions.getExtension<{ javaRequirement?: RequirementsData; status?: string }>("redhat.java");
+ if (!javaExt) {
+ info.javaHomeWarning = "Language Support for Java (redhat.java) is unavailable. Check that it is installed and enabled.";
+ return info;
+ }
+ const api = javaExt.isActive ? javaExt.exports : undefined;
+ if (api?.status === "Error") {
+ info.javaHomeError = "redhat.java reports an error. Open the Java logs for details.";
+ }
+ const requirements = api?.javaRequirement;
+ if (!requirements) {
+ return info;
+ }
+ info.javaDotHome = requirements.tooling_jre;
+ info.toolingJreVersion = requirements.tooling_jre_version;
+ if (requirements.tooling_jre && requirements.tooling_jre_version < requiredJdkVersion) {
+ info.javaHomeWarning = `The runtime reported by redhat.java uses Java ${requirements.tooling_jre_version}, below the required Java ${requiredJdkVersion}. Open the Java logs for details.`;
}
- return {
- javaHome,
- preference
- };
-}
-function invalidJavaHome(reject: any, reason: string) {
- reject(new Error(reason));
+ const configuredHome = vscode.workspace.getConfiguration().get("java.jdt.ls.java.home");
+ if (configuredHome && requirements.tooling_jre && !sameHome(expandHomeDir(configuredHome), requirements.tooling_jre)) {
+ // Use the version reported by redhat.java, not another local runtime probe.
+ const configuredVersion = sameHome(expandHomeDir(configuredHome), requirements.java_home)
+ ? requirements.java_version : undefined;
+ const configurationWarning = configuredVersion && configuredVersion < requiredJdkVersion
+ ? `The configured 'java.jdt.ls.java.home' uses Java ${configuredVersion}, below the required Java ${requiredJdkVersion}. redhat.java reports a different language-server runtime.`
+ : "The configured 'java.jdt.ls.java.home' differs from the runtime reported by redhat.java. Open the Java logs for details.";
+ info.javaHomeWarning = [info.javaHomeWarning, configurationWarning].filter(Boolean).join(" ");
+ }
+ return info;
}
-async function getMajorVersion(javaHome?: string): Promise {
- if (!javaHome) {
- return 0;
+function sameHome(left: string, right?: string): boolean {
+ if (!right) {
+ return false;
}
- const runtime = await getRuntime(javaHome, { withVersion: true });
- return runtime?.version?.major || 0;
-}
\ No newline at end of file
+ const normalize = (home: string) => process.platform === "win32"
+ ? path.resolve(home).toLowerCase() : path.resolve(home);
+ return normalize(left) === normalize(right);
+}
diff --git a/test-fixtures/gradle-import/.gitignore b/test-fixtures/gradle-import/.gitignore
new file mode 100644
index 00000000..b345d1d8
--- /dev/null
+++ b/test-fixtures/gradle-import/.gitignore
@@ -0,0 +1,7 @@
+.autotest/
+.classpath
+.gradle/
+.project
+.settings/
+bin/
+build/
diff --git a/test-fixtures/gradle-import/README.md b/test-fixtures/gradle-import/README.md
new file mode 100644
index 00000000..037e86a6
--- /dev/null
+++ b/test-fixtures/gradle-import/README.md
@@ -0,0 +1,36 @@
+# Gradle import fixture
+
+| Plan | Gradle | Daemon/toolchain | Source |
+|---|---|---|---|
+| `java-gradle` | 8.5 | JDK 21 | `src/java21` |
+| `java-gradle-java25` | 9.8.1 | JDK 25 | `src/java25`, including the finalized `ScopedValue` API |
+
+This wrapper-free fixture uses each plan's explicit Gradle version and JVM.
+Configuration fails if the importer uses the wrong Gradle/JDK pair.
+The plans intentionally do not test wrapper precedence or automatic compatible-JDK
+selection; explicit settings avoid conflating those behaviors with import coverage.
+
+Successful IDE import requires freshly generated BSP project/classpath metadata,
+the expected compiler source level, and completion of a Commons Lang API not present
+in the fixture source text. Generic keyword completion is not sufficient.
+The plans also require real `build` and `run` tasks in the Gradle Projects view and
+the expected configuration marker in Gradle for Java output, without task-model errors.
+The temporary invalid completion probe is explicitly reloaded into the editor,
+then removed and saved before building.
+
+CI shares the existing Gradle environment binding with `java-gradle-java27`,
+preserving `JAVA21_HOME`/`JAVA25_HOME` for these cases. The two-argument helper
+reads the provisioned absolute `GRADLE_CI_GRADLE` path from the environment.
+CI does not rewrite its terminal command or override the IDE's Gradle home.
+It also exports
+`ORG_GRADLE_PROJECT_javaRelease` before launching VS Code. The Java import arguments
+alone do not configure the Gradle extension's separate task-model requests.
+For local runs, set the applicable variable, put that Gradle version on `PATH`,
+set `ORG_GRADLE_PROJECT_javaRelease` to `21` or `25`, and update the plan's JDK paths
+for your installation. The terminal verifier runs
+a clean build, checks the actual Gradle and Java versions, program output and class
+file version, and emits its success marker only after all checks pass.
+Generated IDE metadata and build logs are not fixture inputs.
+CI reuses the shared artifact collector: terminal diagnostics are staged outside
+AutoTest's output and archived under `diagnostics/ci/terminal/`; Gradle/Java
+output-channel and extension-host logs are archived under `logs/ide/logs/`.
diff --git a/test-fixtures/gradle-import/build.gradle b/test-fixtures/gradle-import/build.gradle
new file mode 100644
index 00000000..c940ef02
--- /dev/null
+++ b/test-fixtures/gradle-import/build.gradle
@@ -0,0 +1,42 @@
+plugins {
+ id 'java'
+ id 'application'
+}
+
+def javaRelease = (findProperty('javaRelease') ?: '21').toInteger()
+def expectedGradle = [21: '8.5', 25: '9.8.1'][javaRelease]
+if (expectedGradle == null) {
+ throw new GradleException("Unsupported test Java release: ${javaRelease}")
+}
+if (gradle.gradleVersion != expectedGradle || JavaVersion.current().majorVersion != javaRelease.toString()) {
+ throw new GradleException("Expected Gradle ${expectedGradle}/Java ${javaRelease}, got Gradle ${gradle.gradleVersion}/Java ${JavaVersion.current()}")
+}
+logger.lifecycle("GRADLE_MODEL:${gradle.gradleVersion}:${javaRelease}:${JavaVersion.current().majorVersion}")
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(javaRelease)
+ }
+}
+
+sourceSets {
+ main {
+ java.srcDirs = ["src/java${javaRelease}"]
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.release.set(javaRelease)
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.apache.commons:commons-lang3:3.18.0'
+}
+
+application {
+ mainClass = "example.GradleSmoke${javaRelease}"
+}
diff --git a/test-fixtures/gradle-import/settings.gradle b/test-fixtures/gradle-import/settings.gradle
new file mode 100644
index 00000000..1035127c
--- /dev/null
+++ b/test-fixtures/gradle-import/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'gradle-import-regression'
diff --git a/test-fixtures/gradle-import/src/java21/example/GradleSmoke21.java b/test-fixtures/gradle-import/src/java21/example/GradleSmoke21.java
new file mode 100644
index 00000000..1d3e9d39
--- /dev/null
+++ b/test-fixtures/gradle-import/src/java21/example/GradleSmoke21.java
@@ -0,0 +1,10 @@
+package example;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class GradleSmoke21 {
+ public static void main(String[] args) {
+ String dependency = StringUtils.upperCase("gradle");
+ System.out.println("GRADLE_PROJECT:" + Runtime.version().feature() + ":" + dependency);
+ }
+}
diff --git a/test-fixtures/gradle-import/src/java25/example/GradleSmoke25.java b/test-fixtures/gradle-import/src/java25/example/GradleSmoke25.java
new file mode 100644
index 00000000..7616406f
--- /dev/null
+++ b/test-fixtures/gradle-import/src/java25/example/GradleSmoke25.java
@@ -0,0 +1,12 @@
+package example;
+
+import org.apache.commons.lang3.StringUtils;
+
+public class GradleSmoke25 {
+ public static void main(String[] args) {
+ String dependency = StringUtils.upperCase("gradle");
+ ScopedValue value = ScopedValue.newInstance();
+ String result = ScopedValue.where(value, dependency).call(value::get);
+ System.out.println("GRADLE_PROJECT:" + Runtime.version().feature() + ":" + result);
+ }
+}
diff --git a/test-fixtures/gradle-import/verify.mjs b/test-fixtures/gradle-import/verify.mjs
new file mode 100644
index 00000000..0d7352df
--- /dev/null
+++ b/test-fixtures/gradle-import/verify.mjs
@@ -0,0 +1,89 @@
+import assert from "node:assert/strict";
+import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import path from "node:path";
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+
+const workspace = path.dirname(fileURLToPath(import.meta.url));
+const [release, gradleVersion] = process.argv.slice(2);
+assert.ok(release === "21" || release === "25", "Expected Java 21 or Java 25");
+assert.equal(gradleVersion, release === "21" ? "8.5" : "9.8.1", "Unexpected Gradle/JDK pair");
+const isCI = process.env.GITHUB_ACTIONS === "true";
+const ciGradle = process.env.GRADLE_CI_GRADLE;
+const stagingDirectory = process.env.AUTOTEST_STAGING_DIR;
+const javaHome = process.env[`JAVA${release}_HOME`];
+assert.ok(javaHome, `Set JAVA${release}_HOME to the project JDK`);
+if (isCI) {
+ assert.ok(ciGradle && path.isAbsolute(ciGradle), "CI requires an absolute GRADLE_CI_GRADLE");
+ assert.ok(stagingDirectory && path.isAbsolute(stagingDirectory), "CI requires an absolute AUTOTEST_STAGING_DIR");
+ assert.ok(path.isAbsolute(javaHome), `Expected an absolute project JDK path: ${javaHome}`);
+}
+if (stagingDirectory) assert.ok(path.isAbsolute(stagingDirectory), "AUTOTEST_STAGING_DIR must be absolute");
+const gradleExecutable = isCI ? ciGradle : "gradle";
+
+const env = { ...process.env, JAVA_HOME: javaHome };
+const pathKey = Object.keys(env).find(key => key.toLowerCase() === "path") ?? "PATH";
+env[pathKey] = `${path.join(javaHome, "bin")}${path.delimiter}${env[pathKey] ?? ""}`;
+const java = path.join(javaHome, "bin", process.platform === "win32" ? "java.exe" : "java");
+const logDirectory = stagingDirectory ? path.join(stagingDirectory, "terminal") : path.join(workspace, ".autotest");
+mkdirSync(logDirectory, { recursive: true });
+writeFileSync(path.join(logDirectory, "toolchain.json"), JSON.stringify({
+ node: process.execPath,
+ nodeVersion: process.version,
+ gradle: gradleExecutable,
+ gradleVersion,
+ javaHome,
+ javaRelease: release,
+ projectJavaRelease: process.env.ORG_GRADLE_PROJECT_javaRelease ?? null,
+ workspace,
+}, null, 2));
+process.on("uncaughtExceptionMonitor", error => {
+ writeFileSync(path.join(logDirectory, "failure.log"), error.stack ?? String(error));
+});
+if (process.env.GITHUB_ACTIONS === "true") {
+ assert.equal(process.env.ORG_GRADLE_PROJECT_javaRelease, release,
+ `CI requires javaRelease=${release} to be shared with the IDE, not only passed to the terminal`);
+}
+console.log(`Node ${process.version}: ${process.execPath}`);
+console.log(`Gradle ${gradleVersion}: ${gradleExecutable}`);
+console.log(`JDK ${release}: ${javaHome}`);
+
+function run(command, args, logName, windowsVerbatimArguments = false) {
+ const result = spawnSync(command, args, {
+ cwd: workspace,
+ env,
+ encoding: "utf8",
+ timeout: 240_000,
+ maxBuffer: 16 * 1024 * 1024,
+ windowsVerbatimArguments,
+ });
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
+ writeFileSync(path.join(logDirectory, logName), output);
+ if (result.error) throw result.error;
+ assert.equal(result.status, 0, `Command failed (${result.status ?? result.signal}): ${output}`);
+ return output;
+}
+
+function gradle(args, logName) {
+ return process.platform === "win32"
+ ? run(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `""${gradleExecutable}" ${args.join(" ")}"`], logName, true)
+ : run(gradleExecutable, args, logName);
+}
+
+const javaVersion = run(java, ["-XshowSettings:properties", "-version"], "java-version.log");
+assert.match(javaVersion, new RegExp(`java\\.specification\\.version\\s*=\\s*${release}(?:\\r?\\n|$)`));
+const toolVersion = gradle(["--version"], "gradle-version.log");
+assert.ok(toolVersion.split(/\r?\n/).includes(`Gradle ${gradleVersion}`), toolVersion);
+
+const output = gradle(
+ ["--no-daemon", "--console=plain", "--quiet", `-PjavaRelease=${release}`, "clean", "classes", "run"],
+ "build-and-run.log",
+);
+console.log(output);
+assert.ok(output.split(/\r?\n/).includes(`GRADLE_PROJECT:${release}:GRADLE`), output);
+
+const bytecode = readFileSync(path.join(workspace, "build", "classes", "java", "main", "example", `GradleSmoke${release}.class`));
+assert.equal(bytecode.readUInt32BE(0), 0xcafebabe, "Expected a Java class file");
+assert.equal(bytecode.readUInt16BE(6), Number(release) + 44, "Wrong target Java bytecode");
+assert.equal(bytecode.readUInt16BE(4), 0, "These cases do not require preview features");
+console.log(`GRADLE_JAVA${release}_BUILD_PASSED`);
diff --git a/test-fixtures/java27/README.md b/test-fixtures/java27/README.md
index f3b8bd29..14b137a8 100644
--- a/test-fixtures/java27/README.md
+++ b/test-fixtures/java27/README.md
@@ -45,8 +45,10 @@ For `java-gradle-java27`, CI also exports
an absolute `JAVA27_CI_GRADLE` and uses that executable for the actual build
instead of resolving Gradle from the terminal's PATH. Missing or relative CI
paths fail explicitly, even when diagnostics are disabled; a failed CI build
-does not retry with PATH Gradle. Node, the existing JDK 27 configuration and
-terminal profiles are unchanged.
+does not retry with PATH Gradle. Java 21/25 import plans reuse this environment
+binding mechanism; no preparation script rewrites terminal commands or overrides
+the IDE's Gradle home. Node invocation, JDK 27 configuration, helper arguments,
+and terminal profiles remain unchanged.
The helper persists its actual terminal Node/PATH/JDK identity, selected Gradle
command, command exit metadata, and full compiler/runtime output outside
@@ -60,7 +62,7 @@ Java 27 bytecode and execution. The control never replaces the original failure
or prints the plan's success marker. CI already uses that executable for the
primary build and does not run a duplicate control after failure.
-Gradle preparation only resolves the provisioned executable, verifies version
+Java 27 Gradle preparation resolves the provisioned executable, verifies version
9.8.1 and binds it for the terminal helper. Version output is saved under
`diagnostics/ci/gradle-version.log`, not printed into the workflow log.
For release comparisons, use `test_plan=java-gradle-java27`, `pre_release=false`, and an exact
diff --git a/test-plans/java-gradle-java25.yaml b/test-plans/java-gradle-java25.yaml
index 2462f6e1..c5901a47 100644
--- a/test-plans/java-gradle-java25.yaml
+++ b/test-plans/java-gradle-java25.yaml
@@ -1,71 +1,191 @@
-# Test Plan: Gradle Java 25 (from vscode-java-pack.wiki)
-#
-# Source: wiki Test-Plan.md "Gradle - Java 11" scenario (upgraded to Java 25)
-# Verify: Open Gradle project with JDK 25 β LS ready β editing experience works
-#
-# Uses the eclipse.jdt.ls subprojects project (Gradle 8.5)
-#
-# Prerequisites:
-# - eclipse.jdt.ls repo is cloned locally
-# - JDK 25 is installed (C:\Program Files\Java\jdk-25)
-#
-# Usage: autotest run test-plans/java-gradle-java25.yaml
-
-name: "Gradle Java 25 β JDK 25 Compatibility"
+# Gradle 9.8.1 compatibility: the language server, Gradle and project use JDK 25.
+# Prerequisites: JDK 25, JAVA25_HOME, Gradle 9.8.1, ORG_GRADLE_PROJECT_javaRelease=25.
+# CI binds the IDE/terminal toolchain and saves Gradle diagnostics in test-results.
+# Explicit JDK selection is intentional; this does not test automatic fallback.
+
+name: "Gradle Java 25 β Import, Dependency Completion and Build"
description: |
- Corresponds to the Gradle Java 11 scenario in the wiki Test Plan (upgraded to Java 25):
- Open a Gradle 8.5 project with JDK 25,
- verify the language server starts normally and the editing experience works.
+ Import a real Java 25 project with Gradle 9.8.1 and a JDK 25 daemon.
+ Verify BSP import, the Gradle task model, Java 25 source level and dependency-backed completion,
+ then build and run a Java 25 ScopedValue program with Java 25 bytecode.
+ The incomplete completion probe may temporarily produce a diagnostic;
+ it is removed and saved before the final zero-error and build checks.
setup:
extension: "redhat.java"
extensions:
- "vscjava.vscode-java-pack"
vscodeVersion: "stable"
- workspace: "../../eclipse.jdt.ls/org.eclipse.jdt.ls.tests/projects/gradle/subprojects"
+ workspace: "../test-fixtures/gradle-import"
timeout: 180
settings:
+ java.help.showReleaseNotes: false
+ java.help.firstView: "none"
java.jdt.ls.java.home: "C:\\Program Files\\Java\\jdk-25"
+ java.configuration.updateBuildConfiguration: "automatic"
+ java.configuration.runtimes:
+ - name: "JavaSE-25"
+ path: "C:\\Program Files\\Java\\jdk-25"
+ default: true
+ java.import.maven.enabled: false
+ java.import.gradle.enabled: true
+ java.import.gradle.wrapper.enabled: false
+ java.import.gradle.version: "9.8.1"
+ java.import.gradle.java.home: "C:\\Program Files\\Java\\jdk-25"
+ java.import.gradle.arguments:
+ - "-PjavaRelease=25"
+ java.import.generatesMetadataFilesAtProjectRoot: true
+ java.gradle.buildServer.enabled: "on"
+ java.trace.server: "verbose"
+ gradle.debug: true
steps:
- # ββ Step 1: Wait for LS ready ββββββββββββββββββββββββββββ
- # wiki: "check the status bar icon is π, and there should be no errors"
- id: "ls-ready"
action: "waitForLanguageServer"
- verify: "Gradle subprojects workspace has loaded under JDK 25; Problems panel shows no errors"
verifyProblems:
errors: 0
timeout: 300
- skipLlmVerify: true # waitForLanguageServer is authoritative; LLM only sees the same status bar
+ skipLlmVerify: true
+
+ - id: "show-gradle-task-model"
+ action: "executeVSCodeCommand gradleTasksView.focus"
+ verifyTreeItem:
+ name: "gradle-import-regression"
+ exact: true
+ inView: "Gradle Projects"
+ timeout: 300
+
+ - id: "expand-gradle-tasks"
+ action: "expandTreeItem Tasks"
+ verifyTreeItem:
+ name: "build"
+ exact: true
+ level: 3
+ inView: "Gradle Projects"
+
+ - id: "verify-gradle-build-task"
+ action: "expandTreeItem build"
+ verifyTreeItem:
+ name: "build"
+ exact: true
+ level: 4
+ inView: "Gradle Projects"
+
+ - id: "verify-gradle-run-task"
+ action: "expandTreeItem application"
+ verifyTreeItem:
+ name: "run"
+ exact: true
+ level: 4
+ inView: "Gradle Projects"
+
+ - id: "verify-gradle-model-toolchain"
+ action: "executeVSCodeCommand gradle.showLogs"
+ verifyOutputChannel:
+ channel: "Gradle for Java"
+ contains: "GRADLE_MODEL:9.8.1:25:25"
+ notContains: "Error getting build"
+ skipLlmVerify: true
- # ββ Step 2: Open Java file βββββββββββββββββββββββββββββββ
- # wiki: "Open Foo.java, make sure the editing experience is correctly working"
- - id: "open-test1"
- action: "open file Test1.java"
- verify: "Test1.java file is opened in the editor"
+ - id: "verify-gradle-model"
+ action: "open file GradleSmoke25.java"
+ verifyFile:
+ path: "~/.project"
+ matches: '(org\.eclipse\.buildship\.core\.gradleprojectnature|com\.microsoft\.gradle\.bs\.importer\.GradleBuildServerProjectNature) '
+ skipLlmVerify: true
+
+ - id: "verify-java-source"
+ action: "waitForLanguageServer"
+ verifyEditorTab:
+ title: "GradleSmoke25.java"
+ active: true
+ verifyFile:
+ path: "~/.settings/org.eclipse.jdt.core.prefs"
+ contains: "org.eclipse.jdt.core.compiler.source=25"
+ verifyProblems:
+ errors: 0
timeout: 15
- # ββ Step 3: Verify completion βββββββββββββββββββββββββββ
- # verify text describes the rendered popup; verifyCompletion.notEmpty
- # is the deterministic ground truth β kept lenient so a transient
- # "Loadingβ¦" indicator at screenshot time doesn't downgrade.
- - id: "verify-completion"
- action: "triggerCompletionAt endOfMethod"
- verify: "Code completion has been triggered in HelloWorld.java; the IntelliSense popup is being rendered (the language server may briefly show a 'Loading...' indicator while computing suggestions on a cold cache β this is a valid intermediate state since the deterministic verifyCompletion.notEmpty asserts the LS produced completion items)"
- verifyCompletion:
- notEmpty: true
- waitBefore: 8
- # Retry once on cold-cache "Loading..." LLM downgrades.
- retries: 1
+ - id: "verify-bsp-import"
+ action: "waitForLanguageServer"
+ verifyFile:
+ path: "~/.project"
+ contains: "com.microsoft.gradle.bs.importer.GradleBuildServerProjectNature"
+ skipLlmVerify: true
- # ββ Step 4: Verify editing ββββββββββββββββββββββββββββββββ
- - id: "goto-line"
- action: "goToLine 3"
+ - id: "verify-gradle-classpath"
+ action: "saveFile"
+ verifyFile:
+ path: "~/.classpath"
+ matches: 'org\.eclipse\.buildship\.core\.gradleclasspathcontainer|commons-lang3-3\.18\.0\.jar'
+ skipLlmVerify: true
- - id: "goto-end"
- action: "goToEndOfLine"
+ - id: "insert-dependency-probe"
+ action: "insertLineInFile src/java25/example/GradleSmoke25.java 8 String probe = StringUtils.cap;"
+ verifyFile:
+ path: "~/src/java25/example/GradleSmoke25.java"
+ contains: "String probe = StringUtils.cap;"
+ skipLlmVerify: true
- - id: "type-code"
- action: "typeInEditor // JDK 25 gradle test marker"
+ - id: "reload-dependency-probe"
+ action: "executeVSCodeCommand workbench.action.files.revert"
+ verifyEditorTab:
+ title: "GradleSmoke25.java"
+ active: true
verifyEditor:
- contains: "// JDK 25 gradle test marker"
+ contains: "String probe = StringUtils.cap;"
+ skipLlmVerify: true
+
+ - id: "select-completion-line"
+ action: "goToLine 8"
+ verifyEditorTab:
+ title: "GradleSmoke25.java"
+ active: true
+
+ - id: "move-to-probe-end"
+ action: "executeVSCodeCommand cursorEnd"
+
+ - id: "move-before-semicolon"
+ action: "pressKey ArrowLeft"
+
+ - id: "verify-dependency-completion"
+ action: "triggerCompletion"
+ verifyCompletion:
+ contains: ["capitalize"]
+
+ - id: "dismiss-completion"
+ action: "pressKey Escape"
+
+ - id: "select-probe-line"
+ action: "goToLine 8"
+
+ - id: "remove-dependency-probe"
+ action: "executeVSCodeCommand editor.action.deleteLines"
+
+ - id: "save-source"
+ action: "executeVSCodeCommand workbench.action.files.save"
+ verifyFile:
+ path: "~/src/java25/example/GradleSmoke25.java"
+ matches: '^(?![\s\S]*String probe = StringUtils\.cap;)'
+ skipLlmVerify: true
+
+ - id: "open-build-terminal"
+ action: 'executeVSCodeCommand workbench.action.terminal.new {"cwd":"${workspaceFolder}"}'
+ skipLlmVerify: true
+
+ - id: "verify-build-and-run"
+ action: 'executeVSCodeCommand workbench.action.terminal.sendSequence {"text":"node verify.mjs 25 9.8.1\u000d"}'
+ verifyTerminal:
+ contains: "GRADLE_JAVA25_BUILD_PASSED"
+ verifyProblems:
+ errors: 0
+ timeout: 300
+ skipLlmVerify: true
+
+ - id: "verify-gradle-model-remains-healthy"
+ action: "executeVSCodeCommand gradle.showLogs"
+ verifyOutputChannel:
+ channel: "Gradle for Java"
+ contains: "GRADLE_MODEL:9.8.1:25:25"
+ notContains: "Error getting build"
+ skipLlmVerify: true
diff --git a/test-plans/java-gradle.yaml b/test-plans/java-gradle.yaml
index 43d8baa6..96104d61 100644
--- a/test-plans/java-gradle.yaml
+++ b/test-plans/java-gradle.yaml
@@ -1,81 +1,193 @@
-# Test Plan: Gradle Project (from vscode-java-pack.wiki)
-#
-# Source: wiki Test-Plan.md "Gradle" scenario
-# Verify: Open Gradle project β LS ready β no errors β Java file editing experience works
-#
-# Note: Uses the eclipse.jdt.ls subprojects test project (Gradle 8.5),
-# because vscode-java's simple-gradle uses Gradle 4.10.2 which is incompatible with modern JDKs.
-#
-# Prerequisites:
-# - eclipse.jdt.ls repo is cloned locally
-# - JDK is installed and available
-#
-# Usage: autotest run test-plans/java-gradle.yaml
-
-name: "Java Gradle Project β Basic Gradle Project Editing"
+# Gradle 8.5 compatibility: the importer/daemon and project use JDK 21.
+# The language server uses redhat.java's bundled JDK, independently of Gradle.
+# Prerequisites: JDK 21, JAVA21_HOME, Gradle 8.5, ORG_GRADLE_PROJECT_javaRelease=21.
+# CI binds the IDE/terminal toolchain and saves Gradle diagnostics in test-results.
+# Explicit JDK selection is intentional; this does not test automatic fallback.
+
+name: "Java Gradle Project β Gradle 8.5 with JDK 21"
description: |
- Corresponds to the Gradle scenario in the wiki Test Plan:
- Open a Gradle 8.5 project, verify the language server starts without errors,
- and Java file editing experience works (diagnostics, completion).
+ Import a Gradle 8.5 project using JDK 21 for the Gradle daemon.
+ Verify BSP import, the Gradle task model, Java 21 source level and dependency-backed completion,
+ then build and run the saved project with Java 21 bytecode.
+ The incomplete completion probe may temporarily produce a diagnostic;
+ it is removed and saved before the final zero-error and build checks.
setup:
extension: "redhat.java"
extensions:
- "vscjava.vscode-java-pack"
vscodeVersion: "stable"
- workspace: "../../eclipse.jdt.ls/org.eclipse.jdt.ls.tests/projects/gradle/subprojects"
- timeout: 180 # Gradle project import can be slow (needs to download Gradle distribution)
+ workspace: "../test-fixtures/gradle-import"
+ timeout: 180
+ settings:
+ java.help.showReleaseNotes: false
+ java.help.firstView: "none"
+ java.configuration.updateBuildConfiguration: "automatic"
+ java.configuration.runtimes:
+ - name: "JavaSE-21"
+ path: "C:\\Program Files\\Java\\jdk-21"
+ default: true
+ java.import.maven.enabled: false
+ java.import.gradle.enabled: true
+ java.import.gradle.wrapper.enabled: false
+ java.import.gradle.version: "8.5"
+ java.import.gradle.java.home: "C:\\Program Files\\Java\\jdk-21"
+ java.import.gradle.arguments:
+ - "-PjavaRelease=21"
+ java.import.generatesMetadataFilesAtProjectRoot: true
+ java.gradle.buildServer.enabled: "on"
+ java.trace.server: "verbose"
+ gradle.debug: true
steps:
- # ββ Step 1: Wait for Language Server ready ββββββββββββββ
- # wiki: "check the status bar icon is π, and there should be
- # no errors/problems in the problems view."
- id: "ls-ready"
action: "waitForLanguageServer"
- verify: "Gradle workspace has loaded; Problems panel shows no errors"
verifyProblems:
errors: 0
timeout: 300
- skipLlmVerify: true # waitForLanguageServer is authoritative; LLM only sees the same status bar
-
- # ββ Step 2: Open Foo.java and verify editing experience β
- # wiki: "Open Foo.java file, make sure the editing experience
- # is correctly working including diagnostics, code completion
- # and code action"
- - id: "open-foo"
- action: "open file Test1.java"
- verify: "Test1.java file is opened in the editor"
+ skipLlmVerify: true
+
+ - id: "show-gradle-task-model"
+ action: "executeVSCodeCommand gradleTasksView.focus"
+ verifyTreeItem:
+ name: "gradle-import-regression"
+ exact: true
+ inView: "Gradle Projects"
+ timeout: 300
+
+ - id: "expand-gradle-tasks"
+ action: "expandTreeItem Tasks"
+ verifyTreeItem:
+ name: "build"
+ exact: true
+ level: 3
+ inView: "Gradle Projects"
+
+ - id: "verify-gradle-build-task"
+ action: "expandTreeItem build"
+ verifyTreeItem:
+ name: "build"
+ exact: true
+ level: 4
+ inView: "Gradle Projects"
+
+ - id: "verify-gradle-run-task"
+ action: "expandTreeItem application"
+ verifyTreeItem:
+ name: "run"
+ exact: true
+ level: 4
+ inView: "Gradle Projects"
+
+ - id: "verify-gradle-model-toolchain"
+ action: "executeVSCodeCommand gradle.showLogs"
+ verifyOutputChannel:
+ channel: "Gradle for Java"
+ contains: "GRADLE_MODEL:8.5:21:21"
+ notContains: "Error getting build"
+ skipLlmVerify: true
+
+ - id: "verify-gradle-model"
+ action: "open file GradleSmoke21.java"
+ verifyFile:
+ path: "~/.project"
+ matches: '(org\.eclipse\.buildship\.core\.gradleprojectnature|com\.microsoft\.gradle\.bs\.importer\.GradleBuildServerProjectNature) '
+ skipLlmVerify: true
+
+ - id: "verify-java-source"
+ action: "waitForLanguageServer"
+ verifyEditorTab:
+ title: "GradleSmoke21.java"
+ active: true
+ verifyFile:
+ path: "~/.settings/org.eclipse.jdt.core.prefs"
+ contains: "org.eclipse.jdt.core.compiler.source=21"
+ verifyProblems:
+ errors: 0
timeout: 15
- - id: "verify-completion"
- action: "triggerCompletionAt endOfMethod"
- verifyCompletion:
- notEmpty: true
- waitBefore: 3
- # No `verify:` β the on-screen completion menu can lag behind the
- # internal completion API on slower CI runners (screenshot may capture
- # the "Loading..." indicator while the items are already in the list).
- # Deterministic verifyCompletion above is authoritative.
-
- # Verify the editor can type and save normally
- - id: "goto-line"
- action: "goToLine 2"
- verify: "Cursor moved to line 2"
-
- - id: "goto-end"
- action: "goToEndOfLine"
-
- - id: "type-comment"
- action: "typeInEditor // gradle test marker"
- verifyEditor:
- contains: "// gradle test marker"
+ - id: "verify-bsp-import"
+ action: "waitForLanguageServer"
+ verifyFile:
+ path: "~/.project"
+ contains: "com.microsoft.gradle.bs.importer.GradleBuildServerProjectNature"
+ skipLlmVerify: true
- - id: "save-file"
+ - id: "verify-gradle-classpath"
action: "saveFile"
- verify: "Test1.java is saved to disk"
verifyFile:
- path: "~/project1/src/main/java/project1/Test1.java"
- contains: "// gradle test marker"
- # The disk assertion proves saveFile persisted the in-memory edit. The
- # screenshot-only check can misread the small tab dirty-dot transition.
+ path: "~/.classpath"
+ matches: 'org\.eclipse\.buildship\.core\.gradleclasspathcontainer|commons-lang3-3\.18\.0\.jar'
+ skipLlmVerify: true
+
+ # capitalize is absent from both fixture sources, so a word suggestion
+ # from existing source text cannot satisfy this dependency completion check.
+ - id: "insert-dependency-probe"
+ action: "insertLineInFile src/java21/example/GradleSmoke21.java 8 String probe = StringUtils.cap;"
+ verifyFile:
+ path: "~/src/java21/example/GradleSmoke21.java"
+ contains: "String probe = StringUtils.cap;"
+ skipLlmVerify: true
+
+ - id: "reload-dependency-probe"
+ action: "executeVSCodeCommand workbench.action.files.revert"
+ verifyEditorTab:
+ title: "GradleSmoke21.java"
+ active: true
+ verifyEditor:
+ contains: "String probe = StringUtils.cap;"
+ skipLlmVerify: true
+
+ - id: "select-completion-line"
+ action: "goToLine 8"
+ verifyEditorTab:
+ title: "GradleSmoke21.java"
+ active: true
+
+ - id: "move-to-probe-end"
+ action: "executeVSCodeCommand cursorEnd"
+
+ - id: "move-before-semicolon"
+ action: "pressKey ArrowLeft"
+
+ - id: "verify-dependency-completion"
+ action: "triggerCompletion"
+ verifyCompletion:
+ contains: ["capitalize"]
+
+ - id: "dismiss-completion"
+ action: "pressKey Escape"
+
+ - id: "select-probe-line"
+ action: "goToLine 8"
+
+ - id: "remove-dependency-probe"
+ action: "executeVSCodeCommand editor.action.deleteLines"
+
+ - id: "save-source"
+ action: "executeVSCodeCommand workbench.action.files.save"
+ verifyFile:
+ path: "~/src/java21/example/GradleSmoke21.java"
+ matches: '^(?![\s\S]*String probe = StringUtils\.cap;)'
+ skipLlmVerify: true
+
+ - id: "open-build-terminal"
+ action: 'executeVSCodeCommand workbench.action.terminal.new {"cwd":"${workspaceFolder}"}'
+ skipLlmVerify: true
+
+ - id: "verify-build-and-run"
+ action: 'executeVSCodeCommand workbench.action.terminal.sendSequence {"text":"node verify.mjs 21 8.5\u000d"}'
+ verifyTerminal:
+ contains: "GRADLE_JAVA21_BUILD_PASSED"
+ verifyProblems:
+ errors: 0
+ timeout: 300
+ skipLlmVerify: true
+
+ - id: "verify-gradle-model-remains-healthy"
+ action: "executeVSCodeCommand gradle.showLogs"
+ verifyOutputChannel:
+ channel: "Gradle for Java"
+ contains: "GRADLE_MODEL:8.5:21:21"
+ notContains: "Error getting build"
skipLlmVerify: true
diff --git a/tests/gradle-toolchain.test.js b/tests/gradle-toolchain.test.js
new file mode 100644
index 00000000..80a92337
--- /dev/null
+++ b/tests/gradle-toolchain.test.js
@@ -0,0 +1,183 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+const assert = require("node:assert/strict");
+const fs = require("node:fs");
+const path = require("node:path");
+const test = require("node:test");
+const vm = require("node:vm");
+
+const filename = path.join(__dirname, "..", "test-fixtures", "gradle-import", "verify.mjs");
+const source = fs.readFileSync(filename, "utf8").replace(/^import .+;\r?\n/gm, "").replace("import.meta.url", "fixtureUrl");
+
+function execute(platform, release = "21", options = {}) {
+ const paths = platform === "win32" ? path.win32 : path.posix;
+ const root = platform === "win32" ? "C:\\CI Tools" : "/ci/tools with spaces";
+ const workspace = paths.join(root, "workspace");
+ const javaHome = options.javaHome ?? paths.join(root, `jdk-${release}`);
+ const gradle = paths.join(root, "gradle", "bin", platform === "win32" ? "gradle.bat" : "gradle");
+ const stagingDirectory = paths.join(root, "staging");
+ const logDirectory = paths.join(stagingDirectory, "terminal");
+ const gradleVersion = release === "21" ? "8.5" : "9.8.1";
+ const calls = [];
+ const files = new Map();
+ const messages = [];
+ let monitor;
+ const bytecode = Buffer.alloc(8);
+ bytecode.writeUInt32BE(0xcafebabe);
+ bytecode.writeUInt16BE(options.bytecodeMajor ?? Number(release) + 44, 6);
+ const bound = options.bound !== false;
+ const state = {
+ paths, gradle, javaHome, logDirectory, calls, files, messages,
+ run: () => {
+ try {
+ vm.runInNewContext(source, {
+ assert, path: paths, fixtureUrl: "file:///verify.mjs",
+ fileURLToPath: () => paths.join(workspace, "verify.mjs"),
+ mkdirSync: () => {},
+ writeFileSync: (location, content) => files.set(location, content),
+ readFileSync: () => bytecode,
+ console: { log: message => messages.push(message) },
+ process: {
+ argv: ["node", filename, release, gradleVersion],
+ platform, execPath: paths.join(root, "node"), version: "v22.23.2",
+ env: {
+ PATH: paths.join(root, "wrong-tools"),
+ JAVA_HOME: paths.join(root, "wrong-jdk"),
+ [`JAVA${release}_HOME`]: javaHome,
+ GRADLE_CI_GRADLE: bound ? options.gradle ?? gradle : undefined,
+ AUTOTEST_STAGING_DIR: "staging" in options ? options.staging :
+ options.ci === false ? undefined : stagingDirectory,
+ GITHUB_ACTIONS: options.ci === false ? undefined : "true",
+ ORG_GRADLE_PROJECT_javaRelease: "projectRelease" in options ? options.projectRelease : release,
+ },
+ on: (event, callback) => { if (event === "uncaughtExceptionMonitor") monitor = callback; },
+ },
+ spawnSync: (command, args, spawnOptions) => {
+ calls.push({ command, args, options: spawnOptions });
+ if (calls.length === 1) {
+ return { status: 0, stdout: "", stderr: ` java.specification.version = ${options.javaRelease ?? release}\n` };
+ }
+ if (calls.length === 2) {
+ return { status: 0, stdout: `Gradle ${options.gradleVersion ?? gradleVersion}\n`, stderr: "" };
+ }
+ if (options.spawnError) {
+ return { error: options.spawnError, stdout: "", stderr: "Could not start Gradle\n" };
+ }
+ return {
+ status: options.buildStatus ?? 0,
+ stdout: options.output ?? `GRADLE_PROJECT:${release}:GRADLE\n`,
+ stderr: options.stderr ?? "",
+ };
+ },
+ }, { filename });
+ } catch (error) {
+ if (monitor) monitor(error);
+ throw error;
+ }
+ },
+ };
+ return state;
+}
+
+for (const platform of ["win32", "linux", "darwin"]) {
+ for (const release of ["21", "25"]) {
+ test(`${platform} Java ${release} uses bound tools despite conflicting PATH and JDK environment`, () => {
+ const s = execute(platform, release);
+ s.run();
+ assert.equal(s.calls[0].command, s.paths.join(s.javaHome, "bin", platform === "win32" ? "java.exe" : "java"));
+ assert.equal(s.calls[0].options.env.JAVA_HOME, s.javaHome);
+ assert.equal(s.calls[2].options.env.JAVA_HOME, s.javaHome);
+ if (platform === "win32") {
+ assert.equal(s.calls[1].command, "cmd.exe");
+ assert.equal(s.calls[1].args[3], `""${s.gradle}" --version"`);
+ assert.equal(s.calls[1].options.windowsVerbatimArguments, true);
+ } else {
+ assert.equal(s.calls[1].command, s.gradle);
+ assert.equal(s.calls[2].command, s.gradle);
+ }
+ const identity = JSON.parse(s.files.get(s.paths.join(s.logDirectory, "toolchain.json")));
+ assert.equal(identity.gradle, s.gradle);
+ assert.equal(identity.javaHome, s.javaHome);
+ assert.equal(identity.projectJavaRelease, release);
+ assert.ok(s.files.has(s.paths.join(s.logDirectory, "build-and-run.log")));
+ assert.ok(s.messages.includes(`GRADLE_JAVA${release}_BUILD_PASSED`));
+ });
+ }
+}
+
+test("CI cannot silently fall back to PATH without the shared Gradle environment binding", () => {
+ const s = execute("darwin", "21", { bound: false });
+ assert.throws(s.run, /CI requires an absolute GRADLE_CI_GRADLE/);
+ assert.equal(s.calls.length, 0);
+});
+
+test("relative bound executable paths are rejected before any command runs", () => {
+ const s = execute("linux", "21", { gradle: "gradle" });
+ assert.throws(s.run, /CI requires an absolute GRADLE_CI_GRADLE/);
+ assert.equal(s.calls.length, 0);
+});
+
+for (const staging of [undefined, "relative-staging"]) {
+ test(`CI rejects ${staging === undefined ? "missing" : "relative"} diagnostic staging`, () => {
+ const s = execute("linux", "21", { staging });
+ assert.throws(s.run, /CI requires an absolute AUTOTEST_STAGING_DIR/);
+ assert.equal(s.calls.length, 0);
+ });
+}
+
+test("CI rejects a relative project JDK before running commands", () => {
+ const s = execute("linux", "21", { javaHome: "relative-jdk" });
+ assert.throws(s.run, /Expected an absolute project JDK path/);
+ assert.equal(s.calls.length, 0);
+});
+
+for (const projectRelease of [undefined, "21"]) {
+ test(`Java 25 CI rejects a ${projectRelease === undefined ? "missing" : "conflicting"} shared project release`, () => {
+ const s = execute("darwin", "25", { projectRelease });
+ assert.throws(s.run, /CI requires javaRelease=25 to be shared with the IDE/);
+ assert.equal(s.calls.length, 0);
+ assert.match(s.files.get(s.paths.join(s.logDirectory, "failure.log")), /CI requires javaRelease=25/);
+ assert.ok(!s.messages.includes("GRADLE_JAVA25_BUILD_PASSED"));
+ });
+}
+
+test("local two-argument calls preserve PATH Gradle and project JDK environment behavior", () => {
+ const s = execute("linux", "21", { bound: false, ci: false });
+ s.run();
+ assert.equal(s.calls[1].command, "gradle");
+ assert.equal(s.calls[0].options.env.JAVA_HOME, s.javaHome);
+});
+
+for (const [name, options, expected] of [
+ ["wrong Gradle version", { gradleVersion: "9.7.1" }, /Gradle 9\.7\.1/],
+ ["wrong JDK release", { javaRelease: "25" }, /java\.specification\.version = 25/],
+ ["failed build", { buildStatus: 1, stderr: "Build failed\n" }, /Build failed/],
+ ["wrong program output", { output: "Unexpected output\n" }, /Unexpected output/],
+ ["wrong bytecode", { bytecodeMajor: 69 }, /Wrong target Java bytecode/],
+ ["spawn failure", { spawnError: new Error("ENOENT") }, /ENOENT/],
+]) {
+ test(`${name} remains a failure and preserves diagnostics outside the temporary workspace`, () => {
+ const s = execute("darwin", "21", options);
+ assert.throws(s.run, expected);
+ assert.ok(s.files.has(s.paths.join(s.logDirectory, "toolchain.json")));
+ assert.match(s.files.get(s.paths.join(s.logDirectory, "failure.log")), expected);
+ assert.ok(!s.messages.includes("GRADLE_JAVA21_BUILD_PASSED"));
+ });
+}
+
+for (const plan of ["java-gradle", "java-gradle-java25"]) {
+ test(`${plan} verifies the reloaded probe before consuming completion`, () => {
+ const content = fs.readFileSync(path.join(__dirname, "..", "test-plans", `${plan}.yaml`), "utf8");
+ const ids = [...content.matchAll(/^\s*- id: "([^"]+)"/gm)].map(match => match[1]);
+ assert.match(content, /java\.gradle\.buildServer\.enabled: "on"/);
+ assert.ok(ids.indexOf("ls-ready") < ids.indexOf("show-gradle-task-model"));
+ assert.ok(ids.indexOf("show-gradle-task-model") < ids.indexOf("expand-gradle-tasks"));
+ assert.ok(ids.indexOf("expand-gradle-tasks") < ids.indexOf("verify-gradle-build-task"));
+ assert.ok(ids.indexOf("insert-dependency-probe") < ids.indexOf("reload-dependency-probe"));
+ assert.ok(ids.indexOf("reload-dependency-probe") < ids.indexOf("verify-dependency-completion"));
+ assert.ok(ids.indexOf("remove-dependency-probe") < ids.indexOf("save-source"));
+ assert.ok(ids.indexOf("save-source") < ids.indexOf("verify-build-and-run"));
+ assert.ok(ids.indexOf("verify-gradle-model-remains-healthy") > ids.indexOf("verify-build-and-run"));
+ });
+}
diff --git a/tests/java-runtime.test.js b/tests/java-runtime.test.js
new file mode 100644
index 00000000..995060d6
--- /dev/null
+++ b/tests/java-runtime.test.js
@@ -0,0 +1,667 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+const assert = require("node:assert/strict");
+const fs = require("node:fs");
+const path = require("node:path");
+const test = require("node:test");
+const vm = require("node:vm");
+const ts = require("typescript");
+
+function loadSource(relativePath, mocks, warnings, globals = {}, resolve = require) {
+ const filename = path.join(__dirname, "..", relativePath);
+ const { outputText } = ts.transpileModule(fs.readFileSync(filename, "utf8"), {
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2016, esModuleInterop: true, jsx: ts.JsxEmit.ReactJSX },
+ fileName: filename,
+ });
+ const module = { exports: {} };
+ vm.runInNewContext(outputText, {
+ module, exports: module.exports, process, performance,
+ require: name => Object.prototype.hasOwnProperty.call(mocks, name) ? mocks[name] : resolve(name),
+ console: { warn: warning => warnings.push(warning), error: error => warnings.push(error) },
+ ...globals,
+ }, { filename });
+ return module.exports;
+}
+
+function setupComponents() {
+ const { renderToStaticMarkup } = require("react-dom/server");
+ const cache = new Map();
+ const messages = [];
+ let receive;
+ let html;
+ const globals = {
+ acquireVsCodeApi: () => ({ postMessage: message => messages.push(message) }),
+ document: { getElementById: () => ({}) },
+ window: { addEventListener: (_, listener) => { receive = listener; } },
+ };
+ const mocks = {
+ "react-dom/client": { createRoot: () => ({ render: element => { html = renderToStaticMarkup(element); } }) },
+ };
+ function load(relativePath) {
+ if (!cache.has(relativePath)) {
+ cache.set(relativePath, loadSource(relativePath, mocks, [], globals, name => {
+ if (name.endsWith(".scss") || name.startsWith("@vscode-elements/")) {
+ return {};
+ }
+ if (!name.startsWith(".")) {
+ return require(name);
+ }
+ const source = path.resolve(__dirname, "..", path.dirname(relativePath), name);
+ const filename = fs.existsSync(source + ".tsx") ? source + ".tsx" : source + ".ts";
+ return load(path.relative(path.join(__dirname, ".."), filename));
+ }));
+ }
+ return cache.get(relativePath);
+ }
+ return {
+ load, messages,
+ show: args => {
+ receive({ data: { command: "showJavaRuntimeEntries", args } });
+ return html;
+ },
+ };
+}
+
+function setup(version = "1.57.0") {
+ const extension = { packageJSON: { version }, isActive: false, exports: {} };
+ const projectManager = { isActive: false };
+ const preferences = {};
+ const discovered = [];
+ const writes = [];
+ const warnings = [];
+ const messages = [];
+ const calls = { discovery: 0, inspection: 0, activation: 0 };
+ const runtimes = new Map();
+ let receiveMessage;
+ extension.activate = async () => { calls.activation++; return extension.exports; };
+ const jdkUtils = {
+ findRuntimes: async () => { calls.discovery++; return [...discovered]; },
+ getRuntime: async home => { calls.inspection++; return runtimes.get(home); },
+ };
+ const panel = {
+ webview: {
+ cspSource: "test", asWebviewUri: uri => uri.fsPath,
+ onDidReceiveMessage: callback => { receiveMessage = callback; return { dispose() {} }; },
+ postMessage: async message => { messages.push(message); return true; },
+ },
+ onDidDispose: () => ({ dispose() {} }),
+ };
+ const vscode = {
+ extensions: {
+ getExtension: id => id === "redhat.java" ? extension :
+ id === "vscjava.vscode-java-dependency" ? projectManager : undefined,
+ },
+ Uri: { file: fsPath => ({ fsPath }), parse: uri => ({ fsPath: new URL(uri).pathname }) },
+ ViewColumn: { One: 1 }, ConfigurationTarget: { Global: 1 },
+ window: {
+ createWebviewPanel: () => panel, showOpenDialog: async () => undefined,
+ showWarningMessage: async message => warnings.push(message),
+ showErrorMessage: async message => warnings.push(message),
+ },
+ workspace: {
+ getConfiguration: section => ({
+ get: key => preferences[section ? `${section}.${key}` : key],
+ update: async (key, value) => {
+ const setting = section ? `${section}.${key}` : key;
+ writes.push({ setting, value });
+ preferences[setting] = value;
+ },
+ }),
+ },
+ commands: { executeCommand: async () => [] },
+ };
+ const mocks = { vscode, "jdk-utils": jdkUtils, "expand-home-dir": home => home };
+ const api = loadSource(path.join("src", "java-runtime", "utils", "upstreamApi.ts"), mocks, warnings);
+ const webviewUtils = loadSource(path.join("src", "utils", "webview.ts"), {}, warnings);
+ const jdt = loadSource(path.join("src", "utils", "jdt.ts"), {
+ vscode, "./webview": webviewUtils,
+ }, warnings);
+ const loadView = () => loadSource(path.join("src", "java-runtime", "index.ts"), {
+ ...mocks, "../utils": { getNonce: () => "nonce" }, "../utils/jdt": jdt, "../utils/webview": webviewUtils,
+ "./utils/misc": {}, "./utils/upstreamApi": api,
+ }, warnings);
+ const addJdk = (major, hasJavac = true) => {
+ const homedir = path.join(path.parse(__dirname).root, "jdks", `jdk-${major}-${runtimes.size}`);
+ const runtime = { homedir, hasJavac, version: { major } };
+ runtimes.set(homedir, runtime);
+ return runtime;
+ };
+ const report = (tooling, project = tooling) => {
+ extension.isActive = true;
+ extension.exports.javaRequirement = {
+ tooling_jre: tooling.homedir, tooling_jre_version: tooling.version.major,
+ java_home: project.homedir, java_version: project.version.major,
+ };
+ };
+ const openView = async () => {
+ const view = loadView();
+ await view.javaRuntimeCmdHandler({
+ extensionPath: __dirname, asAbsolutePath: relativePath => path.join(__dirname, relativePath),
+ subscriptions: [],
+ }, "unit-test");
+ return view;
+ };
+ return {
+ api, extension, projectManager, preferences, discovered, writes, warnings, messages, calls, jdkUtils, vscode,
+ addJdk, report, loadView, openView,
+ send: (command, payload = {}) => receiveMessage({ command, ...payload }),
+ };
+}
+
+function setupInventory(s, source, projects) {
+ s.projectManager.isActive = source === "PM";
+ s.vscode.workspace.workspaceFolders = [{ uri: { toString: () => "file:///workspace" } }];
+ s.vscode.commands.executeCommand = async (_, command) =>
+ command === "java.project.list" ?
+ projects.map(uri => ({ uri, name: path.basename(new URL(uri).pathname) })) : projects;
+ s.extension.exports.getProjectSettings = async () => ({
+ "org.eclipse.jdt.ls.core.natureIds": ["org.eclipse.m2e.core.maven2Nature"],
+ "org.eclipse.jdt.core.compiler.source": "17",
+ "org.eclipse.jdt.ls.core.vm.location": "project-jdk",
+ });
+}
+
+for (const [version, minimum] of [
+ ["1.38.0", 17], ["1.39.0", 21], ["1.56.0", 21],
+ ["1.57.0", 25], ["1.57.2026093000", 25], ["1.58.0", 25],
+]) {
+ test(`redhat.java ${version} shares a JDK ${minimum} tooling minimum`, () => {
+ const s = setup(version);
+ assert.equal(s.api.getRequiredJdkVersion(), minimum);
+ assert.equal(s.api.getToolingRuntimeInfo().requiredJdkVersion, minimum);
+ });
+}
+
+test("explicit javac mode follows upstream JDK requirement boundaries without runtime probing", () => {
+ for (const [version, minimum] of [
+ ["1.35.0", 17], ["1.36.0", 23], ["1.38.0", 23], ["1.39.0", 23], ["1.40.0", 23],
+ ["1.41.0", 24], ["1.45.0", 24], ["1.46.0", 25], ["1.56.0", 25], ["1.57.0", 25],
+ ]) {
+ const s = setup(version);
+ s.preferences["java.jdt.ls.javac.enabled"] = "on";
+ assert.equal(s.api.getRequiredJdkVersion(), minimum, `redhat.java ${version}`);
+ const info = s.api.getToolingRuntimeInfo();
+ assert.equal(info.requiredJdkVersion, minimum, `redhat.java ${version}`);
+ assert.equal(info.javaHomeError, undefined);
+ assert.deepEqual(s.calls, { discovery: 0, inspection: 0, activation: 0 });
+ assert.equal(s.writes.length, 0);
+ }
+});
+
+test("off and auto javac modes preserve the normal tooling minimum", () => {
+ for (const mode of ["off", "auto"]) {
+ for (const [version, minimum] of [
+ ["1.36.0", 17], ["1.39.0", 21], ["1.41.0", 21], ["1.46.0", 21], ["1.56.0", 21], ["1.57.0", 25],
+ ]) {
+ const s = setup(version);
+ s.preferences["java.jdt.ls.javac.enabled"] = mode;
+ assert.equal(s.api.getRequiredJdkVersion(), minimum, `redhat.java ${version}, javac=${mode}`);
+ }
+ }
+});
+
+test("an inactive extension provides no runtime result and is not probed or activated", () => {
+ const s = setup();
+ s.preferences["java.jdt.ls.java.home"] = "missing-jdk";
+ s.discovered.push(s.addJdk(25), s.addJdk(27));
+ Object.defineProperty(s.extension, "exports", { get() { throw new Error("Do not read an inactive extension's exports"); } });
+ const info = s.api.getToolingRuntimeInfo();
+ assert.equal(info.javaDotHome, undefined);
+ assert.equal(info.javaHomeError, undefined);
+ assert.equal(info.javaHomeWarning, undefined);
+ assert.deepEqual(s.calls, { discovery: 0, inspection: 0, activation: 0 });
+ assert.equal(s.writes.length, 0);
+});
+
+test("an active extension without requirements is unknown, not a startup failure", () => {
+ const s = setup("1.38.0");
+ s.extension.isActive = true;
+ s.extension.exports.status = "Starting";
+ const info = s.api.getToolingRuntimeInfo();
+ assert.equal(info.javaDotHome, undefined);
+ assert.equal(info.javaHomeError, undefined);
+ assert.equal(s.calls.discovery, 0);
+});
+
+for (const version of [17, 21, 25, 27]) {
+ test(`reported JDK ${version} is displayed without replacement or readiness claims`, () => {
+ const s = setup();
+ const actual = s.addJdk(version);
+ s.report(actual);
+ s.discovered.push(s.addJdk(27));
+ const info = s.api.getToolingRuntimeInfo();
+ assert.equal(info.javaDotHome, actual.homedir);
+ assert.equal(info.toolingJreVersion, version);
+ assert.equal(info.javaHomeError, undefined);
+ if (version < 25) {
+ assert.match(info.javaHomeWarning, /below the required Java 25/);
+ }
+ assert.deepEqual(s.calls, { discovery: 0, inspection: 0, activation: 0 });
+ assert.equal(s.writes.length, 0);
+ });
+}
+
+test("an unavailable extension is reported without inventing a JDK failure", () => {
+ const s = setup();
+ s.vscode.extensions.getExtension = () => undefined;
+ const info = s.api.getToolingRuntimeInfo();
+ assert.match(info.javaHomeWarning, /installed and enabled/);
+ assert.equal(info.javaHomeError, undefined);
+});
+
+test("a reported upstream error is displayed without inventing its cause or opening a popup", () => {
+ const s = setup();
+ s.extension.isActive = true;
+ s.extension.exports.status = "Error";
+ const info = s.api.getToolingRuntimeInfo();
+ assert.match(info.javaHomeError, /redhat.java reports an error/);
+ assert.equal(info.javaDotHome, undefined);
+ s.report(s.addJdk(25));
+ assert.match(s.api.getToolingRuntimeInfo().javaHomeError, /redhat.java reports an error/);
+ assert.equal(s.warnings.length, 0);
+});
+
+for (const [version, mode] of [["1.57.0", undefined], ["1.56.0", "on"]]) {
+ test(`a known under-minimum setting for ${version}, javac=${mode} uses only upstream-reported versions`, () => {
+ const s = setup(version);
+ const configured = s.addJdk(21);
+ const actual = s.addJdk(25);
+ s.preferences["java.jdt.ls.java.home"] = configured.homedir;
+ s.preferences["java.jdt.ls.javac.enabled"] = mode;
+ s.report(actual, configured);
+ const info = s.api.getToolingRuntimeInfo();
+ assert.match(info.javaHomeWarning, /uses Java 21, below the required Java 25/);
+ assert.equal(info.javaDotHome, actual.homedir);
+ assert.equal(info.javaHomeError, undefined);
+ assert.equal(s.calls.inspection, 0);
+ assert.equal(s.writes.length, 0);
+ });
+}
+
+test("an unexplained setting difference does not fabricate a version or fallback cause", () => {
+ const s = setup();
+ const actual = s.addJdk(27);
+ s.report(actual);
+ s.preferences["java.jdt.ls.java.home"] = "uninspected-setting";
+ const info = s.api.getToolingRuntimeInfo();
+ assert.match(info.javaHomeWarning, /differs from the runtime reported/);
+ assert.doesNotMatch(info.javaHomeWarning, /below|invalid|missing/);
+ assert.equal(s.calls.inspection, 0);
+});
+
+test("equivalent configured paths do not produce an ignored-setting warning", () => {
+ const s = setup();
+ const actual = s.addJdk(25);
+ s.report(actual);
+ s.preferences["java.jdt.ls.java.home"] = actual.homedir + path.sep;
+ assert.equal(s.api.getToolingRuntimeInfo().javaHomeWarning, undefined);
+});
+
+test("configuration differences do not hide an under-minimum reported runtime", () => {
+ const s = setup();
+ s.report(s.addJdk(21));
+ s.preferences["java.jdt.ls.java.home"] = "different-setting";
+ const info = s.api.getToolingRuntimeInfo();
+ assert.match(info.javaHomeWarning, /below the required Java 25/);
+ assert.match(info.javaHomeWarning, /differs from the runtime reported/);
+});
+
+test("an older default project runtime is not diagnosed as an invalid tooling setting", () => {
+ const s = setup();
+ const project = s.addJdk(8);
+ s.report(s.addJdk(25), project);
+ s.preferences["java.home"] = project.homedir;
+ s.preferences["java.configuration.runtimes"] = [{ path: project.homedir, default: true }];
+ const before = JSON.stringify(s.preferences);
+ assert.equal(s.api.getToolingRuntimeInfo().javaHomeWarning, undefined);
+ assert.equal(JSON.stringify(s.preferences), before);
+ assert.equal(s.writes.length, 0);
+});
+
+test("project discovery errors do not hide or invalidate the reported tooling runtime", async () => {
+ const s = setup();
+ const actual = s.addJdk(25);
+ s.report(actual);
+ s.jdkUtils.findRuntimes = async () => { throw new Error("Project discovery failed"); };
+ const entries = await s.loadView().findJavaRuntimeEntries();
+ assert.equal(entries.javaDotHome, actual.homedir);
+ assert.equal(entries.toolingJreVersion, 25);
+ assert.equal(entries.javaHomeError, undefined);
+ assert.match(entries.projectJdkError, /Project discovery failed/);
+});
+
+test("a failed project inventory is visible without claiming an unknown runtime has failed", async () => {
+ const s = setup();
+ s.jdkUtils.findRuntimes = async () => { throw "Project discovery failed"; };
+ const entries = await s.loadView().findJavaRuntimeEntries();
+ assert.equal(entries.javaDotHome, undefined);
+ assert.equal(entries.javaHomeError, undefined);
+ assert.match(entries.projectJdkError, /Project discovery failed/);
+});
+
+for (const source of ["PM", "LS"]) {
+ test(`${source} preserves tooling and successful projects when settings or nature IDs are unavailable`, async () => {
+ const s = setup();
+ const actual = s.addJdk(25);
+ s.report(actual);
+ setupInventory(s, source, [
+ "file:///before", "file:///failed", "file:///missing-type", "file:///invalid-type", "file:///after",
+ ]);
+ const getSettings = s.extension.exports.getProjectSettings;
+ s.extension.exports.getProjectSettings = async uri => {
+ if (uri === "file:///failed") {
+ throw new Error("Settings request failed");
+ }
+ const settings = await getSettings();
+ if (uri === "file:///missing-type") {
+ delete settings["org.eclipse.jdt.ls.core.natureIds"];
+ } else if (uri === "file:///invalid-type") {
+ settings["org.eclipse.jdt.ls.core.natureIds"] = [null];
+ } else if (uri === "file:///after") {
+ settings["org.eclipse.jdt.ls.core.natureIds"] = [];
+ }
+ return settings;
+ };
+ const entries = await s.loadView().findJavaRuntimeEntries();
+ assert.equal(entries.javaDotHome, actual.homedir);
+ assert.equal(entries.toolingJreVersion, 25);
+ assert.equal(entries.javaHomeError, undefined);
+ assert.deepEqual(Array.from(entries.projectRuntimes, entry => entry.name), ["before", "after"]);
+ assert.deepEqual(Array.from(entries.projectRuntimes, entry => entry.projectType), ["Maven", "Others"]);
+ assert.ok(entries.projectRuntimes.every(entry => entry.sourceLevel === "17"));
+ assert.match(entries.projectJdkError, /failed.*Settings request failed/);
+ assert.match(entries.projectJdkError, /missing-type.*Project type information is unavailable/);
+ assert.match(entries.projectJdkError, /invalid-type.*Project type information is unavailable/);
+ assert.doesNotMatch(entries.projectJdkError, /includes/);
+ assert.equal(s.writes.length, 0);
+ assert.equal(s.calls.activation, 0);
+ });
+}
+
+test("a failed PM list falls back to LS while preserving its diagnostic", async () => {
+ const s = setup();
+ const actual = s.addJdk(25);
+ s.report(actual);
+ setupInventory(s, "PM", ["file:///sample"]);
+ const executeCommand = s.vscode.commands.executeCommand;
+ s.vscode.commands.executeCommand = async (...args) => {
+ if (args[1] === "java.project.list") {
+ throw new Error("PM list failed");
+ }
+ return executeCommand(...args);
+ };
+ const entries = await s.loadView().findJavaRuntimeEntries();
+ assert.equal(entries.javaDotHome, actual.homedir);
+ assert.deepEqual(Array.from(entries.projectRuntimes, entry => entry.name), ["sample"]);
+ assert.match(entries.projectJdkError, /workspace.*PM list failed/);
+});
+
+test("a failed PM workspace does not repeat previous projects or discard successful entries", async () => {
+ const s = setup();
+ s.report(s.addJdk(25));
+ setupInventory(s, "PM", ["file:///sample"]);
+ s.vscode.workspace.workspaceFolders.push({ uri: { toString: () => "file:///failed-workspace" } });
+ const executeCommand = s.vscode.commands.executeCommand;
+ s.vscode.commands.executeCommand = async (...args) => {
+ assert.equal(args[1], "java.project.list");
+ if (args[2] === "file:///failed-workspace") {
+ throw new Error("Workspace list failed");
+ }
+ return executeCommand(...args);
+ };
+ const entries = await s.loadView().findJavaRuntimeEntries();
+ assert.deepEqual(Array.from(entries.projectRuntimes, entry => entry.name), ["sample"]);
+ assert.match(entries.projectJdkError, /failed-workspace.*Workspace list failed/);
+});
+
+test("failed disk and LS inventories publish tooling metadata and separate upstream diagnostics", async () => {
+ const s = setup();
+ const actual = s.addJdk(25);
+ s.report(actual);
+ s.extension.exports.status = "Error";
+ s.jdkUtils.findRuntimes = async () => { throw new Error("Disk scan failed"); };
+ s.vscode.commands.executeCommand = async () => { throw new Error("LS list failed"); };
+ await s.openView();
+ await s.send("onWillListRuntimes");
+ const entries = s.messages.at(-1).args;
+ assert.equal(entries.javaDotHome, actual.homedir);
+ assert.equal(entries.toolingJreVersion, 25);
+ assert.match(entries.javaHomeError, /redhat.java reports an error/);
+ assert.doesNotMatch(entries.javaHomeError, /Disk scan failed|LS list failed/);
+ assert.equal(entries.projectRuntimes.length, 0);
+ assert.match(entries.projectJdkError, /Disk scan failed/);
+ assert.match(entries.projectJdkError, /LS list failed/);
+ assert.ok(s.warnings.every(warning => !String(warning).includes("Unable to refresh Configure Java Runtime")));
+});
+
+test("project cache remains unchanged while refresh reads the latest upstream result", async () => {
+ const s = setup();
+ s.discovered.push(s.addJdk(8));
+ const view = s.loadView();
+ assert.equal((await view.findJavaRuntimeEntries()).javaDotHome, undefined);
+ const actual = s.addJdk(27);
+ s.report(actual);
+ const entries = await view.findJavaRuntimeEntries();
+ assert.equal(entries.javaDotHome, actual.homedir);
+ assert.equal(entries.javaRuntimes[0].majorVersion, 8);
+ assert.equal(s.calls.discovery, 1);
+ assert.equal(s.calls.inspection, 0);
+ assert.equal(s.calls.activation, 0);
+});
+
+for (const version of [25, 27]) {
+ test(`only explicitly locating JDK ${version} saves the tooling setting`, async () => {
+ const s = setup();
+ const selected = s.addJdk(version);
+ const actual = s.addJdk(27);
+ s.report(actual);
+ s.vscode.window.showOpenDialog = async () => [{ fsPath: selected.homedir }];
+ await s.openView();
+ assert.equal(s.writes.length, 0);
+ await s.send("onWillBrowseForJDK");
+ assert.deepEqual(s.writes, [{ setting: "java.jdt.ls.java.home", value: selected.homedir }]);
+ assert.equal(s.messages.at(-1).args.javaDotHome, actual.homedir);
+ assert.equal(s.calls.activation, 0);
+ });
+}
+
+for (const [version, jdk] of [[24, true], [25, false]]) {
+ test(`locating ${jdk ? "an under-minimum JDK" : "a JRE"} only warns, without saving it`, async () => {
+ const s = setup();
+ const invalid = s.addJdk(version, jdk);
+ s.vscode.window.showOpenDialog = async () => [{ fsPath: invalid.homedir }];
+ await s.openView();
+ await s.send("onWillBrowseForJDK");
+ assert.equal(s.writes.length, 0);
+ assert.match(s.warnings.at(-1), jdk ? /Java 25 or more recent is required/ : /not a valid JDK/);
+ });
+}
+
+test("legacy javac tooling selection enforces its minimum without restricting older project JDKs", async () => {
+ for (const [version, minimum] of [["1.36.0", 23], ["1.41.0", 24], ["1.46.0", 25]]) {
+ const s = setup(version);
+ s.preferences["java.jdt.ls.javac.enabled"] = "on";
+ const project = s.addJdk(17);
+ const actual = s.addJdk(minimum);
+ const invalid = s.addJdk(minimum - 1);
+ s.report(actual, project);
+ s.discovered.push(project);
+ const projectConfiguration = JSON.stringify([{ name: "JavaSE-17", path: project.homedir, default: true }]);
+ s.preferences["java.configuration.runtimes"] = JSON.parse(projectConfiguration);
+ s.vscode.window.showOpenDialog = async () => [{ fsPath: invalid.homedir }];
+ await s.openView();
+ await s.send("onWillListRuntimes");
+ assert.equal(s.messages.at(-1).args.javaRuntimes[0].majorVersion, 17);
+ await s.send("onWillBrowseForJDK");
+ assert.equal(s.writes.length, 0, `redhat.java ${version}`);
+ assert.match(s.warnings.at(-1), new RegExp(`Java ${minimum} or more recent is required`));
+
+ s.vscode.window.showOpenDialog = async () => [{ fsPath: actual.homedir }];
+ await s.send("onWillBrowseForJDK");
+ assert.deepEqual(s.writes, [{ setting: "java.jdt.ls.java.home", value: actual.homedir }]);
+ assert.equal(s.messages.at(-1).args.javaDotHome, actual.homedir);
+ assert.equal(s.messages.at(-1).args.javaRuntimes[0].majorVersion, 17);
+ assert.equal(JSON.stringify(s.preferences["java.configuration.runtimes"]), projectConfiguration);
+ assert.equal(s.calls.activation, 0);
+ }
+});
+
+test("a project scan failure is displayed only as a project inventory problem", async () => {
+ const s = setup();
+ s.jdkUtils.findRuntimes = async () => { throw new Error("Project discovery failed"); };
+ await s.openView();
+ await s.send("onWillListRuntimes");
+ assert.match(s.messages.at(-1).args.projectJdkError, /Project discovery failed/);
+ assert.equal(s.messages.at(-1).args.javaHomeError, undefined);
+ assert.equal(s.writes.length, 0);
+ assert.equal(s.calls.activation, 0);
+});
+
+for (const projectType of ["Maven", "Unmanaged folder"]) {
+ test(`component rendering keeps ${projectType} controls when refresh reports an upstream error`, () => {
+ const s = setup();
+ s.report(s.addJdk(25));
+ const components = setupComponents();
+ components.load(path.join("src", "java-runtime", "assets", "index.ts"));
+ const projectRuntimes = [{ name: "sample", rootPath: "file:///sample", projectType, sourceLevel: "17" }];
+ const show = () => components.show({
+ ...s.api.getToolingRuntimeInfo(), javaRuntimes: [], projectRuntimes,
+ });
+ assert.match(show(), /Configure Runtime for Projects/);
+ s.extension.exports.status = "Error";
+ const html = show();
+ assert.match(html, /Configure Runtime for Projects/);
+ assert.match(html, /redhat.java reports an error/);
+ assert.match(html, /href="command:java.open.logs"/);
+ assert.match(html, /sample/);
+ assert.match(html, /title="Edit"/);
+ assert.match(html, /Language server runtime reported by redhat.java/);
+ assert.doesNotMatch(html, /Configure Runtime for Language Server/);
+ assert.equal(components.messages.length, 1);
+ assert.equal(components.messages[0].command, "onWillListRuntimes");
+ });
+}
+
+test("component rendering preserves existing tooling error controls without project entries", () => {
+ const s = setup();
+ s.report(s.addJdk(25));
+ s.extension.exports.status = "Error";
+ const components = setupComponents();
+ components.load(path.join("src", "java-runtime", "assets", "index.ts"));
+ const html = components.show({
+ ...s.api.getToolingRuntimeInfo(), javaRuntimes: [], projectRuntimes: [],
+ });
+ assert.match(html, /Configure Runtime for Language Server/);
+ assert.match(html, /redhat.java reports an error/);
+ assert.match(html, /Locate an Existing JDK<\/b>/);
+ assert.match(html, /Install a New JDK<\/b>/);
+});
+
+for (const status of [undefined, "Error"]) {
+ test(`component rendering shows inventory errors without projects when upstream status is ${status}`, () => {
+ const s = setup();
+ const actual = s.addJdk(25);
+ s.report(actual);
+ s.extension.exports.status = status;
+ const components = setupComponents();
+ components.load(path.join("src", "java-runtime", "assets", "index.ts"));
+ const html = components.show({
+ ...s.api.getToolingRuntimeInfo(), javaRuntimes: [], projectRuntimes: [],
+ projectJdkError: "Unable to list projects: Settings request failed",
+ });
+ assert.match(html, /Language server runtime reported by redhat.java/);
+ assert.ok(html.includes(actual.homedir));
+ assert.match(html, /Settings request failed/);
+ assert.match(html, /href="command:java.open.logs"/);
+ assert.doesNotMatch(html, /No project detected yet/);
+ if (status === "Error") {
+ assert.match(html, /Configure Runtime for Language Server/);
+ assert.match(html, /redhat.java reports an error/);
+ } else {
+ assert.match(html, /Project information could not be fully loaded/);
+ }
+ });
+}
+
+test("component rendering leaves unknown metadata informational with manual setup available", () => {
+ const components = setupComponents();
+ components.load(path.join("src", "java-runtime", "assets", "index.ts"));
+ const html = components.show({
+ ...setup().api.getToolingRuntimeInfo(), javaRuntimes: [], projectRuntimes: [],
+ });
+ assert.match(html, /Configure Runtime for Projects/);
+ assert.match(html, /runtime information is not available yet/);
+ assert.match(html, /Locate an Existing JDK<\/b>/);
+ assert.match(html, /Install a New JDK<\/b>/);
+ assert.doesNotMatch(html, /redhat.java reports an error/);
+});
+
+for (const [version, mode, minimum] of [
+ ["1.57.0", undefined, 25], ["1.41.0", "on", 24], ["1.56.0", "on", 25],
+]) {
+ test(`FAQ component for ${version}, javac=${mode} shares the minimum and distinguishes project configuration`, () => {
+ const s = setup(version);
+ s.preferences["java.jdt.ls.javac.enabled"] = mode;
+ const { createElement } = require("react");
+ const { renderToStaticMarkup } = require("react-dom/server");
+ const { default: FaqPanel } = setupComponents().load(path.join("src", "beginner-tips", "assets", "tabs", "FaqPanel.tsx"));
+ const html = renderToStaticMarkup(createElement(FaqPanel, { requiredJdkVersion: s.api.getRequiredJdkVersion() }));
+ assert.match(html, new RegExp(`JDK ${minimum}\\+`));
+ assert.match(html, /href="command:java.runtime">Configure Java Runtime<\/a> page configures project JDKs/);
+ assert.match(html, /href="command:java.open.logs"/);
+ const settingLinks = [...html.matchAll(/href="command:java.webview.runCommand\?([^"]+)"/g)]
+ .map(match => JSON.parse(decodeURIComponent(match[1])));
+ assert.deepEqual(settingLinks.find(link => link.command === "workbench.action.openSettings").args, ["java.jdt.ls.java.home"]);
+ assert.doesNotMatch(html, /guide shows the runtime reported/);
+ });
+}
+
+test("startup preserves welcome and release-note scheduling without runtime preflight or setup", async () => {
+ const s = setup();
+ const scheduled = [];
+ const commands = [];
+ const presented = [];
+ let probes = 0;
+ const Serializer = class {};
+ const noop = () => {};
+ s.preferences["java.help.firstView"] = "welcome";
+ s.preferences["java.help.showReleaseNotes"] = true;
+ s.vscode.languages = { registerCodeActionsProvider: noop };
+ s.vscode.window.registerWebviewPanelSerializer = noop;
+ s.vscode.commands.executeCommand = async command => { commands.push(command); };
+ const extension = loadSource(path.join("src", "extension.ts"), {
+ vscode: s.vscode,
+ "vscode-extension-telemetry-wrapper": { instrumentOperation: (name, action) => context => action(name, context) },
+ "./beginner-tips": { BeginnerTipsViewSerializer: Serializer },
+ "./commands": { initialize: noop },
+ "./daemon": { initDaemon: noop },
+ "./exp": { initialize: noop },
+ "./ext-guide": { JavaExtGuideViewSerializer: Serializer },
+ "./formatter-settings": { initFormatterSettingsEditorProvider: noop },
+ "./formatter-settings/RemoteProfileProvider": { initRemoteProfileProvider: noop },
+ "./install-jdk": { InstallJdkViewSerializer: Serializer },
+ "./java-runtime": {
+ JavaRuntimeViewSerializer: Serializer,
+ validateJavaRuntime: async () => { probes++; return false; },
+ },
+ "./misc": { HelpViewType: { None: "none" }, showReleaseNotesOnStart: () => presented.push("releaseNotes") },
+ "./overview": { OverviewViewSerializer: Serializer },
+ "./providers/CodeActionProvider": { CodeActionProvider: Serializer },
+ "./recommendation": { initialize: noop },
+ "./utils": { initialize: noop },
+ "./utils/globalState": { KEY_SHOW_WHEN_USING_JAVA: "welcome" },
+ "./utils/scheduler": { scheduleAction: async name => { scheduled.push(name); return name; } },
+ "./welcome": { WelcomeViewSerializer: Serializer, showWelcomeWebview: () => presented.push("welcome") },
+ "./project-settings/projectSettingsView": { ProjectSettingsViewSerializer: Serializer },
+ "./utils/telemetryFilter": {},
+ }, s.warnings);
+ await extension.activate({
+ subscriptions: [], globalState: { setKeysForSync: noop, get: () => true },
+ });
+ assert.equal(probes, 0);
+ assert.deepEqual(commands, []);
+ assert.deepEqual(scheduled, ["showFirstView", "showReleaseNotes"]);
+ assert.deepEqual(presented, ["welcome", "releaseNotes"]);
+ assert.equal(s.calls.activation, 0);
+});