Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/scripts/prepare-autotest-fixtures.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT license.

param(
[Parameter(Mandatory)]
[ValidatePattern('^[a-z0-9]+(?:-[a-z0-9]+)*$')]
[string] $Plan
)

$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $false

$project = switch ($Plan) {
{ $_ -in @("java-basic-editing", "java-debugger", "java-extension-pack", "java-new-file-snippet", "java-single-file", "java-single-no-workspace") } {
"eclipse/simple-app"
break
}
{ $_ -in @("java-dependency-viewer", "java-maven", "java-webview-migration") } {
"maven/salut"
break
}
"java-maven-java25" { "maven/salut-java11"; break }
"java-maven-multimodule" { "maven/multimodule"; break }
default {
Write-Host "$Plan does not require vscode-java test projects; skipping download."
return
}
}

$destination = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\..\..\vscode-java"))
if (Test-Path -LiteralPath $destination) {
throw "Refusing to overwrite existing fixture directory: $destination"
}

$fixture = "test/resources/projects/$project"
Write-Host "Downloading vscode-java fixture: $fixture"
& git init --quiet $destination
if ($LASTEXITCODE -ne 0) { throw "Failed to initialize $destination (exit $LASTEXITCODE)." }
& git -C $destination remote add origin https://github.com/redhat-developer/vscode-java.git
if ($LASTEXITCODE -ne 0) { throw "Failed to configure fixture remote (exit $LASTEXITCODE)." }
& git -C $destination config remote.origin.promisor true
if ($LASTEXITCODE -ne 0) { throw "Failed to configure partial clone (exit $LASTEXITCODE)." }
& git -C $destination config remote.origin.partialclonefilter blob:none
if ($LASTEXITCODE -ne 0) { throw "Failed to configure blob filtering (exit $LASTEXITCODE)." }

# Non-cone mode avoids downloading unrelated files at the repository root.
& git -C $destination sparse-checkout set --no-cone "/$fixture/"
if ($LASTEXITCODE -ne 0) { throw "Failed to configure sparse checkout (exit $LASTEXITCODE)." }

for ($attempt = 1; $attempt -le 3; $attempt++) {
& git -C $destination fetch --depth 1 --filter=blob:none origin HEAD
$downloadExit = $LASTEXITCODE
if ($downloadExit -eq 0) {
& git -C $destination checkout --detach FETCH_HEAD
$downloadExit = $LASTEXITCODE
}
if ($downloadExit -eq 0) {
Write-Host "Downloaded $fixture to $destination."
return
}
if ($attempt -lt 3) {
Write-Warning "Fixture download attempt $attempt/3 failed (exit $downloadExit); retrying in 10 seconds."
Start-Sleep -Seconds 10
}
}

