From 19ce931c8d8e605715cefff96e024944602ff55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Mon, 27 Nov 2023 12:32:23 +0100 Subject: [PATCH 1/2] feat: Request recordings Fixes #38 --- README.md | 2 +- src/Recording.ts | 6 +- src/hooks/http.ts | 25 +- src/recorder.ts | 10 +- test/__snapshots__/express.test.ts.snap | 892 +++++++++++++----------- test/express.test.ts | 6 +- test/helpers.ts | 14 +- 7 files changed, 546 insertions(+), 409 deletions(-) diff --git a/README.md b/README.md index 9b0195d4..67a5bf67 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,4 @@ development, not ready for production use, and the feature set is currently lim - Node 18+ supported. - Instruments all the files under current directory that aren't node_modules. - Only captures named `function`s and methods. -- Only whole process recording and Jest, mocha, vitest per-test recordings. +- Remote recording not supported yet. diff --git a/src/Recording.ts b/src/Recording.ts index b07ef095..c1ac8113 100644 --- a/src/Recording.ts +++ b/src/Recording.ts @@ -14,7 +14,7 @@ import compactObject from "./util/compactObject"; export default class Recording { constructor(type: AppMap.RecorderType, recorder: string, ...names: string[]) { - const dirs = [recorder, ...names]; + const dirs = [recorder, ...names].map(quotePathSegment); const name = dirs.pop()!; // it must have at least one element this.path = join(appMapDir, ...dirs, makeAppMapFilename(name)); this.stream = new AppMapStream(this.path); @@ -211,3 +211,7 @@ function makeAppMapFilename(name: string): string { // TODO make sure it isn't too long return name + ".appmap.json"; } + +function quotePathSegment(value: string): string { + return value.replaceAll(/[/\\]/g, "-"); +} diff --git a/src/hooks/http.ts b/src/hooks/http.ts index 94dfaead..161aaacc 100644 --- a/src/hooks/http.ts +++ b/src/hooks/http.ts @@ -1,12 +1,14 @@ import assert from "node:assert"; -import { ClientRequest } from "node:http"; import type http from "node:http"; +import { ClientRequest } from "node:http"; import type https from "node:https"; import { URL } from "node:url"; import type AppMap from "../AppMap"; +import Recording from "../Recording"; +import { info } from "../message"; import { parameter } from "../parameter"; -import { recording } from "../recorder"; +import { recording, start } from "../recorder"; import { getTime } from "../util/getTime"; type HTTP = typeof http | typeof https; @@ -111,6 +113,7 @@ function handleRequest(request: http.IncomingMessage, response: http.ServerRespo if (requests.has(request)) return; if (!(request.method && request.url)) return; const url = new URL(request.url, "http://example"); + const timestamp = startRequestRecording(url.pathname); const requestEvent = recording.httpRequest( request.method, url.pathname, @@ -121,8 +124,7 @@ function handleRequest(request: http.IncomingMessage, response: http.ServerRespo const startTime = getTime(); requests.add(request); response.once("finish", () => { - if (fixupEvent(request, requestEvent)) recording.fixup(requestEvent); - handleResponse(requestEvent, startTime, response); + handleResponse(request, requestEvent, startTime, timestamp, response); }); } @@ -184,16 +186,31 @@ function normalizeHeaders( } function handleResponse( + request: http.IncomingMessage, requestEvent: AppMap.HttpServerRequestEvent, startTime: number, + timestamp: string, response: http.ServerResponse, ): void { + if (fixupEvent(request, requestEvent)) recording.fixup(requestEvent); recording.httpResponse( requestEvent.id, getTime() - startTime, response.statusCode, normalizeHeaders(response.getHeaders()), ); + const { request_method, path_info } = requestEvent.http_server_request; + recording.metadata.name = `${request_method} ${path_info} (${response.statusCode}) — ${timestamp}`; + recording.finish(); + info("Wrote %s", recording.path); + start(); // just so there's always a recording running +} + +function startRequestRecording(pathname: string): string { + recording.abandon(); + const timestamp = new Date().toISOString(); + start(new Recording("requests", "requests", [timestamp, pathname].join(" "))); + return timestamp; } function capitalize(str: string): string { diff --git a/src/recorder.ts b/src/recorder.ts index e51c6667..51fe94e5 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -9,7 +9,7 @@ import { FunctionInfo } from "./registry"; import commonPathPrefix from "./util/commonPathPrefix"; import { getTime } from "./util/getTime"; -export let recording: Recording = new Recording("process", "process", new Date().toISOString()); +export let recording: Recording; export function record( this: This, @@ -64,11 +64,15 @@ function isNullPrototype(obj: unknown) { return obj != null && Object.getPrototypeOf(obj) === null; } -export function start(newRecording: Recording) { - assert(!recording.running); +export function start( + newRecording: Recording = new Recording("process", "process", new Date().toISOString()), +) { + assert(!recording?.running); recording = newRecording; } +start(); + process.on("exit", () => { recording.finish(); if (writtenAppMaps.length === 1) info("Wrote %s", writtenAppMaps[0]); diff --git a/test/__snapshots__/express.test.ts.snap b/test/__snapshots__/express.test.ts.snap index c649eae0..17cca0d0 100644 --- a/test/__snapshots__/express.test.ts.snap +++ b/test/__snapshots__/express.test.ts.snap @@ -2,273 +2,288 @@ exports[`mapping Express.js requests 1`] = ` { - "classMap": [ - { - "children": [ - { - "children": [ - { - "location": "./index.js:21", - "name": "helloWorld", - "static": true, - "type": "function", - }, - { - "location": "./index.js:25", - "name": "api", - "static": true, - "type": "function", - }, - { - "location": "./index.js:29", - "name": "postApi", - "static": true, - "type": "function", - }, - ], - "name": "index", - "type": "class", - }, - ], - "name": "index", - "type": "package", - }, - ], - "eventUpdates": { - "11": { - "event": "call", - "http_server_request": { - "headers": { - "Content-Type": "application/json", - "Host": "localhost:27627", - "Transfer-Encoding": "chunked", - }, - "normalized_path_info": "/api/:ident", - "path_info": "/api/bar", - "protocol": "HTTP/1.1", - "request_method": "POST", - }, - "id": 11, - "message": [ - { - "class": "String", - "name": "ident", - "value": "'bar'", - }, - { - "class": "String", - "name": "key", - "value": "'value'", - }, - { - "class": "Object", - "name": "obj", - "object_id": 6, - "properties": [ - { - "class": "Number", - "name": "foo", - }, - { - "class": "Array", - "items": { - "class": "Number", - }, - "name": "arr", - }, - ], - "value": "{ foo: 42, arr: [ 44 ] }", - }, - { - "class": "Array", - "items": { - "class": "Object", - "properties": [ + "./tmp/appmap/requests/ -.appmap.json": { + "classMap": [ + { + "children": [ + { + "children": [ { - "class": "Number", - "name": "foo", + "location": "./index.js:21", + "name": "helloWorld", + "static": true, + "type": "function", }, ], + "name": "index", + "type": "class", }, - "name": "arr", - "object_id": 7, - "size": 2, - "value": "[ { foo: 43 }, { foo: 44 } ]", - }, - { - "class": "Array", - "name": "heterogenous", - "object_id": 8, - "size": 2, - "value": "[ 42, 'str' ]", - }, - ], - "thread_id": 0, - }, - "7": { - "event": "call", - "http_server_request": { - "headers": { - "Host": "localhost:27627", - }, - "normalized_path_info": "/api/:ident", - "path_info": "/api/foo", - "protocol": "HTTP/1.1", - "request_method": "GET", + ], + "name": "index", + "type": "package", }, - "id": 7, - "message": [ - { - "class": "String", - "name": "ident", - "value": "'foo'", - }, - { - "class": "String", - "name": "param1", - "value": "'3'", + ], + "events": [ + { + "event": "call", + "http_server_request": { + "headers": { + "Host": "localhost:27627", + }, + "path_info": "/", + "protocol": "HTTP/1.1", + "request_method": "GET", }, - { - "class": "String", - "name": "param2", - "value": "'4'", + "id": 1, + "thread_id": 0, + }, + { + "defined_class": "", + "event": "call", + "id": 2, + "lineno": 21, + "method_id": "helloWorld", + "parameters": [ + { + "class": "IncomingMessage", + "name": "req", + "object_id": 1, + "value": "[IncomingMessage: GET /]", + }, + { + "class": "ServerResponse", + "name": "res", + "object_id": 2, + "value": "[ServerResponse: 200]", + }, + { + "class": "Function", + "value": "[Function: next]", + }, + ], + "path": "./index.js", + "static": true, + "thread_id": 0, + }, + { + "elapsed": 31.337, + "event": "return", + "id": 3, + "parent_id": 2, + "thread_id": 0, + }, + { + "elapsed": 31.337, + "event": "return", + "http_server_response": { + "headers": { + "Content-Length": "12", + "Content-Type": "text/html; charset=utf-8", + "Etag": "W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"", + "X-Powered-By": "Express", + }, + "status_code": 200, }, - ], - "thread_id": 0, + "id": 4, + "parent_id": 1, + "thread_id": 0, + }, + ], + "metadata": { + "app": "express-appmap-node-test", + "client": { + "name": "appmap-node", + "url": "https://github.com/getappmap/appmap-node", + "version": "test node-appmap version", + }, + "language": { + "engine": "Node.js", + "name": "javascript", + "version": "test node version", + }, + "name": "GET / (200) — ", + "recorder": { + "name": "requests", + "type": "requests", + }, }, + "version": "1.12", }, - "events": [ - { - "event": "call", - "http_server_request": { - "headers": { - "Host": "localhost:27627", + "./tmp/appmap/requests/ -nonexistent.appmap.json": { + "classMap": [], + "events": [ + { + "event": "call", + "http_server_request": { + "headers": { + "Host": "localhost:27627", + }, + "path_info": "/nonexistent", + "protocol": "HTTP/1.1", + "request_method": "GET", }, - "path_info": "/", - "protocol": "HTTP/1.1", - "request_method": "GET", + "id": 1, + "thread_id": 0, }, - "id": 1, - "thread_id": 0, - }, - { - "defined_class": "", - "event": "call", - "id": 2, - "lineno": 21, - "method_id": "helloWorld", - "parameters": [ - { - "class": "IncomingMessage", - "name": "req", - "object_id": 1, - "value": "[IncomingMessage: GET /]", - }, - { - "class": "ServerResponse", - "name": "res", - "object_id": 2, - "value": "[ServerResponse: 200]", - }, - { - "class": "Function", - "value": "[Function: next]", - }, - ], - "path": "./index.js", - "static": true, - "thread_id": 0, - }, - { - "elapsed": 31.337, - "event": "return", - "id": 3, - "parent_id": 2, - "thread_id": 0, - }, - { - "elapsed": 31.337, - "event": "return", - "http_server_response": { - "headers": { - "Content-Length": "12", - "Content-Type": "text/html; charset=utf-8", - "Etag": "W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"", - "X-Powered-By": "Express", + { + "elapsed": 31.337, + "event": "return", + "http_server_response": { + "headers": { + "Content-Length": "150", + "Content-Security-Policy": "default-src 'none'", + "Content-Type": "text/html; charset=utf-8", + "X-Content-Type-Options": "nosniff", + "X-Powered-By": "Express", + }, + "status_code": 404, }, - "status_code": 200, + "id": 2, + "parent_id": 1, + "thread_id": 0, }, - "id": 4, - "parent_id": 1, - "thread_id": 0, - }, - { - "event": "call", - "http_server_request": { - "headers": { - "Host": "localhost:27627", - }, - "path_info": "/nonexistent", - "protocol": "HTTP/1.1", - "request_method": "GET", + ], + "metadata": { + "app": "express-appmap-node-test", + "client": { + "name": "appmap-node", + "url": "https://github.com/getappmap/appmap-node", + "version": "test node-appmap version", + }, + "language": { + "engine": "Node.js", + "name": "javascript", + "version": "test node version", + }, + "name": "GET /nonexistent (404) — ", + "recorder": { + "name": "requests", + "type": "requests", }, - "id": 5, - "thread_id": 0, }, - { - "elapsed": 31.337, - "event": "return", - "http_server_response": { - "headers": { - "Content-Length": "150", - "Content-Security-Policy": "default-src 'none'", - "Content-Type": "text/html; charset=utf-8", - "X-Content-Type-Options": "nosniff", - "X-Powered-By": "Express", + "version": "1.12", + }, + "./tmp/appmap/requests/ -api-foo.appmap.json": { + "classMap": [ + { + "children": [ + { + "children": [ + { + "location": "./index.js:25", + "name": "api", + "static": true, + "type": "function", + }, + ], + "name": "index", + "type": "class", + }, + ], + "name": "index", + "type": "package", + }, + ], + "eventUpdates": { + "1": { + "event": "call", + "http_server_request": { + "headers": { + "Host": "localhost:27627", + }, + "normalized_path_info": "/api/:ident", + "path_info": "/api/foo", + "protocol": "HTTP/1.1", + "request_method": "GET", }, - "status_code": 404, + "id": 1, + "message": [ + { + "class": "String", + "name": "ident", + "value": "'foo'", + }, + { + "class": "String", + "name": "param1", + "value": "'3'", + }, + { + "class": "String", + "name": "param2", + "value": "'4'", + }, + ], + "thread_id": 0, }, - "id": 6, - "parent_id": 5, - "thread_id": 0, }, - { - "event": "call", - "http_server_request": { - "headers": { - "Host": "localhost:27627", + "events": [ + { + "event": "call", + "http_server_request": { + "headers": { + "Host": "localhost:27627", + }, + "path_info": "/api/foo", + "protocol": "HTTP/1.1", + "request_method": "GET", }, - "path_info": "/api/foo", - "protocol": "HTTP/1.1", - "request_method": "GET", + "id": 1, + "message": [ + { + "class": "String", + "name": "param1", + "value": "'3'", + }, + { + "class": "String", + "name": "param2", + "value": "'4'", + }, + ], + "thread_id": 0, }, - "id": 7, - "message": [ - { - "class": "String", - "name": "param1", - "value": "'3'", - }, - { - "class": "String", - "name": "param2", - "value": "'4'", - }, - ], - "thread_id": 0, - }, - { - "defined_class": "", - "event": "call", - "id": 8, - "lineno": 25, - "method_id": "api", - "parameters": [ - { + { + "defined_class": "", + "event": "call", + "id": 2, + "lineno": 25, + "method_id": "api", + "parameters": [ + { + "class": "Object", + "name": "query", + "object_id": 3, + "properties": [ + { + "class": "String", + "name": "param1", + }, + { + "class": "String", + "name": "param2", + }, + ], + "value": "{ param1: '3', param2: '4' }", + }, + ], + "path": "./index.js", + "static": true, + "thread_id": 0, + }, + { + "elapsed": 31.337, + "event": "return", + "id": 3, + "parent_id": 2, + "return_value": { "class": "Object", - "name": "query", - "object_id": 3, + "object_id": 4, "properties": [ + { + "class": "String", + "name": "api", + }, { "class": "String", "name": "param1", @@ -278,80 +293,226 @@ exports[`mapping Express.js requests 1`] = ` "name": "param2", }, ], - "value": "{ param1: '3', param2: '4' }", + "value": "{ api: 'result', param1: '3', param2: '4' }", + }, + "thread_id": 0, + }, + { + "elapsed": 31.337, + "event": "return", + "http_server_response": { + "headers": { + "Content-Length": "42", + "Content-Type": "application/json; charset=utf-8", + "Etag": "W/"2a-YHNH/nKM8UUG4pNEEtm91sktuKc"", + "X-Powered-By": "Express", + }, + "status_code": 200, }, - ], - "path": "./index.js", - "static": true, - "thread_id": 0, + "id": 4, + "parent_id": 1, + "thread_id": 0, + }, + ], + "metadata": { + "app": "express-appmap-node-test", + "client": { + "name": "appmap-node", + "url": "https://github.com/getappmap/appmap-node", + "version": "test node-appmap version", + }, + "language": { + "engine": "Node.js", + "name": "javascript", + "version": "test node version", + }, + "name": "GET /api/foo (200) — ", + "recorder": { + "name": "requests", + "type": "requests", + }, }, - { - "elapsed": 31.337, - "event": "return", - "id": 9, - "parent_id": 8, - "return_value": { - "class": "Object", - "object_id": 4, - "properties": [ + "version": "1.12", + }, + "./tmp/appmap/requests/ -api-bar.appmap.json": { + "classMap": [ + { + "children": [ { - "class": "String", - "name": "api", + "children": [ + { + "location": "./index.js:29", + "name": "postApi", + "static": true, + "type": "function", + }, + ], + "name": "index", + "type": "class", + }, + ], + "name": "index", + "type": "package", + }, + ], + "eventUpdates": { + "1": { + "event": "call", + "http_server_request": { + "headers": { + "Content-Type": "application/json", + "Host": "localhost:27627", + "Transfer-Encoding": "chunked", }, + "normalized_path_info": "/api/:ident", + "path_info": "/api/bar", + "protocol": "HTTP/1.1", + "request_method": "POST", + }, + "id": 1, + "message": [ { "class": "String", - "name": "param1", + "name": "ident", + "value": "'bar'", }, { "class": "String", - "name": "param2", + "name": "key", + "value": "'value'", + }, + { + "class": "Object", + "name": "obj", + "object_id": 6, + "properties": [ + { + "class": "Number", + "name": "foo", + }, + { + "class": "Array", + "items": { + "class": "Number", + }, + "name": "arr", + }, + ], + "value": "{ foo: 42, arr: [ 44 ] }", + }, + { + "class": "Array", + "items": { + "class": "Object", + "properties": [ + { + "class": "Number", + "name": "foo", + }, + ], + }, + "name": "arr", + "object_id": 7, + "size": 2, + "value": "[ { foo: 43 }, { foo: 44 } ]", + }, + { + "class": "Array", + "name": "heterogenous", + "object_id": 8, + "size": 2, + "value": "[ 42, 'str' ]", }, ], - "value": "{ api: 'result', param1: '3', param2: '4' }", + "thread_id": 0, }, - "thread_id": 0, }, - { - "elapsed": 31.337, - "event": "return", - "http_server_response": { - "headers": { - "Content-Length": "42", - "Content-Type": "application/json; charset=utf-8", - "Etag": "W/"2a-YHNH/nKM8UUG4pNEEtm91sktuKc"", - "X-Powered-By": "Express", + "events": [ + { + "event": "call", + "http_server_request": { + "headers": { + "Content-Type": "application/json", + "Host": "localhost:27627", + "Transfer-Encoding": "chunked", + }, + "path_info": "/api/bar", + "protocol": "HTTP/1.1", + "request_method": "POST", }, - "status_code": 200, + "id": 1, + "thread_id": 0, }, - "id": 10, - "parent_id": 7, - "thread_id": 0, - }, - { - "event": "call", - "http_server_request": { - "headers": { - "Content-Type": "application/json", - "Host": "localhost:27627", - "Transfer-Encoding": "chunked", - }, - "path_info": "/api/bar", - "protocol": "HTTP/1.1", - "request_method": "POST", + { + "defined_class": "", + "event": "call", + "id": 2, + "lineno": 29, + "method_id": "postApi", + "parameters": [ + { + "class": "Object", + "name": "body", + "object_id": 5, + "properties": [ + { + "class": "String", + "name": "key", + }, + { + "class": "Object", + "name": "obj", + "properties": [ + { + "class": "Number", + "name": "foo", + }, + { + "class": "Array", + "items": { + "class": "Number", + }, + "name": "arr", + }, + ], + }, + { + "class": "Array", + "items": { + "class": "Object", + "properties": [ + { + "class": "Number", + "name": "foo", + }, + ], + }, + "name": "arr", + }, + { + "class": "Array", + "name": "heterogenous", + }, + ], + "value": "{ + key: 'value', + obj: { foo: 42, arr: [Array] }, + arr: [ [Object], [Object] ], + heterogenous: [ 42, 'str' ] +}", + }, + ], + "path": "./index.js", + "static": true, + "thread_id": 0, }, - "id": 11, - "thread_id": 0, - }, - { - "defined_class": "", - "event": "call", - "id": 12, - "lineno": 29, - "method_id": "postApi", - "parameters": [ - { + { + "elapsed": 31.337, + "event": "return", + "id": 3, + "parent_id": 2, + "return_value": { "class": "Object", - "name": "body", "object_id": 5, "properties": [ { @@ -400,103 +561,44 @@ exports[`mapping Express.js requests 1`] = ` heterogenous: [ 42, 'str' ] }", }, - ], - "path": "./index.js", - "static": true, - "thread_id": 0, - }, - { - "elapsed": 31.337, - "event": "return", - "id": 13, - "parent_id": 12, - "return_value": { - "class": "Object", - "object_id": 5, - "properties": [ - { - "class": "String", - "name": "key", - }, - { - "class": "Object", - "name": "obj", - "properties": [ - { - "class": "Number", - "name": "foo", - }, - { - "class": "Array", - "items": { - "class": "Number", - }, - "name": "arr", - }, - ], - }, - { - "class": "Array", - "items": { - "class": "Object", - "properties": [ - { - "class": "Number", - "name": "foo", - }, - ], - }, - "name": "arr", - }, - { - "class": "Array", - "name": "heterogenous", - }, - ], - "value": "{ - key: 'value', - obj: { foo: 42, arr: [Array] }, - arr: [ [Object], [Object] ], - heterogenous: [ 42, 'str' ] -}", + "thread_id": 0, }, - "thread_id": 0, - }, - { - "elapsed": 31.337, - "event": "return", - "http_server_response": { - "headers": { - "Content-Length": "99", - "Content-Type": "application/json; charset=utf-8", - "Etag": "W/"63-avsgRi3I+MK54okDiWb1V4/K3j8"", - "X-Powered-By": "Express", + { + "elapsed": 31.337, + "event": "return", + "http_server_response": { + "headers": { + "Content-Length": "99", + "Content-Type": "application/json; charset=utf-8", + "Etag": "W/"63-avsgRi3I+MK54okDiWb1V4/K3j8"", + "X-Powered-By": "Express", + }, + "status_code": 200, }, - "status_code": 200, + "id": 4, + "parent_id": 1, + "thread_id": 0, + }, + ], + "metadata": { + "app": "express-appmap-node-test", + "client": { + "name": "appmap-node", + "url": "https://github.com/getappmap/appmap-node", + "version": "test node-appmap version", + }, + "language": { + "engine": "Node.js", + "name": "javascript", + "version": "test node version", + }, + "name": "POST /api/bar (200) — ", + "recorder": { + "name": "requests", + "type": "requests", }, - "id": 14, - "parent_id": 11, - "thread_id": 0, - }, - ], - "metadata": { - "app": "express-appmap-node-test", - "client": { - "name": "appmap-node", - "url": "https://github.com/getappmap/appmap-node", - "version": "test node-appmap version", - }, - "language": { - "engine": "Node.js", - "name": "javascript", - "version": "test node version", - }, - "name": "test process recording", - "recorder": { - "name": "process", - "type": "process", }, + "version": "1.12", }, - "version": "1.12", } `; diff --git a/test/express.test.ts b/test/express.test.ts index a33676ee..8f0842c4 100644 --- a/test/express.test.ts +++ b/test/express.test.ts @@ -1,8 +1,8 @@ import { request } from "node:http"; -import { integrationTest, readAppmap, spawnAppmapNode } from "./helpers"; +import { integrationTest, readAppmaps, spawnAppmapNode } from "./helpers"; integrationTest("mapping Express.js requests", async () => { - expect.assertions(2); + expect.assertions(1); const server = spawnAppmapNode("index.js"); await new Promise((r) => server.stdout.on("data", (chunk: Buffer) => { @@ -31,5 +31,5 @@ integrationTest("mapping Express.js requests", async () => { }); server.kill("SIGINT"); await new Promise((r) => server.on("close", () => r())); - expect(readAppmap()).toMatchSnapshot(); + expect(readAppmaps()).toMatchSnapshot(); }); diff --git a/test/helpers.ts b/test/helpers.ts index 416b16c4..63823dd8 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -108,9 +108,18 @@ function fixEvent(event: unknown) { if ("elapsed" in event && typeof event.elapsed === "number") event.elapsed = 31.337; } +const timestamps: Record = {}; +let timestampId = 0; + +function fixTimeStamps(str: string): string { + return str.replaceAll( + /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/g, + (ts) => (timestamps[ts] ||= ``), + ); +} + function fixPath(path: string): string { - if (path.startsWith(target)) return path.replace(target, "."); - else return path; + return fixTimeStamps(path.replace(target, ".")); } function fixClassMap(classMap: unknown[]) { @@ -126,6 +135,7 @@ function fixMetadata(metadata: AppMap.Metadata) { if (metadata.recorder.type === "process") metadata.name = "test process recording"; if (metadata.language) metadata.language.version = "test node version"; if (metadata.client.version) metadata.client.version = "test node-appmap version"; + if (metadata.name) metadata.name = fixTimeStamps(metadata.name); } function ensureBuilt() { From 591a78bc056eb4e6e39c3890a3c982c8146e6476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Rzepecki?= Date: Mon, 27 Nov 2023 12:40:55 +0100 Subject: [PATCH 2/2] test: Make sure the order in sqlite test is stable --- test/__snapshots__/sqlite.test.ts.snap | 22 +++++++++++----------- test/sqlite/index.js | 15 +++++---------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/test/__snapshots__/sqlite.test.ts.snap b/test/__snapshots__/sqlite.test.ts.snap index 9cfabacc..bbb94c48 100644 --- a/test/__snapshots__/sqlite.test.ts.snap +++ b/test/__snapshots__/sqlite.test.ts.snap @@ -8,7 +8,7 @@ exports[`mapping Sqlite tests 1`] = ` { "children": [ { - "location": "./index.js:12", + "location": "./index.js:13", "name": "main", "static": true, "type": "function", @@ -41,7 +41,7 @@ exports[`mapping Sqlite tests 1`] = ` "defined_class": "", "event": "call", "id": 1, - "lineno": 12, + "lineno": 13, "method_id": "main", "parameters": [], "path": "./index.js", @@ -294,26 +294,26 @@ exports[`mapping Sqlite tests 1`] = ` "thread_id": 0, }, { - "event": "call", + "elapsed": 31.337, + "event": "return", "id": 32, - "sql_query": { - "database_type": "sqlite", - "sql": "SELECT 'Stetement.run without a completion callback'", - }, + "parent_id": 31, "thread_id": 0, }, { - "elapsed": 31.337, - "event": "return", + "event": "call", "id": 33, - "parent_id": 31, + "sql_query": { + "database_type": "sqlite", + "sql": "SELECT 'Statement.run without a completion callback'", + }, "thread_id": 0, }, { "elapsed": 31.337, "event": "return", "id": 34, - "parent_id": 32, + "parent_id": 33, "thread_id": 0, }, ], diff --git a/test/sqlite/index.js b/test/sqlite/index.js index cb513680..c8696482 100644 --- a/test/sqlite/index.js +++ b/test/sqlite/index.js @@ -1,5 +1,6 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires +/* eslint-disable @typescript-eslint/no-var-requires */ const sqlite = require("sqlite3"); +const { setTimeout } = require("node:timers/promises"); const promisify = (method, thisArg, ...args) => new Promise((resolve, reject) => { @@ -67,15 +68,9 @@ async function main() { // We cannot test commands being called without a completion callback with promisify // because promisify already provides the completion callback to resolve the promise. // We test this case with no completion callback here wtihout promisifying them. - // We are serializing them just to get the same [id, parent_id] pairs - // for the events in the appmap snapshot in every test run. - db.serialize( - function () { - db.run("SELECT 'Database.run without a completion callback'"); - db.prepare("SELECT 'Stetement.run without a completion callback'").run().finalize(); - }, - () => db.close(), - ); + db.run("SELECT 'Database.run without a completion callback'"); + await setTimeout(10); // to serialize the appmap + db.prepare("SELECT 'Statement.run without a completion callback'").run().finalize(); } main();