deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
486
packages/process/src/common/shell-command-builder.slow-spec.ts
Normal file
486
packages/process/src/common/shell-command-builder.slow-spec.ts
Normal file
@@ -0,0 +1,486 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* This test suite assumes that we run in a NodeJS environment!
|
||||
*/
|
||||
|
||||
import { spawn, execSync, SpawnOptions, ChildProcess, spawnSync } from 'child_process';
|
||||
import { Readable } from 'stream';
|
||||
import { join } from 'path';
|
||||
import { ShellCommandBuilder, CommandLineOptions, ProcessInfo } from './shell-command-builder';
|
||||
import * as chalk from 'chalk'; // tslint:disable-line:no-implicit-dependencies
|
||||
|
||||
export interface TestProcessInfo extends ProcessInfo {
|
||||
shell: ChildProcess
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
/**
|
||||
* Extra debugging info (very verbose).
|
||||
*/
|
||||
const _debug: boolean = Boolean(process.env['THEIA_PROCESS_TEST_DEBUG']);
|
||||
/**
|
||||
* On Windows, some shells simply mess up the terminal's output.
|
||||
* Enable if you still want to test those.
|
||||
*/
|
||||
const _runWeirdShell: true | undefined = Boolean(process.env['THEIA_PROCESS_TEST_WEIRD_SHELL']) || undefined;
|
||||
/**
|
||||
* You might only have issues with a specific shell (`cmd.exe` I am looking at you).
|
||||
*/
|
||||
const _onlyTestShell: string | undefined = process.env['THEIA_PROCESS_TEST_ONLY'] || undefined;
|
||||
/**
|
||||
* Only log if environment variable is set.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function debug(...parts: any[]): void {
|
||||
if (_debug) {
|
||||
console.debug(...parts);
|
||||
}
|
||||
}
|
||||
|
||||
const testResources = join(__dirname, '../../src/common/tests');
|
||||
const spawnOptions: SpawnOptions = {
|
||||
// We do our own quoting, don't rely on the one done by NodeJS:
|
||||
windowsVerbatimArguments: true,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
};
|
||||
|
||||
// Formatting options, used with `scanLines` for debugging.
|
||||
const stdoutFormat = (prefix: string) => (data: string) =>
|
||||
`${chalk.bold(chalk.yellow(`${prefix} STDOUT:`))} ${chalk.bgYellow(chalk.black(data))}`;
|
||||
const stderrFormat = (prefix: string) => (data: string) =>
|
||||
`${chalk.bold(chalk.red(`${prefix} STDERR:`))} ${chalk.bgRed(chalk.white(data))}`;
|
||||
|
||||
// Default error scanner
|
||||
const errorScanner = (handle: ScanLineHandle<void>) => {
|
||||
if (
|
||||
/^\s*\w+Error:/.test(handle.line) ||
|
||||
/^\s*Cannot find /.test(handle.line)
|
||||
) {
|
||||
throw new Error(handle.text);
|
||||
}
|
||||
};
|
||||
|
||||
// Yarn mangles the PATH and creates some proxy script around node(.exe),
|
||||
// which messes up our environment, failing the tests.
|
||||
const hostNodePath =
|
||||
process.env['npm_node_execpath'] ||
|
||||
process.env['NODE'];
|
||||
if (!hostNodePath) {
|
||||
throw new Error('Could not determine the real node path.');
|
||||
}
|
||||
|
||||
const shellCommandBuilder = new ShellCommandBuilder();
|
||||
const shellConfigs = [{
|
||||
name: 'bash',
|
||||
path: isWindows
|
||||
? _runWeirdShell && execShellCommand('where bash.exe')
|
||||
: execShellCommand('command -v bash'),
|
||||
nodePath:
|
||||
isWindows && 'node' // Good enough
|
||||
}, {
|
||||
name: 'wsl',
|
||||
path: isWindows
|
||||
? _runWeirdShell && execShellCommand('where wsl.exe')
|
||||
: undefined,
|
||||
nodePath:
|
||||
isWindows && 'node' // Good enough
|
||||
}, {
|
||||
name: 'cmd',
|
||||
path: isWindows
|
||||
? execShellCommand('where cmd.exe')
|
||||
: undefined,
|
||||
}, {
|
||||
name: 'powershell',
|
||||
path: execShellCommand(isWindows
|
||||
? 'where powershell'
|
||||
: 'command -v pwsh'),
|
||||
}];
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
// 18d/12m/19y - Ubuntu 16.04:
|
||||
// Powershell sometimes fails when running as part of an npm lifecycle script.
|
||||
// See following error:
|
||||
//
|
||||
//
|
||||
// FailFast:
|
||||
// The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception.
|
||||
//
|
||||
// at System.Environment.FailFast(System.String, System.Exception)
|
||||
// at System.Environment.FailFast(System.String, System.Exception)
|
||||
// at Microsoft.PowerShell.UnmanagedPSEntry.Start(System.String, System.String[], Int32)
|
||||
// at Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])
|
||||
//
|
||||
// Exception details:
|
||||
// System.TypeInitializationException: The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception. ---> System.ArgumentException: Item has already been added. Key in dictionary: 'SPAWN_WRAP_SHIM_ROOT' Key being added: 'SPAWN_WRAP_SHIM_ROOT'
|
||||
// at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
|
||||
// at System.Environment.ToHashtable(IEnumerable`1 pairs)
|
||||
// at System.Environment.GetEnvironmentVariables()
|
||||
// at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformImplementation..ctor()
|
||||
// at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformSingleton.get_Current()
|
||||
// at Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryConfigurationFactory.Initialize(TelemetryConfiguration configuration, TelemetryModules modules)
|
||||
// at Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.get_Active()
|
||||
// at Microsoft.PowerShell.ApplicationInsightsTelemetry..cctor()
|
||||
// --- End of inner exception stack trace ---
|
||||
// at Microsoft.PowerShell.ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry()
|
||||
// at Microsoft.PowerShell.ConsoleHost.Start(String bannerText, String helpText, String[] args)
|
||||
// at Microsoft.PowerShell.ConsoleShell.Start(String bannerText, String helpText, String[] args)
|
||||
// at Microsoft.PowerShell.UnmanagedPSEntry.Start(String consoleFilePath, String[] args, Int32 argc)
|
||||
|
||||
/* eslint-enable max-len */
|
||||
|
||||
let id = 0;
|
||||
for (const shellConfig of shellConfigs) {
|
||||
|
||||
let skipMessage: string | undefined;
|
||||
|
||||
if (typeof _onlyTestShell === 'string' && shellConfig.name !== _onlyTestShell) {
|
||||
skipMessage = `only testing ${_onlyTestShell}`;
|
||||
|
||||
} else if (!shellConfig.path) {
|
||||
// For each shell, skip if we could not find the executable path.
|
||||
skipMessage = 'cannot find shell';
|
||||
|
||||
} else {
|
||||
// Run a test in the shell to catch runtime issues.
|
||||
// CI seems to have issues with some shells depending on the environment...
|
||||
try {
|
||||
const debugName = `${shellConfig.name}/test`;
|
||||
const shellTest = spawnSync(shellConfig.path, {
|
||||
input: 'echo abcdefghijkl\n\n',
|
||||
timeout: 5_000,
|
||||
});
|
||||
debug(stdoutFormat(debugName)(shellTest.stdout.toString()));
|
||||
debug(stderrFormat(debugName)(shellTest.stderr.toString()));
|
||||
if (!/abcdefghijkl/m.test(shellTest.output.toString())) {
|
||||
skipMessage = 'wrong test output';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
skipMessage = 'error occurred';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If skipMessage is set, we should skip the test and explain why.
|
||||
*/
|
||||
const describeOrSkip = (callback: (this: Mocha.Suite) => void) => {
|
||||
const describeMessage = `test ${shellConfig.name} commands`;
|
||||
if (typeof skipMessage === 'undefined') {
|
||||
describe(describeMessage, callback);
|
||||
} else {
|
||||
describe.skip(`${describeMessage} - skip: ${skipMessage}`, callback);
|
||||
}
|
||||
};
|
||||
|
||||
describeOrSkip(function (): void {
|
||||
this.timeout(10_000);
|
||||
|
||||
let nodePath: string;
|
||||
let cwd: string;
|
||||
let submit: string | undefined;
|
||||
let processInfo: TestProcessInfo;
|
||||
let context: TestCaseContext;
|
||||
|
||||
beforeEach(() => {
|
||||
// In WSL, the node path is different than the host one (Windows vs Linux).
|
||||
nodePath = shellConfig.nodePath || hostNodePath;
|
||||
|
||||
// On windows, when running bash we need to convert paths from Windows
|
||||
// to their mounting point, assuming bash is running within WSL.
|
||||
if (isWindows && /bash|wsl/.test(shellConfig.name)) {
|
||||
cwd = convertWindowsPath(testResources);
|
||||
} else {
|
||||
cwd = testResources;
|
||||
}
|
||||
|
||||
// When running powershell, it seems like good measure to send `\n` twice...
|
||||
if (shellConfig.name === 'powershell') {
|
||||
submit = '\n\n';
|
||||
}
|
||||
|
||||
// TestContext holds all state for a given test.
|
||||
const testContextName = `${shellConfig.name}/${++id}`;
|
||||
context = new TestCaseContext(testContextName, submit);
|
||||
processInfo = createShell(context, shellConfig.path!);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processInfo.shell.kill();
|
||||
context.finalize();
|
||||
});
|
||||
|
||||
it('use simple environment variables', async () => {
|
||||
const envName = 'SIMPLE_NAME';
|
||||
const envValue = 'SIMPLE_VALUE';
|
||||
await testCommandLine(
|
||||
context, processInfo,
|
||||
{
|
||||
cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
|
||||
env: {
|
||||
[envName]: envValue,
|
||||
}
|
||||
}, [
|
||||
// stderr
|
||||
scanLines<void>(context, processInfo.shell.stderr!, errorScanner, stderrFormat(context.name)),
|
||||
// stdout
|
||||
scanLines<void>(context, processInfo.shell.stdout!, handle => {
|
||||
errorScanner(handle);
|
||||
if (handle.line.includes(`[${envValue}]`)) {
|
||||
handle.resolve();
|
||||
}
|
||||
}, stdoutFormat(context.name)),
|
||||
]);
|
||||
});
|
||||
|
||||
it('use problematic environment variables', async () => {
|
||||
const envName = 'A?B_C | D $PATH';
|
||||
const envValue = 'SUCCESS';
|
||||
await testCommandLine(
|
||||
context, processInfo,
|
||||
{
|
||||
cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
|
||||
env: {
|
||||
[envName]: envValue,
|
||||
}
|
||||
}, [
|
||||
// stderr
|
||||
scanLines<void>(context, processInfo.shell.stderr!, errorScanner, stderrFormat(context.name)),
|
||||
// stdout
|
||||
scanLines<void>(context, processInfo.shell.stdout!, handle => {
|
||||
errorScanner(handle);
|
||||
if (handle.line.includes(`[${envValue}]`)) {
|
||||
handle.resolve();
|
||||
}
|
||||
if (handle.line.includes('[undefined]')) {
|
||||
handle.reject(new Error(handle.text));
|
||||
}
|
||||
}, stdoutFormat(context.name)),
|
||||
]);
|
||||
});
|
||||
|
||||
it('command with complex arguments', async () => {
|
||||
const left = 'ABC';
|
||||
const right = 'DEF';
|
||||
await testCommandLine(
|
||||
context, processInfo,
|
||||
{
|
||||
cwd, args: [nodePath, '-e', `{
|
||||
const left = '${left}';
|
||||
const right = '${right}';
|
||||
console.log(\`[\${left}|\${right}]\`);
|
||||
}`],
|
||||
}, [
|
||||
// stderr
|
||||
scanLines<void>(context, processInfo.shell.stderr!, errorScanner, stderrFormat(context.name)),
|
||||
// stdout
|
||||
scanLines<void>(context, processInfo.shell.stdout!, handle => {
|
||||
errorScanner(handle);
|
||||
if (handle.line.includes(`[${left}|${right}]`)) {
|
||||
handle.resolve();
|
||||
}
|
||||
}, stdoutFormat(context.name)),
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow `command` to fail and return undefined instead.
|
||||
*/
|
||||
function execShellCommand(command: string): string | undefined {
|
||||
try {
|
||||
// If trimmed output is an empty string, return `undefined` instead:
|
||||
return execSync(command).toString().trim() || undefined;
|
||||
} catch (error) {
|
||||
console.error(command, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When executing `bash.exe` on Windows, the `C:`, `D:`, etc drives are mounted under `/mnt/<drive>/...`
|
||||
*/
|
||||
function convertWindowsPath(windowsPath: string): string {
|
||||
return windowsPath
|
||||
// Convert back-slashes to forward-slashes
|
||||
.replace(/\\/g, '/')
|
||||
// Convert drive-letter to usual mounting point in WSL
|
||||
.replace(/^[A-Za-z]:\//, s => `/mnt/${s[0].toLowerCase()}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display trailing whitespace in a string, such as \r and \n.
|
||||
*/
|
||||
function displayWhitespaces(line: string): string {
|
||||
return line
|
||||
.replace(/\r?\n/, s => s.length === 2 ? '<\\r\\n>\r\n' : '<\\n>\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually run `prepareCommandLine`.
|
||||
*/
|
||||
async function testCommandLine(
|
||||
context: TestCaseContext,
|
||||
processInfo: TestProcessInfo,
|
||||
options: CommandLineOptions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
firstOf: Array<Promise<any>>,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): Promise<any> {
|
||||
const commandLine = shellCommandBuilder.buildCommand(processInfo, options);
|
||||
debug(`${chalk.bold(chalk.white(`${context.name} STDIN:`))} ${chalk.bgWhite(chalk.black(displayWhitespaces(commandLine)))}`);
|
||||
processInfo.shell.stdin!.write(commandLine + context.submit);
|
||||
return Promise.race(firstOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `(Test)ProcessInfo` object by spawning the specified shell.
|
||||
*/
|
||||
function createShell(
|
||||
context: TestCaseContext,
|
||||
shellExecutable: string,
|
||||
shellArguments: string[] = []
|
||||
): TestProcessInfo {
|
||||
const shell = spawn(shellExecutable, shellArguments, spawnOptions);
|
||||
debug(chalk.magenta(`${chalk.bold(`${context.name} SPAWN:`)} ${shellExecutable} ${shellArguments.join(' ')}`));
|
||||
shell.on('close', (code, signal) => debug(chalk.magenta(
|
||||
`${chalk.bold(`${context.name} CLOSE:`)} ${shellExecutable} code(${code}) signal(${signal})`
|
||||
)));
|
||||
return {
|
||||
executable: shellExecutable,
|
||||
arguments: [],
|
||||
shell,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire `callback` once per new detected line.
|
||||
*/
|
||||
async function scanLines<T = void>(
|
||||
context: TestCaseContext,
|
||||
stream: Readable,
|
||||
callback: (handle: ScanLineHandle<T>) => void,
|
||||
debugFormat = (s: string) => s,
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let line = '';
|
||||
let text = '';
|
||||
stream.on('close', () => {
|
||||
debug(debugFormat('<CLOSED>'));
|
||||
});
|
||||
// The `data` listener will be collected on 'close', which will happen
|
||||
// once we kill the process.
|
||||
stream.on('data', data => {
|
||||
if (context.resolved) {
|
||||
return;
|
||||
}
|
||||
const split = data.toString().split('\n');
|
||||
while (!context.resolved && split.length > 1) {
|
||||
line += split.shift()! + '\n';
|
||||
text += line;
|
||||
debug(debugFormat(displayWhitespaces(line)));
|
||||
try {
|
||||
callback({
|
||||
resolve: (value: T) => {
|
||||
if (!context.resolved) {
|
||||
context.resolve();
|
||||
resolve(value);
|
||||
debug(chalk.bold(chalk.green(`${context.name} SCANLINES RESOLVED`)));
|
||||
}
|
||||
},
|
||||
reject: (reason?: Error) => {
|
||||
if (!context.resolved) {
|
||||
context.resolve();
|
||||
reject(reason);
|
||||
debug(chalk.bold(chalk.red(`${context.name} SCANLINES REJECTED`)));
|
||||
}
|
||||
},
|
||||
line,
|
||||
text,
|
||||
});
|
||||
} catch (error) {
|
||||
debug(chalk.bold(chalk.red(`${context.name} SCANLINES THROWED`)));
|
||||
context.resolve();
|
||||
reject(error);
|
||||
break;
|
||||
}
|
||||
line = '';
|
||||
}
|
||||
line += split[0];
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
interface ScanLineHandle<T> {
|
||||
|
||||
/**
|
||||
* Finish listening to new events with a return value.
|
||||
*/
|
||||
resolve: (value: T) => void
|
||||
/**
|
||||
* Finish listening to new events with an error.
|
||||
*/
|
||||
reject: (reason?: Error) => void
|
||||
/**
|
||||
* Currently parsed line.
|
||||
*/
|
||||
line: string
|
||||
/**
|
||||
* The whole output buffer, containing all lines.
|
||||
*/
|
||||
text: string
|
||||
|
||||
}
|
||||
/**
|
||||
* We need a test case context to help with catching listeners that timed-out,
|
||||
* and synchronize multiple listeners so that when one resolves the test case,
|
||||
* the others can be put in "sleep mode" until destruction.
|
||||
*/
|
||||
class TestCaseContext {
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* A name associated with this context, to help with debugging.
|
||||
*/
|
||||
readonly name: string,
|
||||
/**
|
||||
* The characters to send in order to submit a command (mostly
|
||||
* powershell is causing issues).
|
||||
*/
|
||||
public submit = '\n',
|
||||
/**
|
||||
* @internal Current state of the test case, if it is finished or not.
|
||||
*/
|
||||
public resolved = false
|
||||
) { }
|
||||
|
||||
resolve(): void {
|
||||
this.resolved = true;
|
||||
}
|
||||
|
||||
finalize(): void {
|
||||
if (!this.resolved) {
|
||||
this.resolve();
|
||||
debug(chalk.red(`${chalk.bold(`${this.name} CONTEXT:`)} context wasn't resolved when finalizing, resolving!`));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user