Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for Logpoints #343

Merged
merged 2 commits into from
Jul 29, 2021
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.17.0]

## Added

- Added logpoint support.

## [1.16.3]

## Fixed
Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"minimatch": "^3.0.4",
"moment": "^2.29.1",
"relateurl": "^0.2.7",
"string-replace-async": "^2.0.0",
"url-relative": "^1.0.0",
"urlencode": "^1.1.0",
"vscode-debugadapter": "^1.47.0",
Expand Down
53 changes: 53 additions & 0 deletions src/logpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import stringReplaceAsync = require('string-replace-async')
import { isWindowsUri } from './paths'

export class LogPointManager {
private _logpoints = new Map<string, Map<number, string>>()

public addLogPoint(fileUri: string, lineNumber: number, logMessage: string) {
if (isWindowsUri(fileUri)) {
fileUri = fileUri.toLowerCase()
}
if (!this._logpoints.has(fileUri)) {
this._logpoints.set(fileUri, new Map<number, string>())
}
this._logpoints.get(fileUri)!.set(lineNumber, logMessage)
}

public clearFromFile(fileUri: string) {
if (isWindowsUri(fileUri)) {
fileUri = fileUri.toLowerCase()
}
if (this._logpoints.has(fileUri)) {
this._logpoints.get(fileUri)!.clear()
}
}

public hasLogPoint(fileUri: string, lineNumber: number): boolean {
if (isWindowsUri(fileUri)) {
fileUri = fileUri.toLowerCase()
}
return this._logpoints.has(fileUri) && this._logpoints.get(fileUri)!.has(lineNumber)
}

public async resolveExpressions(
fileUri: string,
lineNumber: number,
callback: (expr: string) => Promise<string>
): Promise<string> {
if (isWindowsUri(fileUri)) {
fileUri = fileUri.toLowerCase()
}
if (!this.hasLogPoint(fileUri, lineNumber)) {
return Promise.reject('Logpoint not found')
}
const expressionRegex = /\{(.*?)\}/gm
return await stringReplaceAsync(
this._logpoints.get(fileUri)!.get(lineNumber)!,
expressionRegex,
function (_: string, group: string) {
return group.length === 0 ? Promise.resolve('') : callback(group)
}
)
}
}
2 changes: 1 addition & 1 deletion src/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export function convertClientPathToDebugger(localPath: string, pathMapping?: { [
return serverFileUri
}

function isWindowsUri(path: string): boolean {
export function isWindowsUri(path: string): boolean {
return /^file:\/\/\/[a-zA-Z]:\//.test(path)
}

Expand Down
32 changes: 32 additions & 0 deletions src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { convertClientPathToDebugger, convertDebuggerPathToClient } from './path
import minimatch = require('minimatch')
import { BreakpointManager, BreakpointAdapter } from './breakpoints'
import * as semver from 'semver'
import { LogPointManager } from './logpoint'

if (process.env['VSCODE_NLS_CONFIG']) {
try {
Expand Down Expand Up @@ -148,6 +149,13 @@ class PhpDebugSession extends vscode.DebugSession {
/** Breakpoint Adapters */
private _breakpointAdapters = new Map<xdebug.Connection, BreakpointAdapter>()

/**
* The manager for logpoints. Since xdebug does not support anything like logpoints,
* it has to be managed by the extension/debug server. It does that by a Map referencing
* the log messages per file. Xdebug sees it as a regular breakpoint.
*/
private _logPointManager = new LogPointManager()

/** the promise that gets resolved once we receive the done request */
private _donePromise: Promise<void>

Expand All @@ -170,6 +178,7 @@ class PhpDebugSession extends vscode.DebugSession {
supportsEvaluateForHovers: false,
supportsConditionalBreakpoints: true,
supportsFunctionBreakpoints: true,
supportsLogPoints: true,
exceptionBreakpointFilters: [
{
filter: 'Notice',
Expand Down Expand Up @@ -459,6 +468,24 @@ class PhpDebugSession extends vscode.DebugSession {
} else {
stoppedEventReason = 'breakpoint'
}
// Check for log points
if (this._logPointManager.hasLogPoint(response.fileUri, response.line)) {
const logMessage = await this._logPointManager.resolveExpressions(
response.fileUri,
response.line,
async (expr: string): Promise<string> => {
const evaluated = await connection.sendEvalCommand(expr)
return formatPropertyValue(evaluated.result)
}
)

this.sendEvent(new vscode.OutputEvent(logMessage + '\n', 'console'))
if (stoppedEventReason === 'breakpoint') {
const responseCommand = await connection.sendRunCommand()
await this._checkStatus(responseCommand)
return
}
}
const event: VSCodeDebugProtocol.StoppedEvent = new vscode.StoppedEvent(
stoppedEventReason,
connection.id,
Expand Down Expand Up @@ -533,6 +560,11 @@ class PhpDebugSession extends vscode.DebugSession {
const fileUri = convertClientPathToDebugger(args.source.path!, this._args.pathMappings)
const vscodeBreakpoints = this._breakpointManager.setBreakPoints(args.source, fileUri, args.breakpoints!)
response.body = { breakpoints: vscodeBreakpoints }
// Process logpoints
this._logPointManager.clearFromFile(fileUri)
args.breakpoints!.filter(breakpoint => breakpoint.logMessage).forEach(breakpoint => {
this._logPointManager.addLogPoint(fileUri, breakpoint.line, breakpoint.logMessage!)
})
} catch (error) {
this.sendErrorResponse(response, error)
return
Expand Down
98 changes: 98 additions & 0 deletions src/test/logpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { LogPointManager } from '../logpoint'
import * as assert from 'assert'

describe('logpoint', () => {
const FILE_URI1 = 'file://my/file1'
const FILE_URI2 = 'file://my/file2'
const FILE_URI3 = 'file://my/file3'

const LOG_MESSAGE_VAR = '{$variable1}'
const LOG_MESSAGE_MULTIPLE = '{$variable1} {$variable3} {$variable2}'
const LOG_MESSAGE_TEXT_AND_VAR = 'This is my {$variable1}'
const LOG_MESSAGE_TEXT_AND_MULTIVAR = 'Those variables: {$variable1} ${$variable2} should be replaced'
const LOG_MESSAGE_REPEATED_VAR = 'This {$variable1} and {$variable1} should be equal'
const LOG_MESSAGE_BADLY_FORMATED_VAR = 'Only {$variable1} should be resolved and not }$variable1 and $variable1{}'

const REPLACE_FUNCTION = (str: string): Promise<string> => {
return Promise.resolve(`${str}_value`)
}

let logPointManager: LogPointManager

beforeEach('create new instance', () => (logPointManager = new LogPointManager()))

describe('basic map management', () => {
it('should contain added logpoints', () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR)
logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR)
logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR)
logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR)

assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true)

assert.equal(logPointManager.hasLogPoint(FILE_URI1, 12), false)
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 13), false)
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 10), false)
})

it('should add and clear entries', () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR)
logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR)
logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR)
logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR)

assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true)

logPointManager.clearFromFile(FILE_URI1)

assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), false)
assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), false)
assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true)
assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true)
})
})

describe('variable resolution', () => {
it('should resolve variables', async () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR)
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
assert.equal(result, '$variable1_value')
})

it('should resolve multiple variables', async () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_MULTIPLE)
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
assert.equal(result, '$variable1_value $variable3_value $variable2_value')
})

it('should resolve variables with text', async () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_VAR)
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
assert.equal(result, 'This is my $variable1_value')
})

it('should resolve multiple variables with text', async () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_MULTIVAR)
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
assert.equal(result, 'Those variables: $variable1_value $$variable2_value should be replaced')
})

it('should resolve repeated variables', async () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_REPEATED_VAR)
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
assert.equal(result, 'This $variable1_value and $variable1_value should be equal')
})

it('should resolve repeated bad formated messages correctly', async () => {
logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_BADLY_FORMATED_VAR)
const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION)
assert.equal(result, 'Only $variable1_value should be resolved and not }$variable1 and $variable1')
})
})
})