From 56424f5c975b267b871d7182036bdfbec42f1528 Mon Sep 17 00:00:00 2001 From: Christopher Mead Date: Tue, 21 Jan 2025 10:48:04 -0700 Subject: [PATCH] e2e test: python debugger (#6021) Python debugger test checking variables, stepping and stack for a basic python script. ### QA Notes All tests should pass. @:debug --- test/e2e/infra/index.ts | 1 + test/e2e/infra/test-runner/test-tags.ts | 1 + test/e2e/infra/workbench.ts | 3 + test/e2e/pages/debug.ts | 92 ++++++++++++++++++++++ test/e2e/tests/debug/python-debug.test.ts | 94 +++++++++++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 test/e2e/pages/debug.ts create mode 100644 test/e2e/tests/debug/python-debug.test.ts diff --git a/test/e2e/infra/index.ts b/test/e2e/infra/index.ts index e0cec32ecef..d0e9adb62bd 100644 --- a/test/e2e/infra/index.ts +++ b/test/e2e/infra/index.ts @@ -35,6 +35,7 @@ export * from '../pages/clipboard'; export * from '../pages/extensions'; export * from '../pages/editors'; export * from '../pages/settings'; +export * from '../pages/debug'; // fixtures export * from './fixtures/userSettings'; diff --git a/test/e2e/infra/test-runner/test-tags.ts b/test/e2e/infra/test-runner/test-tags.ts index a7b33ec58c9..a96c9ad7850 100644 --- a/test/e2e/infra/test-runner/test-tags.ts +++ b/test/e2e/infra/test-runner/test-tags.ts @@ -44,6 +44,7 @@ export enum TestTags { TOP_ACTION_BAR = '@:top-action-bar', VARIABLES = '@:variables', WELCOME = '@:welcome', + DEBUG = '@:debug', // platform tags WEB = '@:web', diff --git a/test/e2e/infra/workbench.ts b/test/e2e/infra/workbench.ts index 33e5a5aca76..06fdd7f6a22 100644 --- a/test/e2e/infra/workbench.ts +++ b/test/e2e/infra/workbench.ts @@ -31,6 +31,7 @@ import { Clipboard } from '../pages/clipboard'; import { QuickInput } from '../pages/quickInput'; import { Extensions } from '../pages/extensions'; import { Settings } from '../pages/settings'; +import { Debug } from '../pages/debug'; import { EditorActionBar } from '../pages/editorActionBar'; export interface Commands { @@ -66,6 +67,7 @@ export class Workbench { readonly extensions: Extensions; readonly editors: Editors; readonly settings: Settings; + readonly debug: Debug; readonly editorActionBar: EditorActionBar; constructor(code: Code) { @@ -98,6 +100,7 @@ export class Workbench { this.clipboard = new Clipboard(code); this.extensions = new Extensions(code, this.quickaccess); this.settings = new Settings(code, this.editors, this.editor, this.quickaccess); + this.debug = new Debug(code); this.editorActionBar = new EditorActionBar(code.driver.page, this.viewer, this.quickaccess); } } diff --git a/test/e2e/pages/debug.ts b/test/e2e/pages/debug.ts new file mode 100644 index 00000000000..965b8f09d2b --- /dev/null +++ b/test/e2e/pages/debug.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from '@playwright/test'; +import { Code } from '../infra/code'; + + +const GLYPH_AREA = '.margin-view-overlays>:nth-child'; +const BREAKPOINT_GLYPH = '.codicon-debug-breakpoint'; +const STOP = `.debug-toolbar .action-label[aria-label*="Stop"]`; + +const VIEWLET = 'div[id="workbench.view.debug"]'; +const VARIABLE = `${VIEWLET} .debug-variables .monaco-list-row .expression`; + +const STEP_OVER = `.debug-toolbar .action-label[aria-label*="Step Over"]`; +const STEP_INTO = `.debug-toolbar .action-label[aria-label*="Step Into"]`; +const CONTINUE = `.debug-toolbar .action-label[aria-label*="Continue"]`; +const STEP_OUT = `.debug-toolbar .action-label[aria-label*="Step Out"]`; + +const STACK_FRAME = `${VIEWLET} .monaco-list-row .stack-frame`; + +export interface IStackFrame { + name: string; + lineNumber: number; +} + +/* + * Reuseable Positron debug functionality for tests to leverage + */ +export class Debug { + + constructor(private code: Code) { + + } + + async setBreakpointOnLine(lineNumber: number): Promise { + await expect(this.code.driver.page.locator(`${GLYPH_AREA}(${lineNumber})`)).toBeVisible(); + await this.code.driver.page.locator(`${GLYPH_AREA}(${lineNumber})`).click({ position: { x: 5, y: 5 } }); + await expect(this.code.driver.page.locator(BREAKPOINT_GLYPH)).toBeVisible(); + } + + async startDebugging(): Promise { + await this.code.driver.page.keyboard.press('F5'); + await expect(this.code.driver.page.locator(STOP)).toBeVisible(); + } + + async getVariables(): Promise { + const variableLocators = await this.code.driver.page.locator(VARIABLE).all(); + + const variables: string[] = []; + for (const variable of variableLocators) { + const text = await variable.textContent(); + if (text !== null) { + variables.push(text); + } + } + + return variables; + } + + async stepOver(): Promise { + await this.code.driver.page.locator(STEP_OVER).click(); + } + + async stepInto(): Promise { + await this.code.driver.page.locator(STEP_INTO).click(); + } + + async stepOut(): Promise { + await this.code.driver.page.locator(STEP_OUT).click(); + } + + async continue(): Promise { + await this.code.driver.page.locator(CONTINUE).click(); + } + + async getStack(): Promise { + const stackLocators = await this.code.driver.page.locator(STACK_FRAME).all(); + + const stack: IStackFrame[] = []; + for (const stackLocator of stackLocators) { + const name = await stackLocator.locator('.file-name').textContent(); + const lineNumberRaw = await stackLocator.locator('.line-number').textContent(); + const lineNumber = lineNumberRaw ? parseInt(lineNumberRaw.split(':').shift() || '0', 10) : 0; + stack.push({ name: name || '', lineNumber: lineNumber }); + } + + return stack; + } +} diff --git a/test/e2e/tests/debug/python-debug.test.ts b/test/e2e/tests/debug/python-debug.test.ts new file mode 100644 index 00000000000..cfb2aeff06b --- /dev/null +++ b/test/e2e/tests/debug/python-debug.test.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { expect } from '@playwright/test'; +import { test, tags } from '../_test.setup'; +import { join } from 'path'; +import { Application } from '../../infra'; + +test.use({ + suiteId: __filename +}); + +test.describe('Python Debugging', { + tag: [tags.DEBUG, tags.WEB, tags.WIN] +}, () => { + + test('Python - Verify Basic Script Debugging [C1163800]', { tag: [tags.WIN] }, async function ({ app, python, openFile }) { + + await test.step('Open file, set breakpoint and start debugging', async () => { + await openFile(join('workspaces', 'chinook-db-py', 'chinook-sqlite.py')); + + await app.workbench.debug.setBreakpointOnLine(6); + + await app.workbench.debug.startDebugging(); + }); + + const requiredStrings = ["conn", "data_file_path", "os", "pd", "sqlite3"]; + await test.step('Validate initial variable set', async () => { + + await validateExpectedVariables(app, requiredStrings); + }); + + requiredStrings.push("cur"); + await test.step('Step over and validate variable set with new member', async () => { + await app.workbench.debug.stepOver(); + + await validateExpectedVariables(app, requiredStrings); + }); + + await test.step('Validate current stack', async () => { + const stack = await app.workbench.debug.getStack(); + + expect(stack[0]).toMatchObject({ + name: "chinook-sqlite.py", + lineNumber: 7 + }); + }); + + const internalRequiredStrings = ["columns", "copy", "data", "dtype", "index", "self"]; + await test.step('Step over twice, then into and validate internal variables', async () => { + await app.workbench.debug.stepOver(); + await app.workbench.debug.stepOver(); + await app.workbench.debug.stepInto(); + + await validateExpectedVariables(app, internalRequiredStrings); + }); + + await test.step('Validate current internal stack', async () => { + const stack = await app.workbench.debug.getStack(); + + expect(stack[0]).toMatchObject({ + name: "frame.py", + lineNumber: 702 + }); + + expect(stack[1]).toMatchObject({ + name: "chinook-sqlite.py", + lineNumber: 9 + }); + }); + + await test.step('Step out, continue and wait completion', async () => { + await app.workbench.debug.stepOut(); + await app.workbench.debug.continue(); + + await expect(async () => { + const stack = await app.workbench.debug.getStack(); + expect(stack.length).toBe(0); + }).toPass({ intervals: [1_000], timeout: 60000 }); + }); + }); +}); + +async function validateExpectedVariables(app: Application, expectedVariables: string[]): Promise { + await expect(async () => { + const variables = await app.workbench.debug.getVariables(); + expectedVariables.forEach(prefix => { + expect(variables.some(line => line.startsWith(prefix))).toBeTruthy(); + }); + }).toPass({ intervals: [1_000], timeout: 60000 }); +} +