Initial revision of automated release sanity checks. (#280857)

This commit is contained in:
Dmitriy Vasyura
2026-01-09 17:07:18 +01:00
committed by GitHub
parent 38721fc7ee
commit b1bf400da2
15 changed files with 1300 additions and 2 deletions
+7
View File
@@ -1935,6 +1935,13 @@ export default tseslint.config(
'*' // node modules
]
},
{
'target': 'test/sanity/**',
'restrictions': [
'test/sanity/**',
'*' // node modules
]
},
{
'target': 'test/automation/**',
'restrictions': [
+1
View File
@@ -42,6 +42,7 @@
"mixin-telemetry-docs": "node build/npm/mixin-telemetry-docs.ts",
"smoketest": "node build/lib/preLaunch.ts && cd test/smoke && npm run compile && node test/index.js",
"smoketest-no-compile": "cd test/smoke && node test/index.js",
"sanity-test": "cd test/sanity && npm run compile && node out/index.js",
"download-builtin-extensions": "node build/lib/builtInExtensions.ts",
"download-builtin-extensions-cg": "node build/lib/builtInExtensionsCG.ts",
"monaco-compile-check": "tsgo --project src/tsconfig.monaco.json --noEmit",
+3 -2
View File
@@ -1,9 +1,10 @@
# VSCode Tests
# VS Code Tests
## Contents
This folder contains the various test runners for VSCode. Please refer to the documentation within for how to run them:
This folder contains the various test runners for VS Code. Please refer to the documentation within for how to run them:
* `unit`: our suite of unit tests ([README](unit/README.md))
* `integration`: our suite of API tests ([README](integration/browser/README.md))
* `smoke`: our suite of automated UI tests ([README](smoke/README.md))
* `sanity`: release sanity tests ([README](sanity/README.md))
+5
View File
@@ -0,0 +1,5 @@
.DS_Store
npm-debug.log
Thumbs.db
node_modules/
out/
+46
View File
@@ -0,0 +1,46 @@
# VS Code Release Sanity Check Tests
## Overview
Automated end-to-end release sanity tests for published VS Code builds.
These tests verify critical functionality across different platforms and installation methods,
ensuring that published builds meet quality standards before reaching end users.
See [Sanity Check wiki page](https://github.com/microsoft/vscode/wiki/Sanity-Check) for more details on sanity testing.
## Usage
Many tests will use the underlying platform to install and verify basic VS Code functionality.
Such tests will need to be run on the corresponding target OS/virtual machine and will fail if ran outside.
Use -g or -f command-line options to filter tests to match the host platform.
### Command-Line Options
| Option | Alias | Description |
|--------|-------|-------------|
| `--commit <commit>` | `-c` | The commit SHA to test (required) |
| `--quality <quality>` | `-q` | The quality to test (required, "stable", "insider" or "exploration") |
| `--no-cleanup` | | Do not cleanup downloaded files after each test |
| `--grep <pattern>` | `-g` | Only run tests matching the given regex pattern |
| `--fgrep <string>` | `-f` | Only run tests containing the given string |
| `--help` | `-h` | Show help message |
### Example
To run CLI tests for all platforms on given commit of Insiders build, from the root directory run:
```bash
npm run sanity-test -- --commit 19228f26df517fecbfda96c20956f7c521e072be --quality insider -g "cli*"
```
## References
The following public documentation pages provide details on end-user VS Code setup scenarios.
- [Setup Overview](https://code.visualstudio.com/docs/setup/setup-overview)
- [Linux Setup](https://code.visualstudio.com/docs/setup/linux)
- [macOS Setup](https://code.visualstudio.com/docs/setup/mac)
- [Windows Setup](https://code.visualstudio.com/docs/setup/windows)
- [Portable Mode](https://code.visualstudio.com/docs/editor/portable)
- [VS Code Server](https://code.visualstudio.com/docs/remote/vscode-server)
- [Developing in WSL](https://code.visualstudio.com/docs/remote/wsl)
+172
View File
@@ -0,0 +1,172 @@
{
"name": "code-oss-dev-sanity-test",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "code-oss-dev-sanity-test",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"node-fetch": "^3.3.2",
"playwright": "^1.57.0"
},
"devDependencies": {
"@types/node": "22.x"
}
},
"node_modules/@types/node": {
"version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"deprecated": "Use your platform's native DOMException instead",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.57.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "code-oss-dev-sanity-test",
"version": "0.1.0",
"license": "MIT",
"main": "./out/index.js",
"scripts": {
"compile": "node ../../node_modules/typescript/bin/tsc",
"start": "node ./out/index.js"
},
"dependencies": {
"node-fetch": "^3.3.2",
"playwright": "^1.57.0"
},
"devDependencies": {
"@types/node": "22.x"
}
}
+138
View File
@@ -0,0 +1,138 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { spawn } from 'child_process';
import { TestContext } from './context';
export function setup(context: TestContext) {
describe('CLI', () => {
if (context.platform === 'linux-arm64') {
it('cli-alpine-arm64', async () => {
const dir = await context.downloadAndUnpack('cli-alpine-arm64');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('cli-alpine-x64', async () => {
const dir = await context.downloadAndUnpack('cli-alpine-x64');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'darwin-arm64') {
it('cli-darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('cli-darwin-arm64');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'darwin-x64') {
it('cli-darwin-x64', async () => {
const dir = await context.downloadAndUnpack('cli-darwin-x64');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'linux-arm64') {
it('cli-linux-arm64', async () => {
const dir = await context.downloadAndUnpack('cli-linux-arm64');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'linux-arm') {
it('cli-linux-armhf', async () => {
const dir = await context.downloadAndUnpack('cli-linux-armhf');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('cli-linux-x64', async () => {
const dir = await context.downloadAndUnpack('cli-linux-x64');
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'win32-arm64') {
it('cli-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('cli-win32-arm64');
context.validateAllSignatures(dir);
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
if (context.platform === 'win32-x64') {
it('cli-win32-x64', async () => {
const dir = await context.downloadAndUnpack('cli-win32-x64');
context.validateAllSignatures(dir);
const entryPoint = context.getEntryPoint('cli', dir);
await testCliApp(entryPoint);
});
}
async function testCliApp(entryPoint: string) {
const result = context.runNoErrors(entryPoint, '--version');
const version = result.stdout.trim();
assert.ok(version.includes(`(commit ${context.commit})`));
const workspaceDir = context.createTempDir();
process.chdir(workspaceDir);
context.log(`Changed current directory to: ${workspaceDir}`);
const cliDataDir = context.createTempDir();
const userDataDir = context.createTempDir();
const serverDataDir = context.createTempDir();
const extensionsDir = context.createTempDir();
const args = [
'--cli-data-dir', cliDataDir,
'--user-data-dir', userDataDir,
'tunnel',
'--accept-server-license-terms',
'--server-data-dir', serverDataDir,
'--extensions-dir', extensionsDir,
];
context.log(`Running CLI ${entryPoint} with args: ${args.join(' ')}`);
const cli = spawn(entryPoint, args);
cli.stderr.on('data', (data) => {
context.error(`[CLI Error] ${data.toString().trim()}`);
});
let tunnelUrl: string | undefined = undefined;
cli.stdout.on('data', (data) => {
const text = data.toString();
text.trim().split('\n').forEach((line: string) => {
context.log(`[CLI Output] ${line}`);
});
const match = /Open this link in your browser (https:\/\/.+)/.exec(text);
if (match !== null) {
tunnelUrl = context.getTunnelUrl(match[1]);
context.log(`Tunnel URL: ${tunnelUrl}`);
cli.kill();
}
});
await new Promise<void>((resolve, reject) => {
cli.on('error', reject);
cli.on('exit', () => resolve());
});
assert.ok(tunnelUrl, 'Expected to receive a tunnel URL from the CLI');
}
});
}
+439
View File
@@ -0,0 +1,439 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { spawnSync, SpawnSyncReturns } from 'child_process';
import { createHash } from 'crypto';
import fs from 'fs';
import fetch, { Response } from 'node-fetch';
import os from 'os';
import path from 'path';
/**
* Response from https://update.code.visualstudio.com/api/versions/commit:<commit>/<target>/<quality>
*/
interface ITargetMetadata {
url: string;
name: string;
version: string;
productVersion: string;
hash: string;
timestamp: number;
sha256hash: string;
supportsFastUpdate: boolean;
}
/**
* Provides context and utilities for VS Code sanity tests.
*/
export class TestContext {
private static readonly authenticodeInclude = /^.+\.(exe|dll|sys|cab|cat|msi|jar|ocx|ps1|psm1|psd1|ps1xml|pssc1)$/i;
private readonly tempDirs = new Set<string>();
private readonly logFile: string;
public constructor(
public readonly quality: 'stable' | 'insider' | 'exploration',
public readonly commit: string,
public readonly verbose: boolean,
) {
const osTempDir = fs.realpathSync(os.tmpdir());
const logDir = fs.mkdtempSync(path.join(osTempDir, 'vscode-sanity-log'));
this.logFile = path.join(logDir, 'sanity.log');
console.log(`Log file: ${this.logFile}`);
}
/**
* Returns the current platform in the format <platform>-<arch>.
*/
public get platform(): string {
return `${os.platform()}-${os.arch()}`;
}
/**
* Logs a message with a timestamp.
*/
public log(message: string) {
const line = `[${new Date().toISOString()}] ${message}\n`;
fs.appendFileSync(this.logFile, line);
if (this.verbose) {
console.log(line.trimEnd());
}
}
/**
* Logs an error message and throws an Error.
*/
public error(message: string): never {
const line = `[${new Date().toISOString()}] ERROR: ${message}\n`;
fs.appendFileSync(this.logFile, line);
console.error(line.trimEnd());
throw new Error(message);
}
/**
* Creates a new temporary directory and returns its path.
*/
public createTempDir(): string {
const osTempDir = fs.realpathSync(os.tmpdir());
const tempDir = fs.mkdtempSync(path.join(osTempDir, 'vscode-sanity'));
this.log(`Created temp directory: ${tempDir}`);
this.tempDirs.add(tempDir);
return tempDir;
}
/**
* Ensures that the directory for the specified file path exists.
*/
public ensureDirExists(filePath: string) {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
/**
* Cleans up all temporary directories created during the test run.
*/
public cleanup() {
for (const dir of this.tempDirs) {
this.log(`Deleting temp directory: ${dir}`);
try {
fs.rmSync(dir, { recursive: true, force: true });
this.log(`Deleted temp directory: ${dir}`);
} catch (error) {
this.log(`Failed to delete temp directory: ${dir}: ${error}`);
}
}
this.tempDirs.clear();
}
/**
* Fetches a URL and ensures there are no errors.
* @param url The URL to fetch.
* @returns The fetch Response object.
*/
public async fetchNoErrors(url: string): Promise<Response & { body: NodeJS.ReadableStream }> {
const response = await fetch(url);
if (!response.ok) {
this.error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}
if (response.body === null) {
this.error(`Response body is null for ${url}`);
}
return response as Response & { body: NodeJS.ReadableStream };
}
/**
* Fetches metadata for a specific VS Code release target.
* @param target The target platform (e.g., 'cli-linux-x64').
* @returns The target metadata.
*/
public async fetchMetadata(target: string): Promise<ITargetMetadata> {
const url = `https://update.code.visualstudio.com/api/versions/commit:${this.commit}/${target}/${this.quality}`;
this.log(`Fetching metadata for ${target} from ${url}`);
const response = await this.fetchNoErrors(url);
const result = await response.json() as ITargetMetadata;
if (result.url === undefined || result.sha256hash === undefined) {
this.error(`Invalid metadata response for ${target}: ${JSON.stringify(result)}`);
}
this.log(`Fetched metadata for ${target}: ${JSON.stringify(result)}`);
return result;
}
/**
* Downloads installer for specified VS Code release target.
* @param target The target platform (e.g., 'cli-linux-x64').
* @returns The path to the downloaded file.
*/
public async downloadTarget(target: string): Promise<string> {
const { url, sha256hash } = await this.fetchMetadata(target);
const filePath = path.join(this.createTempDir(), path.basename(url));
this.log(`Downloading ${url} to ${filePath}`);
const { body } = await this.fetchNoErrors(url);
const stream = fs.createWriteStream(filePath);
await new Promise<void>((resolve, reject) => {
body.on('error', reject);
stream.on('error', reject);
stream.on('finish', resolve);
body.pipe(stream);
});
this.log(`Downloaded ${url} to ${filePath}`);
this.validateSha256Hash(filePath, sha256hash);
if (TestContext.authenticodeInclude.test(filePath) && os.platform() === 'win32') {
this.validateSignature(filePath);
}
return filePath;
}
/**
* Validates the SHA-256 hash of a file.
* @param filePath The path to the file to validate.
* @param expectedHash The expected SHA-256 hash in hexadecimal format.
*/
public validateSha256Hash(filePath: string, expectedHash: string) {
this.log(`Validating SHA256 hash for ${filePath}`);
const buffer = fs.readFileSync(filePath);
const hash = createHash('sha256').update(buffer).digest('hex');
if (hash !== expectedHash) {
this.error(`Hash mismatch for ${filePath}: expected ${expectedHash}, got ${hash}`);
}
}
/**
* Validates the Authenticode signature of a Windows executable.
* @param filePath The path to the file to validate.
*/
public validateSignature(filePath: string) {
this.log(`Validating Authenticode signature for ${filePath}`);
const result = this.run('powershell', '-Command', `Get-AuthenticodeSignature "${filePath}" | Select-Object -ExpandProperty Status`);
if (result.error !== undefined) {
this.error(`Failed to run Get-AuthenticodeSignature: ${result.error.message}`);
}
const status = result.stdout.trim();
if (status !== 'Valid') {
this.error(`Authenticode signature is not valid for ${filePath}: ${status}`);
}
}
/**
* Validates signatures for all executable files in the specified directory.
* @param dir The directory to scan for executable files.
*/
public validateAllSignatures(dir: string) {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const file of files) {
const filePath = path.join(dir, file.name);
if (file.isDirectory()) {
this.validateAllSignatures(filePath);
} else if (TestContext.authenticodeInclude.test(file.name)) {
this.validateSignature(filePath);
}
}
}
/**
* Downloads and unpacks the specified VS Code release target.
* @param target The target platform (e.g., 'cli-linux-x64').
* @returns The path to the unpacked directory.
*/
public async downloadAndUnpack(target: string): Promise<string> {
const filePath = await this.downloadTarget(target);
return this.unpackArchive(filePath);
}
/**
* Unpacks a .zip or .tar.gz archive to a temporary directory.
* @param archivePath The path to the archive file.
* @returns The path to the temporary directory where the archive was unpacked.
*/
public unpackArchive(archivePath: string): string {
const dir = this.createTempDir();
this.log(`Unpacking ${archivePath} to ${dir}`);
this.runNoErrors('tar', '-xzf', archivePath, '-C', dir);
this.log(`Unpacked ${archivePath} to ${dir}`);
return dir;
}
/**
* Runs a command synchronously.
* @param command The command to run.
* @param args Optional arguments for the command.
* @returns The result of the spawnSync call.
*/
public run(command: string, ...args: string[]): SpawnSyncReturns<string> {
this.log(`Running command: ${command} ${args.join(' ')}`);
return spawnSync(command, args, { encoding: 'utf-8' }) as SpawnSyncReturns<string>;
}
/**
* Runs a command synchronously and ensures it succeeds.
* @param command The command to run.
* @param args Optional arguments for the command.
* @returns The result of the spawnSync call.
*/
public runNoErrors(command: string, ...args: string[]): SpawnSyncReturns<string> {
const result = this.run(command, ...args);
if (result.error !== undefined) {
this.error(`Failed to run command: ${result.error.message}`);
}
if (result.status !== 0) {
this.error(`Command exited with code ${result.status}: ${result.stderr}`);
}
return result;
}
/**
* Installs a Microsoft Installer package silently.
* @param installerPath The path to the installer executable.
* @returns The path to the installed VS Code executable.
*/
public installWindowsApp(type: 'user' | 'system', installerPath: string): string {
this.log(`Installing ${installerPath} in silent mode`);
this.runNoErrors(installerPath, '/silent', '/mergetasks=!runcode');
this.log(`Installed ${installerPath} successfully`);
const varName = type === 'system' ? 'PROGRAMFILES' : 'LOCALAPPDATA';
const parentDir = process.env[varName];
if (parentDir === undefined) {
this.error(`Environment variable ${varName} is not defined`);
}
let entryPoint: string;
switch (this.quality) {
case 'stable':
entryPoint = path.join(parentDir, 'Microsoft VS Code', 'Code.exe');
break;
case 'insider':
entryPoint = path.join(parentDir, 'Microsoft VS Code Insiders', 'Code - Insiders.exe');
break;
case 'exploration':
entryPoint = path.join(parentDir, 'Microsoft VS Code Exploration', 'Code - Exploration.exe');
break;
}
if (!fs.existsSync(entryPoint)) {
this.error(`Desktop entry point does not exist: ${entryPoint}`);
}
this.log(`Installed VS Code executable at: ${entryPoint}`);
return entryPoint;
}
/**
* Prepares a macOS .app bundle for execution by removing the quarantine attribute.
* @param bundleDir The directory containing the .app bundle.
* @returns The path to the VS Code executable.
*/
public installMacApp(bundleDir: string): string {
let appName: string;
switch (this.quality) {
case 'stable':
appName = 'Visual Studio Code.app';
break;
case 'insider':
appName = 'Visual Studio Code - Insiders.app';
break;
case 'exploration':
appName = 'Visual Studio Code - Exploration.app';
break;
}
const appPath = path.join(bundleDir, appName);
this.log(`Removing quarantine attribute from ${appPath}`);
this.runNoErrors('xattr', '-rd', 'com.apple.quarantine', appPath);
this.log(`Removed quarantine attribute successfully`);
const entryPoint = path.join(appPath, 'Contents/Resources/app/bin/code');
if (!fs.existsSync(entryPoint)) {
this.error(`Desktop entry point does not exist: ${entryPoint}`);
}
this.log(`VS Code executable at: ${entryPoint}`);
return entryPoint;
}
/**
* Installs a Linux RPM package.
* @param packagePath The path to the RPM file.
* @returns The path to the installed VS Code executable.
*/
public installRpm(packagePath: string): string {
this.log(`Installing ${packagePath} using RPM package manager`);
this.runNoErrors('sudo', 'rpm', '-i', packagePath);
this.log(`Installed ${packagePath} successfully`);
const entryPoint = this.getEntryPoint('desktop', '/usr/bin');
this.log(`Installed VS Code executable at: ${entryPoint}`);
return entryPoint;
}
/**
* Installs a Linux DEB package.
* @param packagePath The path to the DEB file.
* @returns The path to the installed VS Code executable.
*/
public installDeb(packagePath: string): string {
this.log(`Installing ${packagePath} using DEB package manager`);
this.runNoErrors('sudo', 'dpkg', '-i', packagePath);
this.log(`Installed ${packagePath} successfully`);
const entryPoint = this.getEntryPoint('desktop', '/usr/bin');
this.log(`Installed VS Code executable at: ${entryPoint}`);
return entryPoint;
}
/**
* Installs a Linux Snap package.
* @param packagePath The path to the Snap file.
* @returns The path to the installed VS Code executable.
*/
public installSnap(packagePath: string): string {
this.log(`Installing ${packagePath} using Snap package manager`);
this.runNoErrors('sudo', 'snap', 'install', packagePath, '--classic', '--dangerous');
this.log(`Installed ${packagePath} successfully`);
const entryPoint = this.getEntryPoint('desktop', '/snap/bin');
this.log(`Installed VS Code executable at: ${entryPoint}`);
return entryPoint;
}
/**
* Returns the entry point executable for the VS Code CLI or Desktop installation in the specified directory.
* @param dir The directory of the VS Code installation.
* @returns The path to the entry point executable.
*/
public getEntryPoint(type: 'cli' | 'desktop', dir: string): string {
let suffix: string;
switch (this.quality) {
case 'stable':
suffix = type === 'cli' ? '' : '';
break;
case 'insider':
suffix = type === 'cli' ? '-insiders' : ' - Insiders';
break;
case 'exploration':
suffix = type === 'cli' ? '-exploration' : ' - Exploration';
break;
}
const extension = os.platform() === 'win32' ? '.exe' : '';
const filePath = path.join(dir, `code${suffix}${extension}`);
if (!fs.existsSync(filePath)) {
this.error(`CLI entry point does not exist: ${filePath}`);
}
return filePath;
}
/**
* 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.commit);
return url.toString();
}
}
+236
View File
@@ -0,0 +1,236 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { _electron } from 'playwright';
import { TestContext } from './context';
export function setup(context: TestContext) {
describe('Desktop', () => {
if (context.platform === 'darwin-x64') {
it('darwin', async () => {
const dir = await context.downloadAndUnpack('darwin');
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'darwin-arm64') {
it('darwin-arm64', async () => {
const dir = await context.downloadAndUnpack('darwin-arm64');
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform.startsWith('darwin-')) {
it('darwin-universal', async () => {
const dir = await context.downloadAndUnpack('darwin-universal');
const entryPoint = context.installMacApp(dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-arm64') {
it('linux-arm64', async () => {
const dir = await context.downloadAndUnpack('linux-arm64');
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-arm') {
it('linux-armhf', async () => {
const dir = await context.downloadAndUnpack('linux-armhf');
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-arm64') {
it('linux-deb-arm64', async () => {
const packagePath = await context.downloadTarget('linux-deb-arm64');
const entryPoint = context.installDeb(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-arm') {
it('linux-deb-armhf', async () => {
const packagePath = await context.downloadTarget('linux-deb-armhf');
const entryPoint = context.installDeb(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('linux-deb-x64', async () => {
const packagePath = await context.downloadTarget('linux-deb-x64');
const entryPoint = context.installDeb(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-arm64') {
it('linux-rpm-arm64', async () => {
const packagePath = await context.downloadTarget('linux-rpm-arm64');
const entryPoint = context.installRpm(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-arm') {
it('linux-rpm-armhf', async () => {
const packagePath = await context.downloadTarget('linux-rpm-armhf');
const entryPoint = context.installRpm(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('linux-rpm-x64', async () => {
const packagePath = await context.downloadTarget('linux-rpm-x64');
const entryPoint = context.installRpm(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('linux-snap-x64', async () => {
const packagePath = await context.downloadTarget('linux-snap-x64');
const entryPoint = context.installSnap(packagePath);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'linux-x64') {
it('linux-x64', async () => {
const dir = await context.downloadAndUnpack('linux-x64');
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'win32-arm64') {
it('win32-arm64', async () => {
const packagePath = await context.downloadTarget('win32-arm64');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('system', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'win32-arm64') {
it('win32-arm64-archive', async () => {
const dir = await context.downloadAndUnpack('win32-arm64-archive');
context.validateAllSignatures(dir);
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'win32-arm64') {
it('win32-arm64-user', async () => {
const packagePath = await context.downloadTarget('win32-arm64-user');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('user', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'win32-x64') {
it('win32-x64', async () => {
const packagePath = await context.downloadTarget('win32-x64');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('system', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'win32-x64') {
it('win32-x64-archive', async () => {
const dir = await context.downloadAndUnpack('win32-x64-archive');
context.validateAllSignatures(dir);
const entryPoint = context.getEntryPoint('desktop', dir);
await testDesktopApp(entryPoint);
});
}
if (context.platform === 'win32-x64') {
it('win32-x64-user', async () => {
const packagePath = await context.downloadTarget('win32-x64-user');
context.validateSignature(packagePath);
const entryPoint = context.installWindowsApp('user', packagePath);
context.validateAllSignatures(path.dirname(entryPoint));
await testDesktopApp(entryPoint);
});
}
async function testDesktopApp(executablePath: string) {
const extensionsDir = context.createTempDir();
const dataDir = context.createTempDir();
const workspaceDir = context.createTempDir();
const args = ['--extensions-dir', extensionsDir, '--user-data-dir', dataDir, workspaceDir];
context.log(`Start VS Code: ${executablePath} with args: ${args.join(' ')}`);
const app = await _electron.launch({ executablePath, args });
const window = await app.firstWindow();
context.log('Dismiss workspace trust dialog');
await window.getByText('Yes, I trust the authors').click();
context.log('Focus Explorer view');
await window.keyboard.press('Control+Shift+E');
context.log('Click New File button');
await window.getByLabel('New File...').click();
context.log('Type file name');
await window.locator('input').fill('helloWorld.txt');
await window.keyboard.press('Enter');
context.log('Focus the code editor');
await window.getByText(/Start typing/).focus();
context.log('Type some content into the file');
await window.keyboard.type('Hello, World!');
context.log('Save the file');
await window.keyboard.press('Control+S');
context.log('Open Extensions view');
await window.keyboard.press('Control+Shift+X');
await window.waitForSelector('.extension-list-item');
context.log('Type extension name to search for');
await window.keyboard.type('GitHub Pull Requests');
await new Promise(resolve => setTimeout(resolve, 2000));
context.log('Click Install on the first extension in the list');
await window.locator('.extension-action:not(.disabled)', { hasText: /Install/ }).first().click();
context.log('Wait for extension to be installed');
await window.locator('.extension-action:not(.disabled)', { hasText: /Uninstall/ }).waitFor();
context.log('Close the application');
await app.close();
context.log('Verify file contents');
const filePath = `${workspaceDir}/helloWorld.txt`;
const fileContents = fs.readFileSync(filePath, 'utf-8');
assert.strictEqual(fileContents, 'Hello, World!');
context.log('Verify extension is installed');
const extensions = fs.readdirSync(extensionsDir);
const hasExtension = extensions.some(ext => ext.startsWith('github.vscode-pull-request-github'));
assert.strictEqual(hasExtension, true);
}
});
}
+40
View File
@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import minimist from 'minimist';
import Mocha, { MochaOptions } from 'mocha';
const options = minimist(process.argv.slice(2), {
string: ['fgrep', 'grep'],
boolean: ['help'],
alias: { fgrep: 'f', grep: 'g', help: 'h' },
});
if (options.help) {
console.info('Usage: npm run sanity-test -- [options]');
console.info('Options:');
console.info(' --commit, -c <commit> The commit to test (required)');
console.info(` --quality, -q <quality> The quality to test (required, "stable", "insider" or "exploration")`);
console.info(' --no-cleanup Do not cleanup downloaded files after each test');
console.info(' --grep, -g <pattern> Only run tests matching the given <pattern>');
console.info(' --fgrep, -f <string> Only run tests containing the given <string>');
console.info(' --verbose, -v Enable verbose logging');
console.info(' --help, -h Show this help message');
process.exit(0);
}
const mochaOptions: MochaOptions = {
color: true,
timeout: 5 * 60 * 1000,
slow: 3 * 60 * 1000,
grep: options.grep,
fgrep: options.fgrep,
};
const mocha = new Mocha(mochaOptions);
mocha.addFile(require.resolve('./main.js'));
mocha.run(failures => {
process.exitCode = failures > 0 ? 1 : 0;
});
+41
View File
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import minimist from 'minimist';
import { setup as setupCliTests } from './cli.test';
import { TestContext } from './context';
import { setup as setupDesktopTests } from './desktop.test';
import { setup as setupServerTests } from './server.test';
import { setup as setupServerWebTests } from './serverWeb.test';
const options = minimist(process.argv.slice(2), {
string: ['commit', 'quality'],
boolean: ['cleanup', 'verbose'],
alias: { commit: 'c', quality: 'q', verbose: 'v' },
default: { cleanup: true, verbose: false },
});
if (!options.commit) {
throw new Error('--commit is required');
}
if (!options.quality) {
throw new Error('--quality is required');
}
const context = new TestContext(options.quality, options.commit, options.verbose);
describe('VS Code Sanity Tests', () => {
if (options.cleanup) {
afterEach(() => {
context.cleanup();
});
}
setupCliTests(context);
setupDesktopTests(context);
setupServerTests(context);
setupServerWebTests(context);
});
+66
View File
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TestContext } from './context';
export function setup(context: TestContext) {
describe('Server', () => {
if (context.platform === 'linux-arm64') {
it('server-alpine-arm64', async () => {
await context.downloadAndUnpack('server-alpine-arm64');
});
}
if (context.platform === 'linux-x64') {
it('server-alpine-x64', async () => {
await context.downloadAndUnpack('server-linux-alpine');
});
}
if (context.platform === 'darwin-arm64') {
it('server-darwin-arm64', async () => {
await context.downloadAndUnpack('server-darwin-arm64');
});
}
if (context.platform === 'darwin-x64') {
it('server-darwin-x64', async () => {
await context.downloadAndUnpack('server-darwin');
});
}
if (context.platform === 'linux-arm64') {
it('server-linux-arm64', async () => {
await context.downloadAndUnpack('server-linux-arm64');
});
}
if (context.platform === 'linux-arm') {
it('server-linux-armhf', async () => {
await context.downloadAndUnpack('server-linux-armhf');
});
}
if (context.platform === 'linux-x64') {
it('server-linux-x64', async () => {
await context.downloadAndUnpack('server-linux-x64');
});
}
if (context.platform === 'win32-arm64') {
it('server-win32-arm64', async () => {
const dir = await context.downloadAndUnpack('server-win32-arm64');
context.validateAllSignatures(dir);
});
}
if (context.platform === 'win32-x64') {
it('server-win32-x64', async () => {
const dir = await context.downloadAndUnpack('server-win32-x64');
context.validateAllSignatures(dir);
});
}
});
}
+66
View File
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TestContext } from './context';
export function setup(context: TestContext) {
describe('Server Web', () => {
if (context.platform === 'linux-arm64') {
it('server-alpine-arm64-web', async () => {
await context.downloadAndUnpack('server-alpine-arm64-web');
});
}
if (context.platform === 'linux-x64') {
it('server-alpine-x64-web', async () => {
await context.downloadAndUnpack('server-linux-alpine-web');
});
}
if (context.platform === 'darwin-arm64') {
it('server-darwin-arm64-web', async () => {
await context.downloadAndUnpack('server-darwin-arm64-web');
});
}
if (context.platform === 'darwin-x64') {
it('server-darwin-x64-web', async () => {
await context.downloadAndUnpack('server-darwin-web');
});
}
if (context.platform === 'linux-arm64') {
it('server-linux-arm64-web', async () => {
await context.downloadAndUnpack('server-linux-arm64-web');
});
}
if (context.platform === 'linux-arm') {
it('server-linux-armhf-web', async () => {
await context.downloadAndUnpack('server-linux-armhf-web');
});
}
if (context.platform === 'linux-x64') {
it('server-linux-x64-web', async () => {
await context.downloadAndUnpack('server-linux-x64-web');
});
}
if (context.platform === 'win32-arm64') {
it('server-win32-arm64-web', async () => {
const dir = await context.downloadAndUnpack('server-win32-arm64-web');
context.validateAllSignatures(dir);
});
}
if (context.platform === 'win32-x64') {
it('server-win32-x64-web', async () => {
const dir = await context.downloadAndUnpack('server-win32-x64-web');
context.validateAllSignatures(dir);
});
}
});
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": false,
"preserveConstEnums": true,
"target": "es2024",
"strict": true,
"noUnusedParameters": false,
"noUnusedLocals": true,
"outDir": "out",
"sourceMap": true,
"skipLibCheck": true,
"esModuleInterop": true,
"lib": [
"esnext", // for #201187
"dom"
]
},
"exclude": [
"node_modules"
]
}