From 5eba2f631f8c30790ac091b1d0c0ad17af4c5026 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 4 May 2023 12:05:03 +0200 Subject: [PATCH] Expose client (browser) logs for smoke tests running in browser (fix #180102) (#181491) --- src/vs/platform/driver/browser/driver.ts | 34 +++++++++++++++++-- src/vs/platform/driver/common/driver.ts | 30 +++++++++------- .../driver/electron-sandbox/driver.ts | 15 +++++--- src/vs/workbench/browser/window.ts | 7 ++-- src/vs/workbench/electron-sandbox/window.ts | 2 +- test/automation/src/code.ts | 10 ++++-- test/automation/src/playwrightDriver.ts | 27 ++++++++++++++- 7 files changed, 98 insertions(+), 27 deletions(-) diff --git a/src/vs/platform/driver/browser/driver.ts b/src/vs/platform/driver/browser/driver.ts index 56fa6897a9d..546187c2921 100644 --- a/src/vs/platform/driver/browser/driver.ts +++ b/src/vs/platform/driver/browser/driver.ts @@ -6,11 +6,39 @@ import { getClientArea, getTopLeftOffset } from 'vs/base/browser/dom'; import { coalesce } from 'vs/base/common/arrays'; import { language, locale } from 'vs/base/common/platform'; -import { IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver } from 'vs/platform/driver/common/driver'; +import { IElement, ILocaleInfo, ILocalizedStrings, ILogFile, IWindowDriver } from 'vs/platform/driver/common/driver'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import localizedStrings from 'vs/platform/languagePacks/common/localizedStrings'; export class BrowserWindowDriver implements IWindowDriver { + constructor( + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService + ) { + } + + async getLogs(): Promise { + const result: ILogFile[] = []; + + const logs = await this.fileService.resolve(this.environmentService.logsHome); + + for (const { name, isDirectory, resource } of logs.children || []) { + if (isDirectory) { + continue; + } + + const contents = (await this.fileService.readFile(resource)).value.toString(); + if (contents) { + result.push({ name, contents }); + } + } + + return result; + } + async setValue(selector: string, text: string): Promise { const element = document.querySelector(selector); @@ -199,6 +227,6 @@ export class BrowserWindowDriver implements IWindowDriver { } } -export function registerWindowDriver(): void { - Object.assign(window, { driver: new BrowserWindowDriver() }); +export function registerWindowDriver(instantiationService: IInstantiationService): void { + Object.assign(window, { driver: instantiationService.createInstance(BrowserWindowDriver) }); } diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index b6633667007..49f076ef786 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -7,24 +7,29 @@ //*START export interface IElement { - tagName: string; - className: string; - textContent: string; - attributes: { [name: string]: string }; - children: IElement[]; - top: number; - left: number; + readonly tagName: string; + readonly className: string; + readonly textContent: string; + readonly attributes: { [name: string]: string }; + readonly children: IElement[]; + readonly top: number; + readonly left: number; } export interface ILocaleInfo { - language: string; - locale?: string; + readonly language: string; + readonly locale?: string; } export interface ILocalizedStrings { - open: string; - close: string; - find: string; + readonly open: string; + readonly close: string; + readonly find: string; +} + +export interface ILogFile { + readonly name: string; + readonly contents: string; } export interface IWindowDriver { @@ -37,6 +42,7 @@ export interface IWindowDriver { writeInTerminal(selector: string, text: string): Promise; getLocaleInfo(): Promise; getLocalizedStrings(): Promise; + getLogs(): Promise; exitApplication(): Promise; } //*END diff --git a/src/vs/platform/driver/electron-sandbox/driver.ts b/src/vs/platform/driver/electron-sandbox/driver.ts index fb9b9a596ff..df9c305cb22 100644 --- a/src/vs/platform/driver/electron-sandbox/driver.ts +++ b/src/vs/platform/driver/electron-sandbox/driver.ts @@ -4,6 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { BrowserWindowDriver } from 'vs/platform/driver/browser/driver'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; interface INativeWindowDriverHelper { exitApplication(): Promise; @@ -11,8 +14,12 @@ interface INativeWindowDriverHelper { class NativeWindowDriver extends BrowserWindowDriver { - constructor(private readonly helper: INativeWindowDriverHelper) { - super(); + constructor( + private readonly helper: INativeWindowDriverHelper, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService + ) { + super(fileService, environmentService); } override exitApplication(): Promise { @@ -20,6 +27,6 @@ class NativeWindowDriver extends BrowserWindowDriver { } } -export function registerWindowDriver(helper: INativeWindowDriverHelper): void { - Object.assign(window, { driver: new NativeWindowDriver(helper) }); +export function registerWindowDriver(instantiationService: IInstantiationService, helper: INativeWindowDriverHelper): void { + Object.assign(window, { driver: instantiationService.createInstance(NativeWindowDriver, helper) }); } diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index f7d6c93053c..b5b325c51ef 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -18,7 +18,7 @@ import { localize } from 'vs/nls'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -36,7 +36,8 @@ export class BrowserWindow extends Disposable { @ILabelService private readonly labelService: ILabelService, @IProductService private readonly productService: IProductService, @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -129,7 +130,7 @@ export class BrowserWindow extends Disposable { private setupDriver(): void { if (this.environmentService.enableSmokeTestDriver) { - registerWindowDriver(); + registerWindowDriver(this.instantiationService); } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index cf6af9debb4..7accc1db263 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -795,7 +795,7 @@ export class NativeWindow extends Disposable { const that = this; let pendingQuit = false; - registerWindowDriver({ + registerWindowDriver(this.instantiationService, { async exitApplication(): Promise { if (pendingQuit) { that.logService.info('[driver] not handling exitApplication() due to pending quit() call'); diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 5aa5e9c1253..e8ad52540b5 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -6,7 +6,7 @@ import * as cp from 'child_process'; import * as os from 'os'; import * as treekill from 'tree-kill'; -import { IElement, ILocaleInfo, ILocalizedStrings } from './driver'; +import { IElement, ILocaleInfo, ILocalizedStrings, ILogFile } from './driver'; import { Logger, measureAndLog } from './logger'; import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { PlaywrightDriver } from './playwrightDriver'; @@ -242,14 +242,18 @@ export class Code { await this.poll(() => this.driver.writeInTerminal(selector, value), () => true, `writeInTerminal '${selector}'`); } - async getLocaleInfo(): Promise { + getLocaleInfo(): Promise { return this.driver.getLocaleInfo(); } - async getLocalizedStrings(): Promise { + getLocalizedStrings(): Promise { return this.driver.getLocalizedStrings(); } + getLogs(): Promise { + return this.driver.getLogs(); + } + private async poll( fn: () => Promise, acceptFn: (result: T) => boolean, diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 1c21a31bb59..4ab495bef30 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as playwright from '@playwright/test'; -import { join } from 'path'; +import { dirname, join } from 'path'; +import { promises } from 'fs'; import { IWindowDriver } from './driver'; import { PageFunction } from 'playwright-core/types/structs'; import { measureAndLog } from './logger'; @@ -107,6 +108,15 @@ export class PlaywrightDriver { // Ignore } + // Web: Extract client logs + if (this.options.web) { + try { + await measureAndLog(() => this.saveWebClientLogs(), 'saveWebClientLogs()', this.options.logger); + } catch (error) { + this.options.logger.log(`Error saving web client logs (${error})`); + } + } + // Web: exit via `close` method if (this.options.web) { try { @@ -131,6 +141,17 @@ export class PlaywrightDriver { } } + private async saveWebClientLogs(): Promise { + const logs = await this.getLogs(); + + for (const log of logs) { + const absoluteLogsPath = join(this.options.logsPath, log.name); + + await promises.mkdir(dirname(absoluteLogsPath), { recursive: true }); + await promises.writeFile(absoluteLogsPath, log.contents); + } + } + async dispatchKeybinding(keybinding: string) { const chords = keybinding.split(' '); for (let i = 0; i < chords.length; i++) { @@ -206,6 +227,10 @@ export class PlaywrightDriver { return this.evaluateWithDriver(([driver]) => driver.getLocalizedStrings()); } + async getLogs() { + return this.page.evaluate(([driver]) => driver.getLogs(), [await this.getDriverHandle()] as const); + } + private async evaluateWithDriver(pageFunction: PageFunction[], T>) { return this.page.evaluate(pageFunction, [await this.getDriverHandle()]); }