Skip to content

Commit

Permalink
feat: Add support for Logpoints (#343)
Browse files Browse the repository at this point in the history
* feat: initial dev of logpoint. (#258)

feat: adding types for string-replace-async library

feat: linting and prettifying

feat: adding tests for logpoint class

feat: fixing formatting

feat: fixing doc for attribute

* Implemented logpoint support.

Co-authored-by: Augusto César Dias <[email protected]>
Co-authored-by: Damjan Cvetko <[email protected]>
  • Loading branch information
3 people authored Jul 29, 2021
1 parent 9a73045 commit cb091b7
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 1 deletion.
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')
})
})
})

0 comments on commit cb091b7

Please sign in to comment.