@jsenv/snapshot 2.6.5 → 2.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/package.json +7 -5
  2. package/src/filesystem_well_known_values.js +18 -26
  3. package/src/get_caller_location.js +22 -0
  4. package/src/main.js +4 -1
  5. package/src/replace_fluctuating_values.js +1 -1
  6. package/src/side_effects/capture_side_effects.js +6 -0
  7. package/src/side_effects/create_capture_side_effects.js +305 -0
  8. package/src/side_effects/filesystem/filesystem_side_effects.js +302 -0
  9. package/src/side_effects/filesystem/group_file_side_effects_per_directory.js +30 -0
  10. package/src/{function_side_effects → side_effects/filesystem}/spy_filesystem_calls.js +48 -25
  11. package/src/side_effects/log/group_log_side_effects.js +29 -0
  12. package/src/side_effects/log/log_side_effects.js +156 -0
  13. package/src/side_effects/render_logs_gif.js +18 -0
  14. package/src/side_effects/render_side_effects.js +435 -0
  15. package/src/side_effects/snapshot_side_effects.js +43 -0
  16. package/src/side_effects/snapshot_tests.js +115 -0
  17. package/src/side_effects/utils/group_side_effects.js +89 -0
  18. package/src/function_side_effects/function_side_effects_collector.js +0 -160
  19. package/src/function_side_effects/function_side_effects_renderer.js +0 -29
  20. package/src/function_side_effects/function_side_effects_snapshot.js +0 -302
  21. package/src/function_side_effects/group_file_side_effects_per_directory.js +0 -114
  22. package/src/function_side_effects/spy_console_calls.js +0 -89
  23. /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.js +0 -0
  24. /package/src/{function_side_effects → side_effects/filesystem}/common_ancestor_path.test.mjs +0 -0
  25. /package/src/{function_side_effects → side_effects}/hook_into_method.js +0 -0
