Skip to content

Commit

Permalink
e2e test: python debugger (#6021)
Browse files Browse the repository at this point in the history
Python debugger test checking variables, stepping and stack for a basic
python script.

### QA Notes

All tests should pass.

@:debug
  • Loading branch information
testlabauto authored Jan 21, 2025
1 parent 6fcb67e commit 56424f5
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 0 deletions.
1 change: 1 addition & 0 deletions test/e2e/infra/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions test/e2e/infra/test-runner/test-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export enum TestTags {
TOP_ACTION_BAR = '@:top-action-bar',
VARIABLES = '@:variables',
WELCOME = '@:welcome',
DEBUG = '@:debug',

// platform tags
WEB = '@:web',
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/infra/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Expand Down
92 changes: 92 additions & 0 deletions test/e2e/pages/debug.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
await this.code.driver.page.keyboard.press('F5');
await expect(this.code.driver.page.locator(STOP)).toBeVisible();
}

async getVariables(): Promise<string[]> {
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<any> {
await this.code.driver.page.locator(STEP_OVER).click();
}

async stepInto(): Promise<any> {
await this.code.driver.page.locator(STEP_INTO).click();
}

async stepOut(): Promise<any> {
await this.code.driver.page.locator(STEP_OUT).click();
}

async continue(): Promise<any> {
await this.code.driver.page.locator(CONTINUE).click();
}

async getStack(): Promise<IStackFrame[]> {
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;
}
}
94 changes: 94 additions & 0 deletions test/e2e/tests/debug/python-debug.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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 });
}

0 comments on commit 56424f5

Please sign in to comment.