From b562ce7067006573d8a2a04a3030ac1b0cf01b2f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 15 Dec 2021 12:28:36 -0800 Subject: [PATCH] Use async/await in env var tests and improve polling Fixes #120077 --- .../src/singlefolder-tests/terminal.test.ts | 206 +++++++++--------- extensions/vscode-api-tests/src/utils.ts | 33 +++ 2 files changed, 136 insertions(+), 103 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index db71fa3a7f2..0fa697a6105 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { deepEqual, deepStrictEqual, doesNotThrow, equal, strictEqual, throws } from 'assert'; +import { deepStrictEqual, doesNotThrow, equal, strictEqual, throws } from 'assert'; import { ConfigurationTarget, Disposable, env, EnvironmentVariableMutator, EnvironmentVariableMutatorType, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalOptions, TerminalState, UIKind, window, workspace } from 'vscode'; -import { assertNoRpc } from '../utils'; +import { assertNoRpc, poll } from '../utils'; // Disable terminal tests: // - Web https://github.com/microsoft/vscode/issues/92826 @@ -220,7 +220,7 @@ import { assertNoRpc } from '../utils'; await new Promise(r => { disposables.push(window.onDidCloseTerminal(t => { if (t === terminal) { - deepEqual(t.exitStatus, { code: undefined }); + deepStrictEqual(t.exitStatus, { code: undefined }); r(); } })); @@ -364,13 +364,13 @@ import { assertNoRpc } from '../utils'; closeEvents.push(e.name); try { if (closeEvents.length === 1) { - deepEqual(openEvents, ['test1']); - deepEqual(dataEvents, [{ name: 'test1', data: 'write1' }]); - deepEqual(closeEvents, ['test1']); + deepStrictEqual(openEvents, ['test1']); + deepStrictEqual(dataEvents, [{ name: 'test1', data: 'write1' }]); + deepStrictEqual(closeEvents, ['test1']); } else if (closeEvents.length === 2) { - deepEqual(openEvents, ['test1', 'test2']); - deepEqual(dataEvents, [{ name: 'test1', data: 'write1' }, { name: 'test2', data: 'write2' }]); - deepEqual(closeEvents, ['test1', 'test2']); + deepStrictEqual(openEvents, ['test1', 'test2']); + deepStrictEqual(dataEvents, [{ name: 'test1', data: 'write1' }, { name: 'test2', data: 'write2' }]); + deepStrictEqual(closeEvents, ['test1', 'test2']); } resolveOnceClosed!(); } catch (e) { @@ -667,29 +667,8 @@ import { assertNoRpc } from '../utils'; }); suite('environmentVariableCollection', () => { - test.skip('should have collection variables apply to terminals immediately after setting', (done) => { - // Text to match on before passing the test - const expectedText = [ - '~a2~', - 'b1~b2~', - '~c2~c1' - ]; - let data = ''; - disposables.push(window.onDidWriteTerminalData(e => { - if (terminal !== e.terminal) { - return; - } - data += sanitizeData(e.data); - // Multiple expected could show up in the same data event - while (expectedText.length > 0 && data.indexOf(expectedText[0]) >= 0) { - expectedText.shift(); - // Check if all string are found, if so finish the test - if (expectedText.length === 0) { - disposables.push(window.onDidCloseTerminal(() => done())); - terminal.dispose(); - } - } - })); + test('should have collection variables apply to terminals immediately after setting', async () => { + // Setup collection and create terminal const collection = extensionContext.environmentVariableCollection; disposables.push({ dispose: () => collection.clear() }); collection.replace('A', '~a2~'); @@ -702,6 +681,16 @@ import { assertNoRpc } from '../utils'; C: 'c1' } }); + + // Listen for all data events + let data = ''; + disposables.push(window.onDidWriteTerminalData(e => { + if (terminal !== e.terminal) { + return; + } + data += sanitizeData(e.data); + })); + // Run both PowerShell and sh commands, errors don't matter we're just looking for // the correct output terminal.sendText('$env:A'); @@ -710,31 +699,21 @@ import { assertNoRpc } from '../utils'; terminal.sendText('echo $B'); terminal.sendText('$env:C'); terminal.sendText('echo $C'); + + // Poll for the echo results to show up + await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('b1~b2~'), 'b1~b2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~c2~c1'), '~c2~c1 should be printed'); + + // Wait for terminal to be disposed + await new Promise(r => { + disposables.push(window.onDidCloseTerminal(() => r())); + terminal.dispose(); + }); }); - test('should have collection variables apply to environment variables that don\'t exist', (done) => { - // Text to match on before passing the test - const expectedText = [ - '~a2~', - '~b2~', - '~c2~' - ]; - let data = ''; - disposables.push(window.onDidWriteTerminalData(e => { - if (terminal !== e.terminal) { - return; - } - data += sanitizeData(e.data); - // Multiple expected could show up in the same data event - while (expectedText.length > 0 && data.indexOf(expectedText[0]) >= 0) { - expectedText.shift(); - // Check if all string are found, if so finish the test - if (expectedText.length === 0) { - disposables.push(window.onDidCloseTerminal(() => done())); - terminal.dispose(); - } - } - })); + test('should have collection variables apply to environment variables that don\'t exist', async () => { + // Setup collection and create terminal const collection = extensionContext.environmentVariableCollection; disposables.push({ dispose: () => collection.clear() }); collection.replace('A', '~a2~'); @@ -747,6 +726,16 @@ import { assertNoRpc } from '../utils'; C: null } }); + + // Listen for all data events + let data = ''; + disposables.push(window.onDidWriteTerminalData(e => { + if (terminal !== e.terminal) { + return; + } + data += sanitizeData(e.data); + })); + // Run both PowerShell and sh commands, errors don't matter we're just looking for // the correct output terminal.sendText('$env:A'); @@ -755,30 +744,21 @@ import { assertNoRpc } from '../utils'; terminal.sendText('echo $B'); terminal.sendText('$env:C'); terminal.sendText('echo $C'); + + // Poll for the echo results to show up + await poll(() => Promise.resolve(), () => data.includes('~a2~'), '~a2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~c2~'), '~c2~ should be printed'); + + // Wait for terminal to be disposed + await new Promise(r => { + disposables.push(window.onDidCloseTerminal(() => r())); + terminal.dispose(); + }); }); - test.skip('should respect clearing entries', (done) => { - // Text to match on before passing the test - const expectedText = [ - '~a1~', - '~b1~' - ]; - let data = ''; - disposables.push(window.onDidWriteTerminalData(e => { - if (terminal !== e.terminal) { - return; - } - data += sanitizeData(e.data); - // Multiple expected could show up in the same data event - while (expectedText.length > 0 && data.indexOf(expectedText[0]) >= 0) { - expectedText.shift(); - // Check if all string are found, if so finish the test - if (expectedText.length === 0) { - disposables.push(window.onDidCloseTerminal(() => done())); - terminal.dispose(); - } - } - })); + test('should respect clearing entries', async () => { + // Setup collection and create terminal const collection = extensionContext.environmentVariableCollection; disposables.push({ dispose: () => collection.clear() }); collection.replace('A', '~a2~'); @@ -790,36 +770,36 @@ import { assertNoRpc } from '../utils'; B: '~b1~' } }); - // Run both PowerShell and sh commands, errors don't matter we're just looking for - // the correct output - terminal.sendText('$env:A'); - terminal.sendText('echo $A'); - terminal.sendText('$env:B'); - terminal.sendText('echo $B'); - }); - test('should respect deleting entries', (done) => { - // Text to match on before passing the test - const expectedText = [ - '~a1~', - '~b2~' - ]; + // Listen for all data events let data = ''; disposables.push(window.onDidWriteTerminalData(e => { if (terminal !== e.terminal) { return; } data += sanitizeData(e.data); - // Multiple expected could show up in the same data event - while (expectedText.length > 0 && data.indexOf(expectedText[0]) >= 0) { - expectedText.shift(); - // Check if all string are found, if so finish the test - if (expectedText.length === 0) { - disposables.push(window.onDidCloseTerminal(() => done())); - terminal.dispose(); - } - } })); + + // Run both PowerShell and sh commands, errors don't matter we're just looking for + // the correct output + terminal.sendText('$env:A'); + terminal.sendText('echo $A'); + terminal.sendText('$env:B'); + terminal.sendText('echo $B'); + + // Poll for the echo results to show up + await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b1~'), '~b1~ should be printed'); + + // Wait for terminal to be disposed + await new Promise(r => { + disposables.push(window.onDidCloseTerminal(() => r())); + terminal.dispose(); + }); + }); + + test('should respect deleting entries', async () => { + // Setup collection and create terminal const collection = extensionContext.environmentVariableCollection; disposables.push({ dispose: () => collection.clear() }); collection.replace('A', '~a2~'); @@ -831,12 +811,32 @@ import { assertNoRpc } from '../utils'; B: '~b2~' } }); + + // Listen for all data events + let data = ''; + disposables.push(window.onDidWriteTerminalData(e => { + if (terminal !== e.terminal) { + return; + } + data += sanitizeData(e.data); + })); + // Run both PowerShell and sh commands, errors don't matter we're just looking for // the correct output terminal.sendText('$env:A'); terminal.sendText('echo $A'); terminal.sendText('$env:B'); terminal.sendText('echo $B'); + + // Poll for the echo results to show up + await poll(() => Promise.resolve(), () => data.includes('~a1~'), '~a1~ should be printed'); + await poll(() => Promise.resolve(), () => data.includes('~b2~'), '~b2~ should be printed'); + + // Wait for terminal to be disposed + await new Promise(r => { + disposables.push(window.onDidCloseTerminal(() => r())); + terminal.dispose(); + }); }); test('get and forEach should work', () => { @@ -847,14 +847,14 @@ import { assertNoRpc } from '../utils'; collection.prepend('C', '~c2~'); // Verify get - deepEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace }); - deepEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append }); - deepEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend }); + deepStrictEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace }); + deepStrictEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append }); + deepStrictEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend }); // Verify forEach const entries: [string, EnvironmentVariableMutator][] = []; collection.forEach((v, m) => entries.push([v, m])); - deepEqual(entries, [ + deepStrictEqual(entries, [ ['A', { value: '~a2~', type: EnvironmentVariableMutatorType.Replace }], ['B', { value: '~b2~', type: EnvironmentVariableMutatorType.Append }], ['C', { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend }] diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index 7bf10716227..941f4d19a28 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { EOL } from 'os'; import * as vscode from 'vscode'; import { TestFS } from './memfs'; @@ -149,3 +150,35 @@ export function suiteRepeat(n: number, description: string, callback: (this: any suite(`${description} (iteration ${i})`, callback); } } + +export async function poll( + fn: () => Thenable, + acceptFn: (result: T) => boolean, + timeoutMessage: string, + retryCount: number = 200, + retryInterval: number = 100 // millis +): Promise { + let trial = 1; + let lastError: string = ''; + + while (true) { + if (trial > retryCount) { + throw new Error(`Timeout: ${timeoutMessage} after ${(retryCount * retryInterval) / 1000} seconds.\r${lastError}`); + } + + let result; + try { + result = await fn(); + if (acceptFn(result)) { + return result; + } else { + lastError = 'Did not pass accept function'; + } + } catch (e: any) { + lastError = Array.isArray(e.stack) ? e.stack.join(EOL) : e.stack; + } + + await new Promise(resolve => setTimeout(resolve, retryInterval)); + trial++; + } +}