@jsenv/snapshot 2.10.2 → 2.11.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/snapshot",
3
- "version": "2.10.2",
3
+ "version": "2.11.0",
4
4
  "description": "Snapshot testing",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@jsenv/assert": "4.4.1",
37
- "@jsenv/ast": "6.2.16",
37
+ "@jsenv/ast": "6.2.17",
38
38
  "@jsenv/exception": "1.1.2",
39
39
  "@jsenv/humanize": "1.2.8",
40
40
  "@jsenv/filesystem": "4.10.2",
@@ -4,17 +4,30 @@ import { createReplaceFilesystemWellKnownValues } from "../filesystem_well_known
4
4
  import { filesystemSideEffects } from "./filesystem/filesystem_side_effects.js";
5
5
  import { logSideEffects } from "./log/log_side_effects.js";
6
6
 
7
+ const executionEffectsDefault = {
8
+ catch: true,
9
+ return: true,
10
+ };
11
+
7
12
  export const createCaptureSideEffects = ({
8
13
  sourceFileUrl,
9
14
  logEffects = true,
10
15
  filesystemEffects = true,
11
16
  filesystemActions,
12
- executionEffects = {},
17
+ executionEffects = executionEffectsDefault,
13
18
  rootDirectoryUrl,
14
19
  replaceFilesystemWellKnownValues = createReplaceFilesystemWellKnownValues({
15
20
  rootDirectoryUrl,
16
21
  }),
17
22
  }) => {
23
+ if (executionEffects === false) {
24
+ executionEffects = {
25
+ catch: false,
26
+ return: false,
27
+ };
28
+ }
29
+ executionEffects = { ...executionEffectsDefault, ...executionEffects };
30
+
18
31
  const detectors = [];
19
32
  if (logEffects) {
20
33
  detectors.push(logSideEffects(logEffects === true ? {} : logEffects));
@@ -283,6 +296,33 @@ export const createCaptureSideEffects = ({
283
296
  finallyCallbackSet.clear();
284
297
  };
285
298
 
299
+ const onThrowOrReject = (value, isThrow) => {
300
+ if (executionEffects.catch === false) {
301
+ throw value;
302
+ }
303
+ if (typeof executionEffects.catch === "function") {
304
+ executionEffects.catch(value);
305
+ }
306
+ if (isThrow) {
307
+ onCatch(value);
308
+ } else {
309
+ onReject(value);
310
+ }
311
+ };
312
+ const onReturnOrResolve = (value, isReturn) => {
313
+ if (executionEffects.return === false) {
314
+ return;
315
+ }
316
+ if (typeof executionEffects.return === "function") {
317
+ executionEffects.return(value);
318
+ }
319
+ if (isReturn) {
320
+ onReturn(value);
321
+ } else {
322
+ onResolve(value);
323
+ }
324
+ };
325
+
286
326
  process.env.CAPTURING_SIDE_EFFECTS = "1";
287
327
  functionExecutingCount++;
288
328
  let returnedPromise = false;
@@ -292,34 +332,22 @@ export const createCaptureSideEffects = ({
292
332
  onReturn(RETURN_PROMISE);
293
333
  returnedPromise = valueReturned.then(
294
334
  (value) => {
295
- onResolve(value);
335
+ onReturnOrResolve(value);
296
336
  onFinally();
297
337
  return sideEffects;
298
338
  },
299
339
  (e) => {
300
- if (executionEffects.catch === false) {
301
- throw e;
302
- }
303
- if (typeof executionEffects.catch === "function") {
304
- executionEffects.catch(e);
305
- }
306
- onReject(e);
340
+ onThrowOrReject(e);
307
341
  onFinally();
308
342
  return sideEffects;
309
343
  },
310
344
  );
311
345
  return returnedPromise;
312
346
  }
313
- onReturn(valueReturned);
347
+ onReturnOrResolve(valueReturned, true);
314
348
  return sideEffects;
315
349
  } catch (e) {
316
- if (executionEffects.catch === false) {
317
- throw e;
318
- }
319
- if (typeof executionEffects.catch === "function") {
320
- executionEffects.catch(e);
321
- }
322
- onCatch(e);
350
+ onThrowOrReject(e, true);
323
351
  return sideEffects;
324
352
  } finally {
325
353
  if (!returnedPromise) {
@@ -49,7 +49,26 @@ export const spyFilesystemCalls = (
49
49
  const fileDescriptorPathMap = new Map();
50
50
  const fileRestoreMap = new Map();
51
51
  const dirRestoreMap = new Map();
52
- const onWriteFileDone = (fileUrl, stateBefore, stateAfter) => {
52
+
53
+ const onWriteFileDone = (stateBefore, stateAfter) =>
54
+ onFileMutationDone(stateBefore, stateAfter);
55
+ const onCopyFileDone = (stateBefore, stateAfter) =>
56
+ onFileMutationDone(stateBefore, stateAfter);
57
+ const onMoveFileDone = (fromStateBefore, toStateBefore, toStateAfter) => {
58
+ if (!toStateAfter.found) {
59
+ // seems to be possible somehow
60
+ return;
61
+ }
62
+ // effect on source file
63
+ registerUndoAndNotify(fromStateBefore, () => {
64
+ onRemoveFile(fromStateBefore.url, fromStateBefore.buffer, "move");
65
+ });
66
+ // effect on target file
67
+ registerUndoAndNotify(toStateBefore, () => {
68
+ onWriteFile(toStateBefore.url, fromStateBefore.buffer, "created");
69
+ });
70
+ };
71
+ const onFileMutationDone = (stateBefore, stateAfter) => {
53
72
  if (!stateAfter.found) {
54
73
  // seems to be possible somehow
55
74
  return;
@@ -58,42 +77,61 @@ export const spyFilesystemCalls = (
58
77
  // - writing file for the 1st time
59
78
  // - updating file content
60
79
  // the important part is the file content in the end of the function execution
61
- const reason =
62
- !stateBefore.found && stateAfter.found
63
- ? "created"
64
- : Buffer.compare(stateBefore.buffer, stateAfter.buffer)
65
- ? "content_modified"
66
- : stateBefore.mtimeMs === stateAfter.mtimeMs
67
- ? ""
68
- : "mtime_modified";
80
+ const reason = getMutationReason(stateBefore, stateAfter);
69
81
  if (!reason) {
70
- // file is exactly the same
71
- // function did not have any effect on the file
72
82
  return;
73
83
  }
74
- const action = getAction(fileUrl);
84
+ registerUndoAndNotify(stateBefore, () => {
85
+ onWriteFile(stateAfter.url, stateAfter.buffer, reason);
86
+ });
87
+ };
88
+ const getMutationReason = (stateBefore, stateAfter) => {
89
+ if (!stateBefore.found && stateAfter.found) {
90
+ return "created";
91
+ }
92
+ if (Buffer.compare(stateBefore.buffer, stateAfter.buffer)) {
93
+ return "content_modified";
94
+ }
95
+ if (stateBefore.mtimeMs !== stateAfter.mtimeMs) {
96
+ return "mtime_modified";
97
+ }
98
+ // file is exactly the same
99
+ // function did not have any effect on the file
100
+ return null;
101
+ };
102
+ const registerUndoAndNotify = (stateBefore, notify) => {
103
+ const url = stateBefore.url;
104
+ const action = getAction(url);
75
105
  const shouldCompare =
76
106
  action === "compare" ||
77
107
  action === "compare_presence_only" ||
78
108
  action === true;
79
- if (action === "undo" || shouldCompare) {
80
- if (undoFilesystemSideEffects && !fileRestoreMap.has(fileUrl)) {
81
- if (stateBefore.found) {
82
- fileRestoreMap.set(fileUrl, () => {
83
- writeFileSync(fileUrl, stateBefore.buffer);
84
- });
85
- } else {
86
- fileRestoreMap.set(fileUrl, () => {
87
- removeFileSync(fileUrl, { allowUseless: true });
88
- });
89
- }
109
+ // "ignore", false, anything else
110
+ undo: {
111
+ if (!undoFilesystemSideEffects) {
112
+ break undo;
113
+ }
114
+ if (action !== "undo" && !shouldCompare) {
115
+ break undo;
116
+ }
117
+ if (fileRestoreMap.has(url)) {
118
+ break undo;
119
+ }
120
+ if (stateBefore.found) {
121
+ fileRestoreMap.set(url, () => {
122
+ writeFileSync(url, stateBefore.buffer);
123
+ });
124
+ } else {
125
+ fileRestoreMap.set(url, () => {
126
+ removeFileSync(url, { allowUseless: true });
127
+ });
90
128
  }
91
129
  }
92
130
  if (shouldCompare) {
93
- onWriteFile(fileUrl, stateAfter.buffer, reason);
131
+ notify();
94
132
  }
95
- // "ignore", false, anything else
96
133
  };
134
+
97
135
  const onWriteDirectoryDone = (directoryUrl) => {
98
136
  const action = getAction(directoryUrl);
99
137
  const shouldCompare =
@@ -121,9 +159,9 @@ export const spyFilesystemCalls = (
121
159
  };
122
160
  const restoreCallbackSet = new Set();
123
161
 
124
- const getFileStateWithinHook = (fileUrl) => {
162
+ const getFileStateWithinHook = (filePath) => {
125
163
  return disableHooksWhileCalling(
126
- () => getFileState(fileUrl),
164
+ () => getFileState(filePath),
127
165
  [openHook, closeHook],
128
166
  );
129
167
  };
@@ -208,14 +246,13 @@ export const spyFilesystemCalls = (
208
246
  fileDescriptorPathMap.delete(fileDescriptor);
209
247
  return;
210
248
  }
211
- const fileUrl = pathToFileURL(filePath);
212
249
  if (buffer) {
213
- onReadFile(String(fileUrl));
250
+ onReadFile(filePath);
214
251
  }
215
252
  fileDescriptorPathMap.delete(fileDescriptor);
216
253
  filesystemStateInfoMap.delete(filePath);
217
- const stateAfter = getFileStateWithinHook(fileUrl);
218
- onWriteFileDone(String(fileUrl), stateBefore, stateAfter);
254
+ const stateAfter = getFileStateWithinHook(filePath);
255
+ onWriteFileDone(stateBefore, stateAfter);
219
256
  },
220
257
  };
221
258
  },
@@ -225,12 +262,11 @@ export const spyFilesystemCalls = (
225
262
  _internalFs,
226
263
  "writeFileUtf8",
227
264
  (filePath) => {
228
- const fileUrl = pathToFileURL(filePath);
229
- const stateBefore = getFileStateWithinHook(fileUrl);
265
+ const stateBefore = getFileStateWithinHook(filePath);
230
266
  return {
231
267
  return: () => {
232
- const stateAfter = getFileStateWithinHook(fileUrl);
233
- onWriteFileDone(String(fileUrl), stateBefore, stateAfter);
268
+ const stateAfter = getFileStateWithinHook(filePath);
269
+ onWriteFileDone(stateBefore, stateAfter);
234
270
  },
235
271
  };
236
272
  },
@@ -242,12 +278,45 @@ export const spyFilesystemCalls = (
242
278
  },
243
279
  };
244
280
  });
281
+ const copyFileHook = hookIntoMethod(
282
+ _internalFs,
283
+ "copyFile",
284
+ (fromPath, toPath) => {
285
+ const toStateBefore = getFileStateWithinHook(toPath);
286
+ return {
287
+ return: () => {
288
+ const toStateAfter = getFileStateWithinHook(toPath);
289
+ onCopyFileDone(toStateBefore, toStateAfter);
290
+ },
291
+ };
292
+ },
293
+ { execute: METHOD_EXECUTION_NODE_CALLBACK },
294
+ );
295
+ const renameHook = hookIntoMethod(
296
+ _internalFs,
297
+ "rename",
298
+ (fromPath, toPath) => {
299
+ const fromStateBefore = getFileStateWithinHook(fromPath);
300
+ const toStateBefore = getFileStateWithinHook(toPath);
301
+ return {
302
+ return: () => {
303
+ const toStateAfter = getFileStateWithinHook(toPath);
304
+ onMoveFileDone(fromStateBefore, toStateBefore, toStateAfter);
305
+ },
306
+ };
307
+ },
308
+ {
309
+ execute: METHOD_EXECUTION_NODE_CALLBACK,
310
+ },
311
+ );
245
312
  restoreCallbackSet.add(() => {
246
313
  mkdirHook.remove();
247
314
  openHook.remove();
248
315
  closeHook.remove();
249
316
  writeFileUtf8Hook.remove();
250
317
  unlinkHook.remove();
318
+ copyFileHook.remove();
319
+ renameHook.remove();
251
320
  });
252
321
  return {
253
322
  restore: () => {
@@ -275,11 +344,13 @@ export const spyFilesystemCalls = (
275
344
  };
276
345
  };
277
346
 
278
- const getFileState = (file) => {
347
+ const getFileState = (filePath) => {
348
+ const fileUrl = pathToFileURL(filePath);
279
349
  try {
280
- const fileBuffer = readFileSync(file);
281
- const { mtimeMs } = statSync(file);
350
+ const fileBuffer = readFileSync(fileUrl);
351
+ const { mtimeMs } = statSync(fileUrl);
282
352
  return {
353
+ url: String(fileUrl),
283
354
  found: true,
284
355
  mtimeMs,
285
356
  buffer: fileBuffer,
@@ -287,6 +358,7 @@ const getFileState = (file) => {
287
358
  } catch (e) {
288
359
  if (e.code === "ENOENT") {
289
360
  return {
361
+ url: String(fileUrl),
290
362
  found: false,
291
363
  };
292
364
  }
@@ -12,6 +12,11 @@ import { renderSideEffects, renderSmallLink } from "./render_side_effects.js";
12
12
  * @param {Object} snapshotTestsOptions
13
13
  * @param {string|url} snapshotTestsOptions.outFilePattern
14
14
  * @param {string|url} snapshotTestsOptions.rootDirectoryUrl
15
+ * @param {Object} [snapshotTestsOptions.executionEffects]
16
+ * @param {boolean} [snapshotTestsOptions.executionEffects.catch=true]
17
+ * Any error thrown by test function is detected and added to side effects
18
+ * @param {boolean} [snapshotTestsOptions.executionEffects.return=true]
19
+ * Test function return value is added to side effects
15
20
  * @param {Object} [snapshotTestsOptions.filesystemActions]
16
21
  * Control what to do when there is a file side effect
17
22
  * "compare", "compare_presence_only", "undo", "ignore"