Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 20
node-version: 22
cache: npm

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: '3.12'

Expand All @@ -37,7 +37,7 @@ jobs:
python python/test_ply_export.py

# Caches the downloaded VS Code and test extensions between runs
- uses: actions/cache@v4
- uses: actions/cache@v6
with:
path: .vscode-test
key: vscode-test-${{ runner.os }}-${{ hashFiles('.vscode-test.mjs') }}
Expand Down
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: 20
node-version: 22
cache: npm

- uses: actions/setup-python@v5
- uses: actions/setup-python@v7
with:
python-version: '3.12'

Expand All @@ -43,7 +43,7 @@ jobs:
- name: Package extension
run: npx --yes @vscode/vsce@3 package --out python-ply-preview.vsix

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
with:
name: vsix
path: python-ply-preview.vsix
Expand All @@ -58,9 +58,9 @@ jobs:
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: vsix

Expand Down
16 changes: 9 additions & 7 deletions src/ViewPLYService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ and (o.shape[-1] in (3, 4, 6) or o.shape[-2] in (3, 4, 6))))\
// --- Service Class ---

export default class ViewPLYService {
private static readonly LOG_PREFIX = '[ViewPLY]';
private static readonly PREVIEW_FOLDER = 'ply_preview';
private static readonly VSCODE_FOLDER = '.vscode';
private static readonly logPrefix = '[ViewPLY]';
private static readonly previewFolder = 'ply_preview';
private static readonly vscodeFolder = '.vscode';

private readonly baseStorageDir: string;
private readonly exportScript: string;
private outputChannel: vscode.OutputChannel;

public constructor(extensionPath: string) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? ".";
this.baseStorageDir = join(workspaceFolder, ViewPLYService.VSCODE_FOLDER, ViewPLYService.PREVIEW_FOLDER);
this.baseStorageDir = join(workspaceFolder, ViewPLYService.vscodeFolder, ViewPLYService.previewFolder);
this.exportScript = readFileSync(join(extensionPath, 'python', 'ply_export.py'), 'utf8');

// Create an Output Channel for better debugging visibility
Expand Down Expand Up @@ -92,7 +92,9 @@ export default class ViewPLYService {
}

const wordRange = document.getWordRangeAtPosition(range.start);
if (!wordRange) return;
if (!wordRange) {
return;
}
const expression = document.getText(wordRange);

const lineText = document.lineAt(range.start.line).text;
Expand Down Expand Up @@ -196,15 +198,15 @@ export default class ViewPLYService {

private logInfo(message: string): void {
const timestamp = new Date().toLocaleTimeString();
const msg = `${ViewPLYService.LOG_PREFIX} [${timestamp}] ${message}`;
const msg = `${ViewPLYService.logPrefix} [${timestamp}] ${message}`;
console.log(msg);
this.outputChannel.appendLine(msg);
}

private logError(message: string, error: any): void {
const errorMessage = error instanceof Error ? error.message : String(error);
const timestamp = new Date().toLocaleTimeString();
const msg = `${ViewPLYService.LOG_PREFIX} [${timestamp}] ERROR: ${message} | Details: ${errorMessage}`;
const msg = `${ViewPLYService.logPrefix} [${timestamp}] ERROR: ${message} | Details: ${errorMessage}`;
console.error(msg);
this.outputChannel.appendLine(msg);
}
Expand Down
109 changes: 85 additions & 24 deletions src/test/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import { existsSync, readdirSync, readFileSync } from 'fs';
import { existsSync, readdirSync, readFileSync, rmSync } from 'fs';
import { join } from 'path';

const VIEW_COMMAND = 'python-ply-preview.view';
Expand Down Expand Up @@ -29,17 +29,79 @@ function readVertexCount(path: string): number {
/** Resolves once the debugger is paused on a stack frame. */
function waitForStop(): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Debugger did not stop at the breakpoint')), 60000);
const done = () => {
clearTimeout(timer);
listener.dispose();
};
const timer = setTimeout(() => {
done();
const item = vscode.debug.activeStackItem;
const state = item instanceof vscode.DebugStackFrame ? 'frame' : item instanceof vscode.DebugThread ? 'thread' : 'none';
reject(new Error(`Debugger did not stop at the breakpoint (activeStackItem: ${state}).\n` +
`Debug adapter log:\n${adapterLog.join('\n')}`));
}, 60000);
const listener = vscode.debug.onDidChangeActiveStackItem(item => {
if (item instanceof vscode.DebugStackFrame) {
clearTimeout(timer);
listener.dispose();
done();
resolve();
}
});
});
}

