From 2032e2f7fce4f083be455abd65dcaec803b6019c Mon Sep 17 00:00:00 2001 From: wenyutang-ms Date: Tue, 13 Oct 2026 17:02:24 +0800 Subject: [PATCH] fix: download only required AutoTest fixtures Use shallow partial fetches and project-only sparse checkouts, skip upstream downloads for local fixtures, and retry fetch/checkout failures up to three times. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a7196b98-8fa1-4856-a96b-663c8c3cd0c9 --- .github/scripts/prepare-autotest-fixtures.ps1 | 67 ++++++++++++++ .github/skills/uitest/SKILL.md | 7 +- .github/workflows/e2e-autotest.yml | 10 +-- tests/autotest-fixtures.test.js | 90 +++++++++++++++++++ 4 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 .github/scripts/prepare-autotest-fixtures.ps1 create mode 100644 tests/autotest-fixtures.test.js diff --git a/.github/scripts/prepare-autotest-fixtures.ps1 b/.github/scripts/prepare-autotest-fixtures.ps1 new file mode 100644 index 00000000..f4d7d206 --- /dev/null +++ b/.github/scripts/prepare-autotest-fixtures.ps1 @@ -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)." diff --git a/.github/skills/uitest/SKILL.md b/.github/skills/uitest/SKILL.md index bd42b686..22d750a4 100644 --- a/.github/skills/uitest/SKILL.md +++ b/.github/skills/uitest/SKILL.md @@ -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. diff --git a/.github/workflows/e2e-autotest.yml b/.github/workflows/e2e-autotest.yml index 200c2804..56e7b112 100644 --- a/.github/workflows/e2e-autotest.yml +++ b/.github/workflows/e2e-autotest.yml @@ -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 diff --git a/tests/autotest-fixtures.test.js b/tests/autotest-fixtures.test.js new file mode 100644 index 00000000..cf961aec --- /dev/null +++ b/tests/autotest-fixtures.test.js @@ -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, []); +});