@@ -1,160 +0,0 @@
1
- import { createException } from "@jsenv/exception";
2
- import { replaceFluctuatingValues } from "../replace_fluctuating_values.js";
3
- import { wrapIntoMarkdownBlock } from "./function_side_effects_renderer.js";
4
-
5
- const RETURN_PROMISE = {};
6
-
7
- let functionExecutingCount = 0;
8
-
9
- export const collectFunctionSideEffects = (
10
- fn,
11
- sideEffectDetectors,
12
- { rootDirectoryUrl },
13
- ) => {
14
- const sideEffects = [];
15
- const addSideEffect = (sideEffect) => {
16
- sideEffects.push(sideEffect);
17
- return sideEffect;
18
- };
19
- const finallyCallbackSet = new Set();
20
- for (const sideEffectDetector of sideEffectDetectors) {
21
- const uninstall = sideEffectDetector.install(addSideEffect);
22
- finallyCallbackSet.add(() => {
23
- uninstall();
24
- });
25
- }
26
- if (functionExecutingCount) {
27
- // The reason for this warning:
28
- // 1. fs side effect detectors is not yet fully compatible with that because
29
- // callback.oncomplete redefinition might be wrong for open, mkdir etc
30
- // (at least this is to be tested)
31
- // 2. It's usually a sign code forgets to put await in front of
32
- // collectFunctionSideEffects or snapshotFunctionSideEffects
33
- // 3. collectFunctionSideEffects is meant to collect a function side effect
34
- // during unit test. So in unit test the function being tested should be analyized
35
- // and should not in turn analyze an other one
36
- console.warn(
37
- `collectFunctionSideEffects called while other function(s) side effects are collected`,
38
- );
39
- }
40
-
41
- const onCatch = (valueThrow) => {
42
- sideEffects.push({
43
- type: "throw",
44
- value: valueThrow,
45
- label: "throw",
46
- text: wrapIntoMarkdownBlock(
47
- renderValueThrownOrRejected(
48
- createException(valueThrow, { rootDirectoryUrl }),
49
- { rootDirectoryUrl },
50
- ),
51
- ),
52
- });
53
- };
54
- const onReturn = (valueReturned) => {
55
- if (valueReturned === RETURN_PROMISE) {
56
- sideEffects.push({
57
- type: "return",
58
- value: valueReturned,
59
- label: "return promise",
60
- text: null,
61
- });
62
- } else {
63
- sideEffects.push({
64
- type: "return",
65
- value: valueReturned,
66
- label: "return",
67
- text: wrapIntoMarkdownBlock(
68
- renderReturnValueOrResolveValue(valueReturned, {
69
- rootDirectoryUrl,
70
- }),
71
- "js",
72
- ),
73
- });
74
- }
75
- };
76
- const onResolve = (value) => {
77
- sideEffects.push({
78
- type: "resolve",
79
- value,
80
- label: "resolve",
81
- text: wrapIntoMarkdownBlock(
82
- renderReturnValueOrResolveValue(value, { rootDirectoryUrl }),
83
- "js",
84
- ),
85
- });
86
- };
87
- const onReject = (reason) => {
88
- sideEffects.push({
89
- type: "reject",
90
- value: reason,
91
- label: "reject",
92
- text: wrapIntoMarkdownBlock(
93
- renderValueThrownOrRejected(
94
- createException(reason, { rootDirectoryUrl }),
95
- { rootDirectoryUrl },
96
- ),
97
- ),
98
- });
99
- };
100
- const onFinally = () => {
101
- delete process.env.SNAPSHOTING_FUNCTION_SIDE_EFFECTS;
102
- functionExecutingCount--;
103
- for (const finallyCallback of finallyCallbackSet) {
104
- finallyCallback();
105
- }
106
- finallyCallbackSet.clear();
107
- };
108
-
109
- process.env.SNAPSHOTING_FUNCTION_SIDE_EFFECTS = "1";
110
- functionExecutingCount++;
111
- let returnedPromise = false;
112
- try {
113
- const valueReturned = fn();
114
- if (valueReturned && typeof valueReturned.then === "function") {
115
- onReturn(RETURN_PROMISE);
116
- returnedPromise = valueReturned.then(
117
- (value) => {
118
- onResolve(value);
119
- onFinally();
120
- return sideEffects;
121
- },
122
- (e) => {
123
- onReject(e);
124
- onFinally();
125
- return sideEffects;
126
- },
127
- );
128
- return returnedPromise;
129
- }
130
- onReturn(valueReturned);
131
- return sideEffects;
132
- } catch (e) {
133
- onCatch(e);
134
- return sideEffects;
135
- } finally {
136
- if (!returnedPromise) {
137
- onFinally();
138
- }
139
- }
140
- };
141
-
142
- const renderReturnValueOrResolveValue = (value, { rootDirectoryUrl }) => {
143
- if (value === undefined) {
144
- return "undefined";
145
- }
146
- return replaceFluctuatingValues(JSON.stringify(value, null, " "), {
147
- stringType: "json",
148
- rootDirectoryUrl,
149
- });
150
- };
151
-
152
- const renderValueThrownOrRejected = (value, { rootDirectoryUrl }) => {
153
- return replaceFluctuatingValues(
154
- value ? value.stack || value.message || value : String(value),
155
- {
156
- stringType: "error",
157
- rootDirectoryUrl,
158
- },
159
- );
160
- };
@@ -1,29 +0,0 @@
1
- export const renderSideEffects = (sideEffects) => {
2
- let string = "";
3
- let index = 0;
4
- for (const sideEffect of sideEffects) {
5
- if (sideEffect.skippable) {
6
- continue;
7
- }
8
- if (string) {
9
- string += "\n\n";
10
- }
11
- let label = `${index + 1}. ${sideEffect.label}`;
12
- let text = sideEffect.text;
13
- string += label;
14
- if (text) {
15
- string += "\n";
16
- string += text;
17
- }
18
- index++;
19
- }
20
- return string;
21
- };
22
-
23
- export const wrapIntoMarkdownBlock = (value, blockName = "") => {
24
- const start = "```";
25
- const end = "```";
26
- return `${start}${blockName}
27
- ${value}
28
- ${end}`;
29
- };
@@ -1,302 +0,0 @@
1
- import { readDirectorySync, writeFileSync } from "@jsenv/filesystem";
2
- import {
3
- ensurePathnameTrailingSlash,
4
- urlIsInsideOf,
5
- urlToExtension,
6
- urlToRelativeUrl,
7
- } from "@jsenv/urls";
8
- import {
9
- takeDirectorySnapshot,
10
- takeFileSnapshot,
11
- } from "../filesystem_snapshot.js";
12
- import {
13
- createReplaceFilesystemWellKnownValues,
14
- createWellKnown,
15
- } from "../filesystem_well_known_values.js";
16
- import { replaceFluctuatingValues } from "../replace_fluctuating_values.js";
17
- import { collectFunctionSideEffects } from "./function_side_effects_collector.js";
18
- import {
19
- renderSideEffects,
20
- wrapIntoMarkdownBlock,
21
- } from "./function_side_effects_renderer.js";
22
- import { groupFileSideEffectsPerDirectory } from "./group_file_side_effects_per_directory.js";
23
- import { spyConsoleCalls } from "./spy_console_calls.js";
24
- import { spyFilesystemCalls } from "./spy_filesystem_calls.js";
25
-
26
- const filesystemEffectsDefault = {
27
- outDirectory: null,
28
- preserve: false,
29
- };
30
- const consoleEffectsDefault = {
31
- prevent: true,
32
- };
33
-
34
- export const snapshotFunctionSideEffects = (
35
- fn,
36
- sideEffectFileUrl,
37
- { consoleEffects = true, filesystemEffects = true, rootDirectoryUrl } = {},
38
- ) => {
39
- if (consoleEffects === true) {
40
- consoleEffects = {};
41
- }
42
- if (filesystemEffects === true) {
43
- filesystemEffects = {};
44
- }
45
- const replaceFilesystemWellKnownValues =
46
- createReplaceFilesystemWellKnownValues({
47
- rootDirectoryUrl,
48
- });
49
- const sideEffectFileSnapshot = takeFileSnapshot(sideEffectFileUrl);
50
- const callbackSet = new Set();
51
- const sideEffectDetectors = [
52
- ...(consoleEffects
53
- ? [
54
- {
55
- name: "console",
56
- install: (addSideEffect) => {
57
- consoleEffects = { ...consoleEffectsDefault, ...consoleEffects };
58
- const { prevent } = consoleEffects;
59
- const onConsole = (methodName, message) => {
60
- addSideEffect({
61
- type: `console:${methodName}`,
62
- value: message,
63
- label: `console.${methodName}`,
64
- text: wrapIntoMarkdownBlock(
65
- replaceFluctuatingValues(message, {
66
- stringType: "console",
67
- replaceFilesystemWellKnownValues,
68
- }),
69
- "console",
70
- ),
71
- });
72
- };
73
- const consoleSpy = spyConsoleCalls(
74
- {
75
- error: (message) => {
76
- onConsole("error", message);
77
- },
78
- warn: (message) => {
79
- onConsole("warn", message);
80
- },
81
- info: (message) => {
82
- onConsole("info", message);
83
- },
84
- log: (message) => {
85
- onConsole("log", message);
86
- },
87
- stdout: (message) => {
88
- addSideEffect({
89
- type: `process:stdout`,
90
- value: message,
91
- label: `process.stdout`,
92
- text: wrapIntoMarkdownBlock(
93
- replaceFluctuatingValues(message, {
94
- stringType: "console",
95
- replaceFilesystemWellKnownValues,
96
- }),
97
- "console",
98
- ),
99
- });
100
- },
101
- stderr: (message) => {
102
- addSideEffect({
103
- type: `process:stderr`,
104
- value: message,
105
- label: `process.stderr`,
106
- text: wrapIntoMarkdownBlock(
107
- replaceFluctuatingValues(message, {
108
- stringType: "console",
109
- replaceFilesystemWellKnownValues,
110
- }),
111
- "console",
112
- ),
113
- });
114
- },
115
- },
116
- {
117
- preventConsoleSideEffects: prevent,
118
- },
119
- );
120
- return () => {
121
- consoleSpy.restore();
122
- };
123
- },
124
- },
125
- ]
126
- : []),
127
- ...(filesystemEffects
128
- ? [
129
- {
130
- name: "filesystem",
131
- install: (addSideEffect) => {
132
- filesystemEffects = {
133
- ...filesystemEffectsDefault,
134
- ...filesystemEffects,
135
- };
136
- let writeFile;
137
- const { include, preserve, baseDirectory, outDirectory } =
138
- filesystemEffects;
139
- if (baseDirectory) {
140
- replaceFilesystemWellKnownValues.addWellKnownFileUrl(
141
- baseDirectory,
142
- createWellKnown("base"),
143
- { position: "start" },
144
- );
145
- }
146
- const renderLabel = (label) => {
147
- return replaceFluctuatingValues(label, {
148
- replaceFilesystemWellKnownValues,
149
- });
150
- };
151
-
152
- if (outDirectory) {
153
- const fsEffectsOutDirectoryUrl = ensurePathnameTrailingSlash(
154
- new URL(outDirectory, sideEffectFileUrl),
155
- );
156
- const fsEffectsOutDirectorySnapshot = takeDirectorySnapshot(
157
- fsEffectsOutDirectoryUrl,
158
- );
159
- const writeFileCallbackSet = new Set();
160
- const getFilesystemActionInfo = (action, url) => {
161
- let toUrl;
162
- let urlDisplayed = url;
163
- if (baseDirectory) {
164
- urlDisplayed = urlToRelativeUrl(url, baseDirectory, {
165
- preferRelativeNotation: true,
166
- });
167
- if (
168
- url.href === baseDirectory.href ||
169
- urlIsInsideOf(url, baseDirectory)
170
- ) {
171
- const toRelativeUrl = urlToRelativeUrl(
172
- url,
173
- baseDirectory,
174
- );
175
- toUrl = new URL(toRelativeUrl, fsEffectsOutDirectoryUrl);
176
- } else {
177
- const toRelativeUrl =
178
- replaceFilesystemWellKnownValues(url);
179
- toUrl = new URL(toRelativeUrl, fsEffectsOutDirectoryUrl);
180
- }
181
- // otherwise we need to replace the url with well known
182
- } else {
183
- const toRelativeUrl = replaceFilesystemWellKnownValues(url);
184
- toUrl = new URL(toRelativeUrl, fsEffectsOutDirectoryUrl);
185
- }
186
- const toUrlDisplayed = urlToRelativeUrl(
187
- toUrl,
188
- sideEffectFileUrl,
189
- { preferRelativeNotation: true },
190
- );
191
- return {
192
- toUrl,
193
- label: renderLabel(
194
- `${action} "${urlDisplayed}" (see ${toUrlDisplayed})`,
195
- ),
196
- };
197
- };
198
-
199
- callbackSet.add((sideEffects) => {
200
- // gather all file side effect next to each other
201
- // collapse them if they have a shared ancestor
202
- groupFileSideEffectsPerDirectory(sideEffects, {
203
- baseDirectory,
204
- getFilesystemActionInfo,
205
- });
206
- for (const writeFileCallback of writeFileCallbackSet) {
207
- writeFileCallback();
208
- }
209
- writeFileCallbackSet.clear();
210
- fsEffectsOutDirectorySnapshot.compare();
211
- });
212
- writeFile = (url, content) => {
213
- const { toUrl, label } = getFilesystemActionInfo(
214
- "write file",
215
- url,
216
- );
217
- writeFileCallbackSet.add(() => {
218
- writeFileSync(toUrl, content);
219
- });
220
- addSideEffect({
221
- type: "fs:write_file",
222
- value: { url: String(url), content },
223
- label,
224
- text: null,
225
- });
226
- };
227
- } else {
228
- writeFile = (url, content) => {
229
- let urlDisplayed = url;
230
- if (baseDirectory) {
231
- urlDisplayed = urlToRelativeUrl(url, baseDirectory, {
232
- preferRelativeNotation: true,
233
- });
234
- }
235
- addSideEffect({
236
- type: "fs:write_file",
237
- value: { url: String(url), content },
238
- label: renderLabel(`write file "${urlDisplayed}"`),
239
- text: wrapIntoMarkdownBlock(
240
- content,
241
- urlToExtension(url).slice(1),
242
- ),
243
- });
244
- };
245
- }
246
- const filesystemSpy = spyFilesystemCalls(
247
- {
248
- writeFile,
249
- writeDirectory: (url) => {
250
- const writeDirectorySideEffect = addSideEffect({
251
- type: "fs:write_directory",
252
- value: { url: String(url) },
253
- label: renderLabel(`write directory "${url}"`),
254
- text: null,
255
- });
256
- // if directory ends up with something inside we'll not report
257
- // this side effect because:
258
- // - it was likely created to write the file
259
- // - the file creation will be reported and implies directory creation
260
- filesystemSpy.addBeforeUndoCallback(() => {
261
- try {
262
- const dirContent = readDirectorySync(url);
263
- if (dirContent.length) {
264
- writeDirectorySideEffect.skippable = true;
265
- }
266
- } catch (e) {}
267
- });
268
- },
269
- },
270
- {
271
- include,
272
- undoFilesystemSideEffects: !preserve,
273
- },
274
- );
275
- return () => {
276
- filesystemSpy.restore();
277
- };
278
- },
279
- },
280
- ]
281
- : []),
282
- ];
283
- const onSideEffectsCollected = (sideEffects) => {
284
- for (const callback of callbackSet) {
285
- callback(sideEffects);
286
- }
287
- callbackSet.clear();
288
- sideEffectFileSnapshot.update(renderSideEffects(sideEffects), {
289
- mockFluctuatingValues: false,
290
- });
291
- };
292
- const returnValue = collectFunctionSideEffects(fn, sideEffectDetectors, {
293
- rootDirectoryUrl,
294
- });
295
- if (returnValue && typeof returnValue.then === "function") {
296
- return returnValue.then((sideEffects) => {
297
- onSideEffectsCollected(sideEffects);
298
- });
299
- }
300
- onSideEffectsCollected(returnValue);
301
- return undefined;
302
- };
@@ -1,114 +0,0 @@
1
- import { pathToFileURL } from "node:url";
2
- import { findCommonAncestorPath } from "./common_ancestor_path.js";
3
-
4
- export const groupFileSideEffectsPerDirectory = (
5
- sideEffects,
6
- { getFilesystemActionInfo },
7
- ) => {
8
- const groupArray = groupFileTogether(sideEffects);
9
-
10
- const convertToPathname = (writeFileSideEffect) => {
11
- return new URL(writeFileSideEffect.value.url).pathname;
12
- };
13
-
14
- for (const group of groupArray) {
15
- if (group.id !== "file") {
16
- continue;
17
- }
18
- const fileEffectArray = group.values;
19
- if (fileEffectArray.length < 2) {
20
- continue;
21
- }
22
- const commonAncestorPath = findCommonAncestorPath(
23
- fileEffectArray,
24
- convertToPathname,
25
- );
26
- const firstEffect = fileEffectArray[0];
27
- const firstEffectIndex = sideEffects.indexOf(firstEffect);
28
- const commonAncestorUrl = pathToFileURL(commonAncestorPath);
29
- const numberOfFiles = fileEffectArray.length;
30
- const { label } = getFilesystemActionInfo(
31
- `write ${numberOfFiles} files into`,
32
- commonAncestorUrl,
33
- );
34
- for (const fileEffect of fileEffectArray) {
35
- sideEffects.splice(sideEffects.indexOf(fileEffect), 1);
36
- }
37
- sideEffects.splice(firstEffectIndex, 0, {
38
- type: "fs:write_file",
39
- label,
40
- });
41
- }
42
- };
43
-
44
- const groupBy = (array, groupCallback) => {
45
- let i = 0;
46
- const groupArray = [];
47
- let currentGroup = null;
48
- while (i < array.length) {
49
- const value = array[i];
50
- i++;
51
- let ignoreCalled = false;
52
- let ignore = () => {
53
- ignoreCalled = true;
54
- };
55
- const groupId = groupCallback(value, { ignore });
56
- if (ignoreCalled) {
57
- continue;
58
- }
59
- if (currentGroup === null) {
60
- currentGroup = {
61
- id: groupId,
62
- values: [value],
63
- };
64
- groupArray.push(currentGroup);
65
- continue;
66
- }
67
- if (groupId === currentGroup.id) {
68
- currentGroup.values.push(value);
69
- continue;
70
- }
71
- currentGroup = {
72
- id: groupId,
73
- values: [value],
74
- };
75
- groupArray.push(currentGroup);
76
- }
77
- return groupArray;
78
- };
79
-
80
- const groupFileTogether = (sideEffects) =>
81
- groupBy(sideEffects, (sideEffect, { ignore }) => {
82
- if (sideEffect.type === "fs:write_directory") {
83
- ignore();
84
- return null;
85
- }
86
- if (sideEffect.type === "fs:write_file") {
87
- return "file";
88
- }
89
- return "other";
90
- });
91
-
92
- // const groups = groupFileTogether([
93
- // {
94
- // name: "a",
95
- // type: "fs:write_file",
96
- // },
97
- // {
98
- // name: "b",
99
- // type: "fs:write_directory",
100
- // },
101
- // {
102
- // name: "c",
103
- // type: "fs:write_file",
104
- // },
105
- // {
106
- // name: "d",
107
- // type: "other",
108
- // },
109
- // {
110
- // name: "e",
111
- // type: "fs:write_file",
112
- // },
113
- // ]);
114
- // debugger;
@@ -1,89 +0,0 @@
1
- import { hookIntoMethod } from "./hook_into_method.js";
2
-
3
- export const spyConsoleCalls = (
4
- { error, warn, info, log, trace, stdout, stderr },
5
- { preventConsoleSideEffects },
6
- ) => {
7
- const restoreCallbackSet = new Set();
8
- const errorHook = hookIntoMethod(console, "error", (message) => {
9
- return {
10
- preventOriginalCall: preventConsoleSideEffects,
11
- return: () => {
12
- error(message);
13
- },
14
- };
15
- });
16
- const warnHook = hookIntoMethod(console, "warn", (message) => {
17
- return {
18
- preventOriginalCall: preventConsoleSideEffects,
19
- return: () => {
20
- warn(message);
21
- },
22
- };
23
- });
24
- const infoHook = hookIntoMethod(console, "info", (message) => {
25
- return {
26
- preventOriginalCall: preventConsoleSideEffects,
27
- return: () => {
28
- info(message);
29
- },
30
- };
31
- });
32
- const logHook = hookIntoMethod(console, "log", (message) => {
33
- return {
34
- preventOriginalCall: preventConsoleSideEffects,
35
- return: () => {
36
- log(message);
37
- },
38
- };
39
- });
40
- const traceHook = hookIntoMethod(console, "trace", (message) => {
41
- return {
42
- preventOriginalCall: preventConsoleSideEffects,
43
- return: () => {
44
- trace(message);
45
- },
46
- };
47
- });
48
- const processStdouthook = hookIntoMethod(
49
- process.stdout,
50
- "write",
51
- (message) => {
52
- return {
53
- preventOriginalCall: preventConsoleSideEffects,
54
- return: () => {
55
- stdout(message);
56
- },
57
- };
58
- },
59
- );
60
- const processStderrHhook = hookIntoMethod(
61
- process.stderr,
62
- "write",
63
- (message) => {
64
- return {
65
- preventOriginalCall: preventConsoleSideEffects,
66
- return: () => {
67
- stderr(message);
68
- },
69
- };
70
- },
71
- );
72
- restoreCallbackSet.add(() => {
73
- errorHook.remove();
74
- warnHook.remove();
75
- infoHook.remove();
76
- logHook.remove();
77
- traceHook.remove();
78
- processStdouthook.remove();
79
- processStderrHhook.remove();
80
- });
81
- return {
82
- restore: () => {
83
- for (const restoreCallback of restoreCallbackSet) {
84
- restoreCallback();
85
- }
86
- restoreCallbackSet.clear();
87
- },
88
- };
89
- };