async function startDebuggingAndWaitForStop(): Promise<void> {
// fixture.py calls breakpoint(): an editor breakpoint races with debugpy startup on slow CI runners
const stopped = waitForStop();
// The timeout can fire before `await stopped` below; it is still rethrown there
stopped.catch(() => undefined);
const started = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders![0], {
type: 'debugpy',
name: 'ViewPLY integration test',
request: 'launch',
program: fixturePath,
// Resolved in .vscode-test.mjs
python: process.env.VIEWPLY_TEST_PYTHON,
console: 'internalConsole',
justMyCode: true,
});
assert.ok(started, 'Debug session failed to start');
await stopped;
}

// Debug Adapter Protocol traffic with debugpy, reported when the debugger fails to stop
const adapterLog: string[] = [];
const adapterLogStart = Date.now();

function logAdapter(line: string): void {
adapterLog.push(`+${Date.now() - adapterLogStart}ms ${line}`);
if (adapterLog.length > 100) {
adapterLog.shift();
}
}

function trackDebugAdapter(): vscode.Disposable {
return vscode.debug.registerDebugAdapterTrackerFactory('debugpy', {
createDebugAdapterTracker: () => ({
onWillReceiveMessage: (message: any) => {
if (message.type === 'request') {
logAdapter(`-> ${message.command}`);
}
},
onDidSendMessage: (message: any) => {
if (message.type === 'event') {
const detail = message.event === 'output' ? `: ${String(message.body?.output).trim().slice(0, 300)}`
: message.event === 'stopped' ? ` (${message.body?.reason})` : '';
logAdapter(`<- event ${message.event}${detail}`);
} else if (message.type === 'response' && !message.success) {
logAdapter(`<- ${message.command} failed: ${message.message}`);
}
},
onError: error => logAdapter(`adapter error: ${error.message}`),
onExit: (code, signal) => logAdapter(`adapter exited: code=${code} signal=${signal}`),
}),
});
}

/** Returns the range of the first occurrence of `text` in the document. */
function rangeOf(document: vscode.TextDocument, text: string): vscode.Range {
const offset = document.getText().indexOf(text);
Expand All @@ -57,34 +119,33 @@ async function codeActionTitles(document: vscode.TextDocument, range: vscode.Ran

suite('View PLY during a debug session', () => {
let document: vscode.TextDocument;
let adapterTracker: vscode.Disposable;

suiteSetup(async function () {
// Room for one retry of the debugger start below
this.timeout(180000);
adapterTracker = trackDebugAdapter();

// Start clean: manual F5 runs in this folder can leave files from sessions that never terminated
rmSync(previewDir, { recursive: true, force: true });

suiteSetup(async () => {
document = await vscode.workspace.openTextDocument(fixturePath);
await vscode.window.showTextDocument(document);

const breakpointLine = document.getText().split(/\r?\n/).findIndex(line => line.includes('# BREAKPOINT'));
vscode.debug.addBreakpoints([
new vscode.SourceBreakpoint(new vscode.Location(document.uri, new vscode.Position(breakpointLine, 0))),
]);

const stopped = waitForStop();
const started = await vscode.debug.startDebugging(vscode.workspace.workspaceFolders![0], {
type: 'debugpy',
name: 'ViewPLY integration test',
request: 'launch',
program: fixturePath,
// Resolved in .vscode-test.mjs
python: process.env.VIEWPLY_TEST_PYTHON,
console: 'internalConsole',
justMyCode: true,
});
assert.ok(started, 'Debug session failed to start');
await stopped;
// On CI runners debugpy occasionally (~1 in 25 runs) never reports the stop; retry once and keep the
// adapter log of the failed attempt so a recurring problem still surfaces
try {
await startDebuggingAndWaitForStop();
} catch (error) {
console.warn(`First debugger start failed, retrying.\n${error instanceof Error ? error.message : error}`);
await vscode.debug.stopDebugging();
await startDebuggingAndWaitForStop();
}
});

suiteTeardown(async () => {
await vscode.debug.stopDebugging();
vscode.debug.removeBreakpoints(vscode.debug.breakpoints);
adapterTracker.dispose();
});

test('code action is offered for point clouds without writing a file', async () => {
Expand Down
3 changes: 2 additions & 1 deletion test-fixtures/workspace/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@


def main():
print(pts, pts_rgb, lidar, T4x4, subset) # BREAKPOINT
breakpoint() # debugpy stops here; no editor breakpoint needed
print(pts, pts_rgb, lidar, T4x4, subset)


main()
Loading