diff --git a/test/sanity/README.md b/test/sanity/README.md index be312b3dac5..d854439a472 100644 --- a/test/sanity/README.md +++ b/test/sanity/README.md @@ -16,7 +16,7 @@ Use -g or -f command-line options to filter tests to match the host platform. ### Command-Line Options -| Option | Alias | Description | +|Option|Alias|Description| |--------|-------|-------------| |`--commit `|`-c`|The commit to test (required)| |`--quality `|`-q`|The quality to test (required, "stable", "insider" or "exploration")| @@ -43,7 +43,7 @@ npm run sanity-test -- --commit 19228f26df517fecbfda96c20956f7c521e072be --quali Platform-specific scripts are provided in the `scripts/` directory to set up the environment and run tests: -| Script | Platform | Description | +|Script|Platform|Description| |--------|----------|-------------| |`run-win32.cmd`|Windows|Runs tests using Edge as the Playwright browser| |`run-macOS.sh`|macOS|Installs Playwright WebKit and runs tests| @@ -55,7 +55,7 @@ Platform-specific scripts are provided in the `scripts/` directory to set up the The `run-docker.sh` script accepts the following options: -| Option | Description | +|Option|Description| |--------|-------------| |`--container `|Container dockerfile name (required, e.g., "ubuntu", "alpine")| |`--arch `|Target architecture: amd64, arm64, or arm (default: amd64)| @@ -67,7 +67,7 @@ All other arguments are passed through to the sanity test runner. Docker container definitions are provided in the `containers/` directory for testing on various Linux distributions: -| Container | Base Image | Description | +|Container|Base Image|Description| |-----------|------------|-------------| |`alpine`|Alpine 3.x|Alpine Linux with musl libc| |`centos`|CentOS Stream 9|RHEL-compatible distribution| @@ -103,7 +103,7 @@ Sanity tests run in Azure Pipelines via the `product-sanity-tests.yml` pipeline. ### Pipeline Parameters -| Parameter | Description | +|Parameter|Description| |-----------|-------------| |`buildQuality`|The quality of the build to test: "exploration", "insider", or "stable"| |`buildCommit`|The published build commit SHA| diff --git a/test/sanity/src/cli.test.ts b/test/sanity/src/cli.test.ts index 4db34cf390d..d22761ee92c 100644 --- a/test/sanity/src/cli.test.ts +++ b/test/sanity/src/cli.test.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { spawn } from 'child_process'; +import { Browser } from 'playwright'; import { TestContext } from './context.js'; +import { GitHubAuth } from './githubAuth.js'; +import { UITest } from './uiTest.js'; export function setup(context: TestContext) { context.test('cli-alpine-arm64', ['alpine', 'arm64'], async () => { @@ -72,45 +74,77 @@ export function setup(context: TestContext) { } const result = context.runNoErrors(entryPoint, '--version'); - const version = result.stdout.trim(); - assert.ok(version.includes(`(commit ${context.options.commit})`), `Expected CLI version to include commit ${context.options.commit}, got: ${version}`); + const version = result.stdout.trim().match(/\(commit ([a-f0-9]+)\)/)?.[1]; + assert.strictEqual(version, context.options.commit, `Expected commit ${context.options.commit} but got ${version}`); - const workspaceDir = context.createTempDir(); - process.chdir(workspaceDir); - context.log(`Changed current directory to: ${workspaceDir}`); + if (!context.capabilities.has('github-account')) { + return; + } - const args = [ - '--cli-data-dir', context.createTempDir(), - '--user-data-dir', context.createTempDir(), - 'tunnel', - '--accept-server-license-terms', - '--server-data-dir', context.createTempDir(), - '--extensions-dir', context.createTempDir(), - ]; + const cliDataDir = context.createTempDir(); + const test = new UITest(context); + const auth = new GitHubAuth(context); + let browser: Browser | undefined; - context.log(`Running CLI ${entryPoint} with args ${args.join(' ')}`); - const cli = spawn(entryPoint, args, { detached: true }); + context.log('Logging out of Dev Tunnel to ensure fresh authentication'); + context.run(entryPoint, '--cli-data-dir', cliDataDir, 'tunnel', 'user', 'logout'); - cli.stderr.on('data', (data) => { - context.error(`[CLI Error] ${data.toString().trim()}`); - }); + context.log('Starting Dev Tunnel to local server using CLI'); + await context.runCliApp('CLI', entryPoint, + [ + '--cli-data-dir', cliDataDir, + 'tunnel', + '--accept-server-license-terms', + '--server-data-dir', context.createTempDir(), + '--extensions-dir', test.extensionsDir, + '--verbose' + ], + async (line) => { + const deviceCode = /To grant access .* use code ([A-Z0-9-]+)/.exec(line)?.[1]; + if (deviceCode) { + context.log(`Device code detected: ${deviceCode}, starting device flow authentication`); + browser = await context.launchBrowser(); + await auth.runDeviceCodeFlow(browser, deviceCode); + return; + } - cli.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[CLI Output] ${line}`); - }); + const tunnelUrl = /Open this link in your browser (https?:\/\/[^\s]+)/.exec(line)?.[1]; + if (tunnelUrl) { + const tunnelId = new URL(tunnelUrl).pathname.split('/').pop()!; + const url = context.getTunnelUrl(tunnelUrl, test.workspaceDir); + context.log(`CLI started successfully with tunnel URL: ${url}`); - const match = /Using GitHub for authentication/.exec(text); - if (match !== null) { - context.log(`CLI started successfully and is waiting for authentication`); - context.killProcessTree(cli.pid!); + if (!browser) { + throw new Error('Browser instance is not available'); + } + + context.log(`Navigating to ${url}`); + const page = await context.getPage(browser.newPage()); + await page.goto(url); + + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); + + context.log('Selecting GitHub Account'); + await page.locator('span.monaco-highlighted-label', { hasText: 'GitHub' }).click(); + + context.log('Clicking Allow on confirmation dialog'); + await page.getByRole('button', { name: 'Allow' }).click(); + + await auth.runUserWebFlow(page); + + context.log('Waiting for connection to be established'); + await page.getByRole('button', { name: `remote ${tunnelId}` }).waitFor({ timeout: 5 * 60 * 1000 }); + + await test.run(page); + + context.log('Closing browser'); + await browser.close(); + + test.validate(); + return true; + } } - }); - - await new Promise((resolve, reject) => { - cli.on('error', reject); - cli.on('exit', resolve); - }); + ); } } diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts index 873596c0ff6..c0b053ee309 100644 --- a/test/sanity/src/context.ts +++ b/test/sanity/src/context.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { spawnSync, SpawnSyncReturns } from 'child_process'; +import { spawn, spawnSync, SpawnSyncReturns } from 'child_process'; import { createHash } from 'crypto'; import fs from 'fs'; import { test } from 'mocha'; @@ -33,6 +33,7 @@ interface ITargetMetadata { export class TestContext { private static readonly authenticodeInclude = /^.+\.(exe|dll|sys|cab|cat|msi|jar|ocx|ps1|psm1|psd1|ps1xml|pssc1)$/i; private static readonly codesignExclude = /node_modules\/(@parcel\/watcher\/build\/Release\/watcher\.node|@vscode\/deviceid\/build\/Release\/windows\.node|@vscode\/ripgrep\/bin\/rg|@vscode\/spdlog\/build\/Release\/spdlog.node|kerberos\/build\/Release\/kerberos.node|@vscode\/native-watchdog\/build\/Release\/watchdog\.node|node-pty\/build\/Release\/(pty\.node|spawn-helper)|vsda\/build\/Release\/vsda\.node|native-watchdog\/build\/Release\/watchdog\.node)$/; + private static readonly notarizeExclude = /extensions\/microsoft-authentication\/dist\/libmsalruntime\.dylib$/; private readonly tempDirs = new Set(); private readonly wslTempDirs = new Set(); @@ -388,7 +389,7 @@ export class TestContext { this.log(`Validating codesign signature for ${filePath}`); - const result = this.run('codesign', '--verify', '--deep', '--strict', '--verbose', filePath); + const result = this.run('codesign', '--verify', '--deep', '--strict', '--verbose=2', filePath); if (result.error !== undefined) { this.error(`Failed to run codesign: ${result.error.message}`); } @@ -396,6 +397,19 @@ export class TestContext { if (result.status !== 0) { this.error(`Codesign signature is not valid for ${filePath}: ${result.stderr}`); } + + if (!TestContext.notarizeExclude.test(filePath)) { + this.log(`Validating notarization for ${filePath}`); + + const notaryResult = this.run('spctl', '--assess', '--type', 'open', '--context', 'context:primary-signature', '--verbose=2', filePath); + if (notaryResult.error !== undefined) { + this.error(`Failed to run spctl: ${notaryResult.error.message}`); + } + + if (notaryResult.status !== 0) { + this.error(`Notarization is not valid for ${filePath}: ${notaryResult.stderr}`); + } + } } /** @@ -915,17 +929,6 @@ export class TestContext { return dataDir; } - /** - * Returns the tunnel URL for the VS Code server including vscode-version parameter. - * @param baseUrl The base URL for the VS Code server. - * @returns The tunnel URL with vscode-version parameter. - */ - public getTunnelUrl(baseUrl: string): string { - const url = new URL(baseUrl); - url.searchParams.set('vscode-version', this.options.commit); - return url.toString(); - } - /** * Launches a web browser for UI testing. * @returns The launched Browser instance. @@ -993,6 +996,25 @@ export class TestContext { return url; } + /** + * Returns the tunnel URL for the VS Code server. + * @param baseUrl The base URL for *vscode.dev/tunnel connection. + * @param workspaceDir Optional folder path to open + * @returns The tunnel URL with folder in pathname. + */ + public getTunnelUrl(baseUrl: string, workspaceDir?: string): string { + const url = new URL(baseUrl); + url.searchParams.set('vscode-version', this.options.commit); + if (workspaceDir) { + let folder = workspaceDir.replaceAll('\\', '/'); + if (!folder.startsWith('/')) { + folder = `/${folder}`; + } + url.pathname = url.pathname.replace(/\/+$/, '') + folder; + } + return url.toString(); + } + /** * Returns a random alphanumeric token of length 10. */ @@ -1026,4 +1048,63 @@ export class TestContext { } return `~/${serverDir}/extensions`; } + + /** + * Runs a VS Code command-line application (such as server or CLI). + * @param name The name of the app as it will appear in logs. + * @param command Command to run. + * @param args Arguments for the command. + * @param onLine Callback to handle output lines. + */ + public async runCliApp(name: string, command: string, args: string[], onLine: (text: string) => Promise) { + this.log(`Starting ${name} with command line: ${command} ${args.join(' ')}`); + + const app = spawn(command, args, { + shell: /\.(sh|cmd)$/.test(command), + detached: !this.capabilities.has('windows'), + stdio: ['ignore', 'pipe', 'pipe'] + }); + + try { + await new Promise((resolve, reject) => { + app.stderr.on('data', (data) => { + const text = `[${name}] ${data.toString().trim()}`; + if (/ECONNRESET/.test(text)) { + this.log(text); + } else { + reject(new Error(text)); + } + }); + + let terminated = false; + app.stdout.on('data', (data) => { + const text = data.toString().trim(); + if (/\berror\b/.test(text)) { + reject(new Error(`[${name}] ${text}`)); + } + + for (const line of text.split('\n')) { + this.log(`[${name}] ${line}`); + onLine(line).then((result) => { + if (terminated = !!result) { + this.log(`Terminating ${name} process`); + resolve(); + } + }).catch(reject); + } + }); + + app.on('error', reject); + app.on('exit', (code) => { + if (code === 0) { + resolve(); + } else if (!terminated) { + reject(new Error(`[${name}] Exited with code ${code}`)); + } + }); + }); + } finally { + this.killProcessTree(app.pid!); + } + } } diff --git a/test/sanity/src/desktop.test.ts b/test/sanity/src/desktop.test.ts index 2ef0a11925a..8a9b57e6dc3 100644 --- a/test/sanity/src/desktop.test.ts +++ b/test/sanity/src/desktop.test.ts @@ -38,34 +38,37 @@ export function setup(context: TestContext) { context.test('desktop-darwin-x64-dmg', ['darwin', 'x64', 'desktop'], async () => { const packagePath = await context.downloadTarget('darwin-x64-dmg'); + context.validateCodesignSignature(packagePath); if (!context.options.downloadOnly) { - const mountPoint = context.mountDmg(packagePath); - context.validateAllCodesignSignatures(mountPoint); - const entryPoint = context.getDesktopEntryPoint(mountPoint); + const dir = context.mountDmg(packagePath); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getDesktopEntryPoint(dir); await testDesktopApp(entryPoint); - context.unmountDmg(mountPoint); + context.unmountDmg(dir); } }); context.test('desktop-darwin-arm64-dmg', ['darwin', 'arm64', 'desktop'], async () => { const packagePath = await context.downloadTarget('darwin-arm64-dmg'); + context.validateCodesignSignature(packagePath); if (!context.options.downloadOnly) { - const mountPoint = context.mountDmg(packagePath); - context.validateAllCodesignSignatures(mountPoint); - const entryPoint = context.getDesktopEntryPoint(mountPoint); + const dir = context.mountDmg(packagePath); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getDesktopEntryPoint(dir); await testDesktopApp(entryPoint); - context.unmountDmg(mountPoint); + context.unmountDmg(dir); } }); context.test('desktop-darwin-universal-dmg', ['darwin', 'desktop'], async () => { const packagePath = await context.downloadTarget('darwin-universal-dmg'); + context.validateCodesignSignature(packagePath); if (!context.options.downloadOnly) { - const mountPoint = context.mountDmg(packagePath); - context.validateAllCodesignSignatures(mountPoint); - const entryPoint = context.getDesktopEntryPoint(mountPoint); + const dir = context.mountDmg(packagePath); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getDesktopEntryPoint(dir); await testDesktopApp(entryPoint); - context.unmountDmg(mountPoint); + context.unmountDmg(dir); } }); @@ -234,7 +237,6 @@ export function setup(context: TestContext) { ]; args.push(test.workspaceDir); - context.log(`Starting VS Code ${entryPoint} with args ${args.join(' ')}`); const app = await _electron.launch({ executablePath: entryPoint, args }); const window = await context.getPage(app.firstWindow()); diff --git a/test/sanity/src/detectors.ts b/test/sanity/src/detectors.ts index 343efb92a48..ed1cc693099 100644 --- a/test/sanity/src/detectors.ts +++ b/test/sanity/src/detectors.ts @@ -16,7 +16,8 @@ export type Capability = | 'deb' | 'rpm' | 'snap' | 'desktop' | 'browser' - | 'wsl'; + | 'wsl' + | 'github-account'; /** * Detect the capabilities of the current environment. @@ -29,6 +30,7 @@ export function detectCapabilities(): ReadonlySet { detectDesktop(capabilities); detectBrowser(capabilities); detectWSL(capabilities); + detectGitHubAccount(capabilities); return capabilities; } @@ -62,7 +64,7 @@ function detectArch(capabilities: Set) { let arch = os.arch(); if (os.platform() === 'win32') { - const winArch = process.env['PROCESSOR_ARCHITEW6432'] || process.env['PROCESSOR_ARCHITECTURE']; + const winArch = process.env.PROCESSOR_ARCHITEW6432 || process.env.PROCESSOR_ARCHITECTURE; if (winArch === 'ARM64') { arch = 'arm64'; } else if (winArch === 'AMD64') { @@ -131,7 +133,10 @@ function detectBrowser(capabilities: Set) { break; } case 'win32': { - const path = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'; + const path = + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? + `${process.env['ProgramFiles(x86)']}\\Microsoft\\Edge\\Application\\msedge.exe`; + if (fs.existsSync(path)) { capabilities.add('browser'); } @@ -144,14 +149,20 @@ function detectBrowser(capabilities: Set) { * Detect if WSL is available on Windows. */ function detectWSL(capabilities: Set) { - if (os.platform() !== 'win32') { - return; - } - const systemRoot = process.env['SystemRoot']; - if (systemRoot) { - const wslPath = `${systemRoot}\\System32\\wsl.exe`; + if (os.platform() === 'win32') { + const wslPath = `${process.env.SystemRoot}\\System32\\wsl.exe`; if (fs.existsSync(wslPath)) { capabilities.add('wsl'); } } } + +/** + * Detect if GitHub account and password are available in the environment. + */ +function detectGitHubAccount(capabilities: Set) { + if (process.env.GITHUB_ACCOUNT && process.env.GITHUB_PASSWORD) { + capabilities.add('github-account'); + } +} + diff --git a/test/sanity/src/githubAuth.ts b/test/sanity/src/githubAuth.ts new file mode 100644 index 00000000000..d7576d761cd --- /dev/null +++ b/test/sanity/src/githubAuth.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Browser, Page } from 'playwright'; +import { TestContext } from './context.js'; + +/** + * Handles GitHub authentication flows in the browser. + */ +export class GitHubAuth { + // private readonly username = process.env.GITHUB_ACCOUNT; + // private readonly password = process.env.GITHUB_PASSWORD; + + public constructor(private readonly context: TestContext) { } + + /** + * Runs GitHub device authentication flow in a browser. + * @param browser Browser to use. + * @param code Device authentication code to use. + */ + public async runDeviceCodeFlow(browser: Browser, code: string) { + this.context.log(`Running GitHub device flow with code ${code}`); + const page = await browser.newPage(); + await page.goto('https://github.com/login/device'); + } + + /** + * Runs GitHub user authentication flow in the browser. + * @param page Authentication page. + */ + public async runUserWebFlow(page: Page) { + this.context.log(`Running GitHub browser flow at ${page.url()}`); + } +} diff --git a/test/sanity/src/index.ts b/test/sanity/src/index.ts index 1b5bd6ca37f..6439018a9b6 100644 --- a/test/sanity/src/index.ts +++ b/test/sanity/src/index.ts @@ -16,7 +16,7 @@ const options = minimist(process.argv.slice(2), { }); if (options.help) { - console.info('Usage: npm run sanity-test -- [options]'); + console.info(`Usage: node ${path.basename(process.argv[1])} [options]`); console.info('Options:'); console.info(' --commit, -c The commit to test (required)'); console.info(` --quality, -q The quality to test (required, "stable", "insider" or "exploration")`); @@ -36,7 +36,7 @@ if (options.help) { const testResults = options['test-results']; const mochaOptions: MochaOptions = { color: true, - timeout: (options.timeout ?? 600) * 1000, + timeout: (options.timeout ?? 10 * 60) * 1000, slow: 3 * 60 * 1000, grep: options.grep, fgrep: options.fgrep, diff --git a/test/sanity/src/server.test.ts b/test/sanity/src/server.test.ts index 580c29e0156..ff2384f5c9a 100644 --- a/test/sanity/src/server.test.ts +++ b/test/sanity/src/server.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { spawn } from 'child_process'; import { TestContext } from './context.js'; export function setup(context: TestContext) { @@ -71,54 +70,30 @@ export function setup(context: TestContext) { return; } - const args = [ - '--accept-server-license-terms', - '--connection-token', context.getRandomToken(), - '--host', '0.0.0.0', - '--port', context.getUniquePort(), - '--server-data-dir', context.createTempDir(), - '--extensions-dir', context.createTempDir(), - ]; + await context.runCliApp('Server', entryPoint, + [ + '--accept-server-license-terms', + '--connection-token', context.getRandomToken(), + '--host', '0.0.0.0', + '--port', context.getUniquePort(), + '--server-data-dir', context.createTempDir(), + '--extensions-dir', context.createTempDir() + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return false; + } - context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`); - const detached = !context.capabilities.has('windows'); - const server = spawn(entryPoint, args, { shell: true, detached }); + const url = new URL('version', context.getWebServerUrl(port)).toString(); - let testError: Error | undefined; + context.log(`Fetching version from ${url}`); + const response = await context.fetchNoErrors(url); + const version = await response.text(); + assert.strictEqual(version, context.options.commit, `Expected commit ${context.options.commit} but got ${version}`); - server.stderr.on('data', (data) => { - context.error(`[Server Error] ${data.toString().trim()}`); - }); - - server.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[Server Output] ${line}`); - }); - - const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; - if (port) { - const url = context.getWebServerUrl(port); - url.pathname = '/version'; - runWebTest(url.toString()) - .catch((error) => { testError = error; }) - .finally(() => context.killProcessTree(server.pid!)); + return true; } - }); - - await new Promise((resolve, reject) => { - server.on('error', reject); - server.on('exit', resolve); - }); - - if (testError) { - throw testError; - } - } - - async function runWebTest(url: string) { - const response = await context.fetchNoErrors(url); - const text = await response.text(); - assert.strictEqual(text, context.options.commit, `Expected commit ${context.options.commit} but got ${text}`); + ); } } diff --git a/test/sanity/src/serverWeb.test.ts b/test/sanity/src/serverWeb.test.ts index 1a5cc7a21e3..89084cb6c6c 100644 --- a/test/sanity/src/serverWeb.test.ts +++ b/test/sanity/src/serverWeb.test.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { spawn } from 'child_process'; import { TestContext } from './context.js'; import { UITest } from './uiTest.js'; @@ -73,68 +72,39 @@ export function setup(context: TestContext) { const token = context.getRandomToken(); const test = new UITest(context); - const args = [ - '--accept-server-license-terms', - '--port', context.getUniquePort(), - '--connection-token', token, - '--server-data-dir', context.createTempDir(), - '--extensions-dir', test.extensionsDir, - '--user-data-dir', test.userDataDir - ]; + await context.runCliApp('Server', entryPoint, + [ + '--accept-server-license-terms', + '--port', context.getUniquePort(), + '--connection-token', token, + '--server-data-dir', context.createTempDir(), + '--extensions-dir', test.extensionsDir, + '--user-data-dir', test.userDataDir + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return false; + } - context.log(`Starting server ${entryPoint} with args ${args.join(' ')}`); - const detached = !context.capabilities.has('windows'); - const server = spawn(entryPoint, args, { shell: true, detached }); - - let testError: Error | undefined; - - server.stderr.on('data', (data) => { - const text = data.toString().trim(); - if (!/ECONNRESET/.test(text)) { - context.error(`[Server Error] ${text}`); - } - }); - - server.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[Server Output] ${line}`); - }); - - const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; - if (port) { const url = context.getWebServerUrl(port, token, test.workspaceDir).toString(); - runUITest(url, test) - .catch((error) => { testError = error; }) - .finally(() => context.killProcessTree(server.pid!)); + const browser = await context.launchBrowser(); + const page = await context.getPage(browser.newPage()); + + context.log(`Navigating to ${url}`); + await page.goto(url, { waitUntil: 'networkidle' }); + + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); + + await test.run(page); + + context.log('Closing browser'); + await browser.close(); + + test.validate(); + return true; } - }); - - await new Promise((resolve, reject) => { - server.on('error', reject); - server.on('exit', resolve); - }); - - if (testError) { - throw testError; - } - } - - async function runUITest(url: string, test: UITest) { - const browser = await context.launchBrowser(); - const page = await context.getPage(browser.newPage()); - - context.log(`Navigating to ${url}`); - await page.goto(url, { waitUntil: 'networkidle' }); - - context.log('Waiting for the workbench to load'); - await page.waitForSelector('.monaco-workbench'); - - await test.run(page); - - context.log('Closing browser'); - await browser.close(); - - test.validate(); + ); } } diff --git a/test/sanity/src/wsl.test.ts b/test/sanity/src/wsl.test.ts index d5c7212607f..cd54740ac98 100644 --- a/test/sanity/src/wsl.test.ts +++ b/test/sanity/src/wsl.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { spawn } from 'child_process'; import { _electron } from 'playwright'; import { TestContext } from './context.js'; import { UITest } from './uiTest.js'; @@ -59,55 +58,32 @@ export function setup(context: TestContext) { return; } - const wslPath = context.toWslPath(entryPoint); - const args = [ - '--accept-server-license-terms', - '--connection-token', context.getRandomToken(), - '--host', '0.0.0.0', - '--port', context.getUniquePort(), - '--server-data-dir', context.createWslTempDir(), - '--extensions-dir', context.createWslTempDir(), - ]; + await context.runCliApp('WSL Server', 'wsl', + [ + context.toWslPath(entryPoint), + '--accept-server-license-terms', + '--connection-token', context.getRandomToken(), + '--host', '0.0.0.0', + '--port', context.getUniquePort(), + '--server-data-dir', context.createWslTempDir(), + '--extensions-dir', context.createWslTempDir(), + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return; + } - context.log(`Starting server in WSL: ${wslPath} with args ${args.join(' ')}`); - const server = spawn('wsl', [wslPath, ...args], { shell: true }); + const url = new URL('version', context.getWebServerUrl(port)).toString(); - let testError: Error | undefined; + context.log(`Fetching version from ${url}`); + const response = await context.fetchNoErrors(url); + const version = await response.text(); + assert.strictEqual(version, context.options.commit, `Expected commit ${context.options.commit} but got ${version}`); - server.stderr.on('data', (data) => { - context.error(`[WSL Server Error] ${data.toString().trim()}`); - }); - - server.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[WSL Server Output] ${line}`); - }); - - const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; - if (port) { - const url = context.getWebServerUrl(port); - url.pathname = '/version'; - runWebTest(url.toString()) - .catch((error) => { testError = error; }) - .finally(() => context.killProcessTree(server.pid!)); + return true; } - }); - - await new Promise((resolve, reject) => { - server.on('error', reject); - server.on('exit', resolve); - }); - - if (testError) { - throw testError; - } - } - - async function runWebTest(url: string) { - const response = await context.fetchNoErrors(url); - const text = await response.text(); - assert.strictEqual(text, context.options.commit, `Expected commit ${context.options.commit} but got ${text}`); + ); } async function testServerWeb(entryPoint: string) { @@ -115,75 +91,47 @@ export function setup(context: TestContext) { return; } - const wslPath = context.toWslPath(entryPoint); const wslWorkspaceDir = context.createWslTempDir(); const wslExtensionsDir = context.createWslTempDir(); const token = context.getRandomToken(); const test = new WslUITest(context, undefined, wslWorkspaceDir, wslExtensionsDir); - const args = [ - '--accept-server-license-terms', - '--connection-token', token, - '--host', '0.0.0.0', - '--port', context.getUniquePort(), - '--server-data-dir', context.createWslTempDir(), - '--extensions-dir', wslExtensionsDir, - '--user-data-dir', context.createWslTempDir(), - ]; + await context.runCliApp('WSL Server', 'wsl', + [ + context.toWslPath(entryPoint), + '--accept-server-license-terms', + '--connection-token', token, + '--host', '0.0.0.0', + '--port', context.getUniquePort(), + '--server-data-dir', context.createWslTempDir(), + '--extensions-dir', wslExtensionsDir, + '--user-data-dir', context.createWslTempDir(), + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return false; + } - context.log(`Starting web server in WSL: ${wslPath} with args ${args.join(' ')}`); - const server = spawn('wsl', [wslPath, ...args], { shell: true }); - - let testError: Error | undefined; - - server.stderr.on('data', (data) => { - const text = data.toString().trim(); - if (!/ECONNRESET/.test(text)) { - context.error(`[WSL Server Error] ${text}`); - } - }); - - server.stdout.on('data', (data) => { - const text = data.toString().trim(); - text.split('\n').forEach((line: string) => { - context.log(`[WSL Server Output] ${line}`); - }); - - const port = /Extension host agent listening on (\d+)/.exec(text)?.[1]; - if (port) { const url = context.getWebServerUrl(port, token, wslWorkspaceDir).toString(); - runUITest(url, test) - .catch((error) => { testError = error; }) - .finally(() => context.killProcessTree(server.pid!)); + const browser = await context.launchBrowser(); + const page = await context.getPage(browser.newPage()); + + context.log(`Navigating to ${url}`); + await page.goto(url, { waitUntil: 'networkidle' }); + + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); + + await test.run(page); + + context.log('Closing browser'); + await browser.close(); + + test.validate(); + return true; } - }); - - await new Promise((resolve, reject) => { - server.on('error', reject); - server.on('exit', resolve); - }); - - if (testError) { - throw testError; - } - } - - async function runUITest(url: string, test: WslUITest) { - const browser = await context.launchBrowser(); - const page = await context.getPage(browser.newPage()); - - context.log(`Navigating to ${url}`); - await page.goto(url, { waitUntil: 'networkidle' }); - - context.log('Waiting for the workbench to load'); - await page.waitForSelector('.monaco-workbench'); - - await test.run(page); - - context.log('Closing browser'); - await browser.close(); - - test.validate(); + ); } async function testDesktopApp(entryPoint: string, dataDir: string) {