throw "Failed to download vscode-java fixture $fixture after 3 attempts (exit $downloadExit)."
7 changes: 4 additions & 3 deletions .github/skills/uitest/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ The repository uses `@vscjava/vscode-autotest`: YAML plans in `test-plans/*.yaml

- Node.js >= 18 and JDK 21+ installed and on `PATH`.
- Close any running VS Code instance before running a plan locally; a running instance can block AutoTest from launching its own VS Code.
- When a plan references external sample projects, clone them as siblings of this repo first (CI does this automatically):
- When a plan references a `vscode-java` sample project, download its fixture as a sibling of this repo first (CI uses the same helper):

```powershell
git clone --depth 1 https://github.com/redhat-developer/vscode-java.git ../vscode-java
git clone --depth 1 https://github.com/eclipse-jdtls/eclipse.jdt.ls.git ../eclipse.jdt.ls
.\.github\scripts\prepare-autotest-fixtures.ps1 -Plan java-basic-editing
```

The helper uses a shallow, blob-filtered fetch and sparse checkout of only the plan's test project, with up to three attempts for fetch/checkout failures. It refuses to overwrite an existing `vscode-java` sibling; existing local clones can be used as-is. Plans using local fixtures skip the download, and no current plan requires an `eclipse.jdt.ls` clone. When adding a plan that uses an upstream project, update the helper's plan-to-project mapping.

## Workflow

1. Identify the scenario and search `test-plans/*.yaml` for an existing plan that already covers the area.
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/e2e-autotest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ jobs:
"JAVA27_DIAGNOSTICS_DIR=$terminalDirectory" >> $env:GITHUB_ENV
}

- name: Clone vscode-java (test projects)
run: git clone --depth 1 https://github.com/redhat-developer/vscode-java.git ../vscode-java

- name: Clone eclipse.jdt.ls (Gradle test projects)
run: git clone --depth 1 https://github.com/eclipse-jdtls/eclipse.jdt.ls.git ../eclipse.jdt.ls
- name: Download required test project
shell: pwsh
env:
AUTOTEST_PLAN: ${{ matrix.plan }}
run: ./.github/scripts/prepare-autotest-fixtures.ps1 -Plan $env:AUTOTEST_PLAN

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand Down
90 changes: 90 additions & 0 deletions tests/autotest-fixtures.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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 { spawnSync } = require("node:child_process");
const test = require("node:test");

const root = path.join(__dirname, "..");
const helper = path.join(root, ".github", "scripts", "prepare-autotest-fixtures.ps1");
const source = fs.readFileSync(helper, "utf8");
const hasPowerShell = spawnSync("pwsh", ["-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"]).status === 0;
const quote = value => `'${value.replaceAll("'", "''")}'`;

function execute(plan, options = {}) {
const script = `
$global:calls = [System.Collections.Generic.List[object]]::new()
$global:waits = [System.Collections.Generic.List[int]]::new()
$global:failures = ${options.failures ?? 0}
function git {
$global:calls.Add(@($args | ForEach-Object { [string] $_ }))
$global:LASTEXITCODE = 0
if ($args -contains ${quote(options.failCommand ?? "fetch")} -and $global:failures -gt 0) {
$global:failures--
$global:LASTEXITCODE = 1
}
}
function Start-Sleep { param([int] $Seconds) $global:waits.Add($Seconds) }
function Test-Path { param([string] $LiteralPath) return $${options.existing ? "true" : "false"} }
$failure = $null
try {
& {
${source.replaceAll("$PSScriptRoot", quote(path.dirname(helper)))}
} -Plan ${quote(plan)}
} catch { $failure = $_.Exception.Message }
Write-Output ("__RESULT__" + (@{ calls = @($global:calls.ToArray()); waits = @($global:waits.ToArray()); failure = $failure } | ConvertTo-Json -Depth 5 -Compress))
`;
const result = spawnSync("pwsh", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
const output = result.stdout.split(/\r?\n/).find(line => line.startsWith("__RESULT__"));
assert.ok(output, result.stdout);
return JSON.parse(output.slice("__RESULT__".length));
}

test("each plan downloads only its referenced upstream project, or skips upstream downloads", { skip: !hasPowerShell }, () => {
for (const filename of fs.readdirSync(path.join(root, "test-plans")).filter(name => name.endsWith(".yaml"))) {
const plan = filename.slice(0, -5);
const content = fs.readFileSync(path.join(root, "test-plans", filename), "utf8");
const fixture = content.match(/^\s+(?:workspace|file): "\.\.\/\.\.\/vscode-java\/(test\/resources\/projects\/(?:eclipse|maven)\/[^/"]+)/m)?.[1];
const result = execute(plan);
assert.equal(result.failure, null, plan);
if (!fixture) {
assert.deepEqual(result.calls, [], plan);
continue;
}
const destination = path.resolve(root, "..", "vscode-java");
assert.deepEqual(result.calls.find(args => args.includes("sparse-checkout")), [
"-C", destination, "sparse-checkout", "set", "--no-cone", `/${fixture}/`,
], plan);
assert.deepEqual(result.calls.find(args => args.includes("fetch")), [
"-C", destination, "fetch", "--depth", "1", "--filter=blob:none", "origin", "HEAD",
], plan);
assert.ok(result.calls.some(args => args.includes("remote.origin.promisor") && args.includes("true")), plan);
assert.ok(result.calls.some(args => args.includes("remote.origin.partialclonefilter") && args.includes("blob:none")), plan);
assert.equal(result.calls.filter(args => args.includes("checkout")).length, 1, plan);
}
});

for (const failCommand of ["fetch", "checkout"]) {
test(`a transient ${failCommand} failure retries and succeeds`, { skip: !hasPowerShell }, () => {
const result = execute("java-maven", { failCommand, failures: 1 });
assert.equal(result.failure, null);
assert.equal(result.calls.filter(args => args.includes("fetch")).length, 2);
assert.deepEqual(result.waits, [10]);
});

test(`persistent ${failCommand} failures stop after three attempts`, { skip: !hasPowerShell }, () => {
const result = execute("java-maven", { failCommand, failures: 3 });
assert.match(result.failure, /after 3 attempts \(exit 1\)/);
assert.equal(result.calls.filter(args => args.includes("fetch")).length, 3);
assert.deepEqual(result.waits, [10, 10]);
});
}

test("existing sibling clones are never overwritten", { skip: !hasPowerShell }, () => {
const result = execute("java-maven", { existing: true });
assert.match(result.failure, /Refusing to overwrite existing fixture directory/);
assert.deepEqual(result.calls, []);
});
Loading