11'use strict'
22
3+ const assert = require ( 'assert' )
34const { basename, join } = require ( 'path' )
45const { readFileSync } = require ( 'fs' )
56const { randomUUID } = require ( 'crypto' )
@@ -12,27 +13,111 @@ const { generateProbeConfig } = require('../../packages/dd-trace/test/debugger/d
1213const BREAKPOINT_TOKEN = '// BREAKPOINT'
1314const pollInterval = 0.1
1415
16+ /**
17+ * @typedef {import('../../packages/dd-trace/test/debugger/devtools_client/utils').ProbeConfig } ProbeConfig
18+ */
19+
20+ /**
21+ * @typedef {typeof import('../../packages/dd-trace/test/debugger/devtools_client/utils').generateProbeConfig }
22+ * GenerateProbeConfigFn
23+ */
24+
25+ /**
26+ * @typedef {Object } BreakpointInfo
27+ * @property {string } sourceFile
28+ * @property {string } deployedFile
29+ * @property {number } line
30+ * @property {string } url
31+ */
32+
33+ /**
34+ * A breakpoint with helpers bound for convenient testing.
35+ *
36+ * @typedef {BreakpointInfo & {
37+ * rcConfig: object|null,
38+ * triggerBreakpoint: (url: string) => Promise<import('axios').AxiosResponse<unknown>>,
39+ * generateRemoteConfig: (overrides?: object) => object,
40+ * generateProbeConfig: GenerateProbeConfigFn
41+ * }} EnrichedBreakpoint
42+ */
43+
44+ /**
45+ * The live‑debugger integration test harness returned by {@link setup}. Provides the spawned app process, fake agent,
46+ * axios client, and helpers to generate remote config and trigger breakpoints.
47+ *
48+ * @typedef {Object } DebuggerTestEnvironment
49+ * @property {BreakpointInfo } breakpoint - Primary breakpoint metadata.
50+ * @property {EnrichedBreakpoint[] } breakpoints - All discovered breakpoints with helpers.
51+ * @property {import('axios').AxiosInstance } axios - HTTP client bound to the test app. Throws if accessed before
52+ * `beforeEach` hook runs.
53+ * @property {string } appFile - Absolute path to the test app entry file. Throws if accessed before `before` hook runs.
54+ * @property {import('../helpers').FakeAgent } agent - Started fake agent instance. Throws if accessed before
55+ * `beforeEach` hook runs.
56+ * @property {import('../helpers').SpawnedProcess } proc - Spawned app process. Throws if accessed before `beforeEach`
57+ * hook runs.
58+ * @property {object|null } rcConfig - Default remote config for the primary breakpoint.
59+ * @property {() => Promise<import('axios').AxiosResponse<unknown>> } triggerBreakpoint - Triggers the primary breakpoint
60+ * once installed.
61+ * @property {(overrides?: object) => object } generateRemoteConfig - Generates RC for the primary breakpoint.
62+ * @property {GenerateProbeConfigFn } generateProbeConfig - Generates probe config for the primary breakpoint.
63+ */
64+
1565module . exports = {
1666 pollInterval,
1767 setup
1868}
1969
70+ /**
71+ * Setup the integration test harness for live‑debugger scenarios.
72+ *
73+ * @param {object } [options] The options for the test environment.
74+ * @param {object } [options.env] The environment variables to set in the test environment.
75+ * @param {string } [options.testApp] The path to the test application file.
76+ * @param {string } [options.testAppSource] The path to the test application source file.
77+ * @param {string[] } [options.dependencies] The dependencies to install in the test environment.
78+ * @param {boolean } [options.silent] Whether to silence the output of the test environment.
79+ * @param {(data: Buffer) => void } [options.stdioHandler] The function to handle the standard output of the test
80+ * environment.
81+ * @param {(data: Buffer) => void } [options.stderrHandler] The function to handle the standard error output of the test
82+ * environment.
83+ * @returns {DebuggerTestEnvironment } Test harness with agent, app process, axios client and breakpoint helpers.
84+ */
2085function setup ( { env, testApp, testAppSource, dependencies, silent, stdioHandler, stderrHandler } = { } ) {
21- let cwd
86+ let cwd , axios , appFile , agent , proc
2287
2388 const breakpoints = getBreakpointInfo ( {
2489 deployedFile : testApp ,
2590 sourceFile : testAppSource ,
2691 stackIndex : 1 // `1` to disregard the `setup` function
27- } )
92+ } ) . map ( ( breakpoint ) => /** @type {EnrichedBreakpoint } */ ( {
93+ rcConfig : null ,
94+ triggerBreakpoint : triggerBreakpoint . bind ( null , breakpoint . url ) ,
95+ generateRemoteConfig : generateRemoteConfig . bind ( null , breakpoint ) ,
96+ generateProbeConfig : generateProbeConfig . bind ( null , breakpoint ) ,
97+ ...breakpoint
98+ } ) )
2899
100+ /** @type {DebuggerTestEnvironment } */
29101 const t = {
30102 breakpoint : breakpoints [ 0 ] ,
31103 breakpoints,
32104
33- axios : null ,
34- appFile : null ,
35- agent : null ,
105+ get axios ( ) {
106+ assert ( axios , 'axios must be initialized in beforeEach hook' )
107+ return axios
108+ } ,
109+ get appFile ( ) {
110+ assert ( appFile , 'appFile must be initialized in before hook' )
111+ return appFile
112+ } ,
113+ get agent ( ) {
114+ assert ( agent , 'agent must be initialized in beforeEach hook' )
115+ return agent
116+ } ,
117+ get proc ( ) {
118+ assert ( proc , 'proc must be initialized in beforeEach hook' )
119+ return proc
120+ } ,
36121
37122 // Default to the first breakpoint in the file (normally there's only one)
38123 rcConfig : null ,
@@ -41,18 +126,13 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
41126 generateProbeConfig : generateProbeConfig . bind ( null , breakpoints [ 0 ] )
42127 }
43128
44- // Allow specific access to each breakpoint
45- for ( let i = 0 ; i < breakpoints . length ; i ++ ) {
46- t . breakpoints [ i ] = {
47- rcConfig : null ,
48- triggerBreakpoint : triggerBreakpoint . bind ( null , breakpoints [ i ] . url ) ,
49- generateRemoteConfig : generateRemoteConfig . bind ( null , breakpoints [ i ] ) ,
50- generateProbeConfig : generateProbeConfig . bind ( null , breakpoints [ i ] ) ,
51- ...breakpoints [ i ]
52- }
53- }
54-
55- // Trigger the breakpoint once probe is successfully installed
129+ /**
130+ * Trigger the breakpoint once probe is successfully installed
131+ *
132+ * @param {string } url The URL of the HTTP route containing the breakpoint to trigger.
133+ * @returns {Promise<import('axios').AxiosResponse<unknown>> } A promise that resolves with the response from the HTTP
134+ * request after the breakpoint is triggered.
135+ */
56136 async function triggerBreakpoint ( url ) {
57137 let triggered = false
58138 return new Promise ( ( resolve , reject ) => {
@@ -67,6 +147,13 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
67147 } )
68148 }
69149
150+ /**
151+ * Generate a remote config for a breakpoint
152+ *
153+ * @param {BreakpointInfo } breakpoint - The breakpoint to generate a remote config for.
154+ * @param {object } [overrides] - The overrides to apply to the remote config.
155+ * @returns {object } - The remote config.
156+ */
70157 function generateRemoteConfig ( breakpoint , overrides = { } ) {
71158 overrides . id = overrides . id || randomUUID ( )
72159 return {
@@ -81,7 +168,7 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
81168 before ( function ( ) {
82169 cwd = sandboxCwd ( )
83170 // The sandbox uses the `integration-tests` folder as its root
84- t . appFile = join ( cwd , 'debugger' , breakpoints [ 0 ] . deployedFile )
171+ appFile = join ( cwd , 'debugger' , breakpoints [ 0 ] . deployedFile )
85172 } )
86173
87174 beforeEach ( async function ( ) {
@@ -90,8 +177,8 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
90177 // Allow specific access to each breakpoint
91178 t . breakpoints . forEach ( ( breakpoint ) => { breakpoint . rcConfig = generateRemoteConfig ( breakpoint ) } )
92179
93- t . agent = await new FakeAgent ( ) . start ( )
94- t . proc = await spawnProc ( t . appFile , {
180+ agent = await new FakeAgent ( ) . start ( )
181+ proc = await spawnProc ( /** @type { string } */ ( t . appFile ) , {
95182 cwd,
96183 env : {
97184 DD_DYNAMIC_INSTRUMENTATION_ENABLED : 'true' ,
@@ -103,7 +190,7 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
103190 } ,
104191 silent : silent ?? false
105192 } , stdioHandler , stderrHandler )
106- t . axios = Axios . create ( { baseURL : t . proc . url } )
193+ axios = Axios . create ( { baseURL : t . proc . url } )
107194 } )
108195
109196 afterEach ( async function ( ) {
@@ -114,10 +201,20 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
114201 return t
115202}
116203
204+ /**
205+ * Get breakpoint information from a test file by scanning for BREAKPOINT_TOKEN markers.
206+ *
207+ * @param {Object } [options] - Options for finding breakpoints.
208+ * @param {string } [options.deployedFile] - The deployed file path. If not provided, will be inferred from the stack
209+ * trace.
210+ * @param {string } [options.sourceFile] - The source file path. Defaults to `deployedFile` if not provided.
211+ * @param {number } [options.stackIndex=0] - The stack index to use when inferring the file from the stack trace.
212+ * @returns {BreakpointInfo[] } An array of breakpoint information objects found in the file.
213+ */
117214function getBreakpointInfo ( { deployedFile, sourceFile = deployedFile , stackIndex = 0 } = { } ) {
118215 if ( ! deployedFile ) {
119216 // First, get the filename of file that called this function
120- const testFile = new Error ( ) . stack
217+ const testFile = /** @type { string } */ ( new Error ( ) . stack )
121218 . split ( '\n' ) [ stackIndex + 2 ] // +2 to skip this function + the first line, which is the error message
122219 . split ( ' (' ) [ 1 ]
123220 . slice ( 0 , - 1 )
@@ -127,6 +224,8 @@ function getBreakpointInfo ({ deployedFile, sourceFile = deployedFile, stackInde
127224 deployedFile = sourceFile = join ( 'target-app' , basename ( testFile ) . replace ( '.spec' , '' ) )
128225 }
129226
227+ assert ( sourceFile , 'sourceFile must be provided or inferred from stack trace' )
228+
130229 // Finally, find the line number(s) of the breakpoint(s)
131230 const lines = readFileSync ( join ( __dirname , sourceFile ) , 'utf8' ) . split ( '\n' )
132231 const result = [ ]
0 commit comments