@j-o-r/sh 1.1.28 → 1.1.31

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/lib/Test.js CHANGED
@@ -1,51 +1,60 @@
1
1
  import { nextTick } from 'process';
2
- import { jsType } from './SH.js'
2
+ import { jsType } from './SH.js';
3
3
  import AsyncTracker from './AsyncTracker.js';
4
+
4
5
  /**
5
- * Valid jsTypes for test methods
6
- */
6
+ * Valid jsTypes for test callbacks (from {@link jsType}).
7
+ * @type {('Function'|'AsyncFunction')[]}
8
+ */
7
9
  const FNC = ['Function', 'AsyncFunction'];
8
- // Settle async calls in a SYNC function
10
+
11
+ /**
12
+ * Default timeout (ms) to settle async calls in sync test functions.
13
+ * @type {number}
14
+ */
9
15
  const SETTLE_ASYNC = 50;
10
16
 
11
17
  /**
12
- * @typedef {(function(): Promise<any>)} AsyncFunction
13
- */
18
+ * @typedef {() => Promise<any>} AsyncFunction
19
+ * @description Async test callback.
20
+ */
14
21
 
15
22
  /**
16
- * @typedef {Object} testDefinition
17
- * @prop {string} description
18
- * @prop {Function|AsyncFunction} callback - syc/ async function
19
- */
23
+ * @typedef {Object} TestDefinition
24
+ * @property {string} description - Test name/description.
25
+ * @property {Function | AsyncFunction} callback - Sync/async test function.
26
+ */
20
27
 
21
28
  /**
22
- * @typedef {Object} testReport
23
- * @prop {string} description
24
- * @prop {number} duration - start time in MS
25
- * @prop {boolean} executed - Has it been called?
26
- */
29
+ * @typedef {Object} TestReport
30
+ * @property {string} description - Test description.
31
+ * @property {number} duration - Execution duration (ms).
32
+ * @property {boolean} executed - Whether the test ran.
33
+ */
27
34
 
28
35
  /**
29
- * @typedef {Object} Report
30
- * @prop {number} tests
31
- * @prop {number} duration - start time in MS
32
- * @prop {number} errors - number of errors
33
- * @prop {number} executed - number of tests executed
34
- */
36
+ * @typedef {Object} TestReportSummary
37
+ * @property {number} tests - Total tests defined.
38
+ * @property {number} duration - Total execution time (ms).
39
+ * @property {number} errors - Number of failures.
40
+ * @property {number} executed - Number of tests run.
41
+ */
42
+
35
43
  /**
36
- * Get the current time
37
- * used for calculating a duration
38
- * @returns {number}
39
- */
44
+ * Returns current timestamp (ms) for duration calculations.
45
+ *
46
+ * @returns {number} Current time (Date.getTime()).
47
+ */
40
48
  function getNow() {
41
49
  return new Date().getTime();
42
50
  }
43
51
 
44
52
  /**
45
- * Get the duration based on a previous gathered start time
46
- * @param {number} start - start time
47
- * @returns {number}
48
- */
53
+ * Calculates duration from start time.
54
+ *
55
+ * @param {number} start - Start timestamp (ms).
56
+ * @returns {number} Duration (ms).
57
+ */
49
58
  function getDuration(start) {
50
59
  return new Date().getTime() - start;
51
60
  }
@@ -55,85 +64,78 @@ class Test {
55
64
  #promiseTracker;
56
65
  #catchErrors = false;
57
66
  #currentTest = -1;
58
- /** @type {testDefinition[]} */
67
+ /** @type {TestDefinition[]} */
59
68
  #tests = [];
60
- /** @type {testReport[]} */
69
+ /** @type {TestReport[]} */
61
70
  #reports = [];
62
- /**
63
- * @type {Error[]}
64
- */
71
+ /** @type {Error[]} */
65
72
  #errors = [];
66
- /** verbosed **/
67
73
  #quiet = false;
68
- /** Timeout in ms to settle async code blocks called from sync methods */
69
74
  #TO = SETTLE_ASYNC;
75
+
70
76
  /**
71
- * @param {boolean} [quiet] - does not output a report when true, default `false`
72
- */
77
+ * Creates a test runner instance.
78
+ *
79
+ * Tracks tests, errors, unresolved promises via {@link AsyncTracker}.
80
+ * Supports sync/async callbacks; detects global errors.
81
+ *
82
+ * @param {boolean} [quiet=false] - Suppress console reports.
83
+ * @example
84
+ * const t = new Test();
85
+ * t.add('basic', () => { throw new Error('fail'); });
86
+ * const report = await t.run();
87
+ */
73
88
  constructor(quiet = false) {
74
89
  if (quiet) {
75
90
  this.#quiet = true;
76
91
  }
77
- // Track for unresolved promises
78
92
  this.#promiseTracker = new AsyncTracker();
79
93
  this.#promiseTracker.enable('PROMISE');
80
94
  }
95
+
81
96
  /**
82
- * Set the timeout when a synced function is called.
83
- * This settles async code used in a sync function
84
- * and give some time to catch errors (#HACK)
85
- * @param {number} timeout - in MS, default 50
86
- */
97
+ * Sets timeout for settling async in sync tests (hack for late errors).
98
+ *
99
+ * @param {number} timeout - Timeout (ms); default 50.
100
+ * @example
101
+ * t.syncTimeout(100);
102
+ */
87
103
  syncTimeout(timeout) {
88
104
  this.#TO = timeout;
89
105
  }
90
106
 
91
107
  /**
92
- * Activate a listener on errors outside the call stack scope
93
- * @param {boolean} active - register / unregister
94
- */
95
- #detectErrors(active = true) {
96
- const errListener = (err) => {
97
- this.#handleError(err, true);
98
- }
99
- if (active) {
100
- if (!this.#catchErrors) {
101
- process.on('uncaughtException', errListener);
102
- this.#catchErrors = true;
103
- }
104
- } else {
105
- if (this.#catchErrors) {
106
- process.removeListener('uncaughtException', errListener);
107
- this.#catchErrors = false;
108
- }
109
- }
110
-
111
- }
112
-
113
- /**
114
- *
115
- * @param {string} description
116
- * @param {Function|AsyncFunction} callback - sync / async function
117
- * @throws Error when conditions are not met
118
- * @returns {Test}
119
- */
108
+ * Adds a test case.
109
+ *
110
+ * @param {string} description - Test name.
111
+ * @param {Function | AsyncFunction} callback - Test function.
112
+ * @returns {Test} Self for chaining.
113
+ * @throws {Error} Invalid description (non-string) or callback (not function).
114
+ * @example
115
+ * t.add('check 1+1', () => expect(1+1).toBe(2));
116
+ */
120
117
  add(description, callback) {
121
- if (typeof (description) !== 'string') {
122
- throw new Error(`'description' should be a string`)
118
+ if (typeof description !== 'string') {
119
+ throw new Error(`'description' should be a string`);
123
120
  }
124
121
  if (!FNC.includes(jsType(callback))) {
125
- throw new Error(`'callback' should be a (async) Function`)
122
+ throw new Error(`'callback' should be a (async) Function`);
126
123
  }
127
124
  this.#tests.push({ description, callback });
128
125
  return this;
129
126
  }
127
+
130
128
  /**
131
- * Execute tests
132
- * @param {number[]} [execute] - limit the execution tests
133
- * @returns {Promise<Report>}
134
- */
129
+ * Runs tests (all or selected); returns summary.
130
+ *
131
+ * Prints progress/errors; checks unresolved promises post-run.
132
+ *
133
+ * @param {number[]} [execute] - Indices of tests to run (default: all).
134
+ * @returns {Promise<TestReportSummary>} Summary stats.
135
+ * @example
136
+ * await t.run([0, 2]); // Run tests 0 and 2
137
+ */
135
138
  async run(execute) {
136
- // Detect errors outside the call stack
137
139
  this.#detectErrors(true);
138
140
  let errors = false;
139
141
  let i = 0;
@@ -155,16 +157,13 @@ class Test {
155
157
  await Promise.resolve(cb.callback());
156
158
  executed = true;
157
159
  if (type === 'Function') {
158
- // To settle async calls in sync functions
159
- // (catching errors outside this call stack, that may throw later (=== bad practise))
160
- await new Promise(resolve => setTimeout(resolve, this.#TO)); // This will pause the the current loop.
161
- // add timeout for an honest execution time
160
+ await new Promise(resolve => setTimeout(resolve, this.#TO));
162
161
  start = start + this.#TO;
163
162
  }
164
163
  } catch (e) {
165
164
  executed = true;
166
165
  if (!errors) {
167
- errors = true
166
+ errors = true;
168
167
  }
169
168
  error = e;
170
169
  }
@@ -180,9 +179,63 @@ class Test {
180
179
  this.#detectErrors(false);
181
180
  return this.#report();
182
181
  }
182
+
183
+ /**
184
+ * Prints unresolved promises report (if any).
185
+ *
186
+ * @example
187
+ * t.unresolved();
188
+ */
189
+ unresolved() {
190
+ const count = this.#promiseTracker.report(true);
191
+ if (count > 0) {
192
+ console.log('--------------------------------------------------');
193
+ console.log(`${count} Unresolved Promises detected.`);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Resets all tests, reports, errors, tracker.
199
+ *
200
+ * @example
201
+ * t.reset();
202
+ */
203
+ reset() {
204
+ this.#tests = [];
205
+ this.#reports = [];
206
+ this.#errors = [];
207
+ this.#promiseTracker.reset();
208
+ }
209
+
210
+ /**
211
+ * Toggles global error listener (uncaughtException).
212
+ *
213
+ * @private
214
+ * @param {boolean} active - Enable/disable.
215
+ */
216
+ #detectErrors(active = true) {
217
+ const errListener = (err) => {
218
+ this.#handleError(err, true);
219
+ };
220
+ if (active) {
221
+ if (!this.#catchErrors) {
222
+ process.on('uncaughtException', errListener);
223
+ this.#catchErrors = true;
224
+ }
225
+ } else {
226
+ if (this.#catchErrors) {
227
+ process.removeListener('uncaughtException', errListener);
228
+ this.#catchErrors = false;
229
+ }
230
+ }
231
+ }
232
+
183
233
  /**
184
- * @returns {Promise<Report>}
185
- */
234
+ * Generates final report summary.
235
+ *
236
+ * @private
237
+ * @returns {Promise<TestReportSummary>}
238
+ */
186
239
  async #report() {
187
240
  let duration = 0;
188
241
  let executed = 0;
@@ -197,64 +250,41 @@ class Test {
197
250
  executed = 1 + executed;
198
251
  }
199
252
  }
200
-
201
253
  }
202
254
  const errors = this.#errors.length;
203
255
  if (!this.#quiet) {
204
256
  console.log('--------------------------------------------------');
205
257
  console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
206
258
  if (tests !== executed) {
207
- if (!this.#quiet) console.log('** Not all tests have been executed **');
208
- // return { tests, executed, duration, errors };
259
+ console.log('** Not all tests have been executed **');
209
260
  }
210
- // Report on unresolved promises when NOT quiet
211
- // This must be on a next tick because the current Promise (where this code is in) is not resolved yet
212
261
  setTimeout(() => {
213
- const count = this.#promiseTracker.report(true);
214
- if (count > 0) {
215
- console.log('--------------------------------------------------');
216
- console.log(`${count} Unresolved Promises detected.`);
217
- }
262
+ const count = this.#promiseTracker.report(true);
263
+ if (count > 0) {
264
+ console.log('--------------------------------------------------');
265
+ console.log(`${count} Unresolved Promises detected.`);
266
+ }
218
267
  }, 10);
219
268
  }
220
269
  return { tests, executed, duration, errors };
221
270
  }
222
271
 
223
- unresolved() {
224
- const count = this.#promiseTracker.report(true);
225
- if (count > 0) {
226
- console.log('--------------------------------------------------');
227
- console.log(`${count} Unresolved Promises detected.`);
228
- }
229
- }
230
-
231
- /**
232
- * Empty tests
233
- */
234
- reset() {
235
- this.#tests = [];
236
- this.#reports = [];
237
- this.#errors = [];
238
- this.#promiseTracker.reset();
239
- }
240
272
  /**
241
- * Handle an error for the current test
242
- * @param {Error} err
243
- * @param {boolean} [outside] default false, Error is catched ouside the callscope of the test
244
- */
273
+ * Handles/logs test errors (current or global).
274
+ *
275
+ * @private
276
+ * @param {Error} err - Error instance.
277
+ * @param {boolean} [outside=false] - Global error flag.
278
+ */
245
279
  #handleError(err, outside = false) {
246
- // A global error is an error catched outside the callstack of the test
247
- const ERR = outside ? 'GLOBAL_ERROR' : 'ERROR'
280
+ const ERR = outside ? 'GLOBAL_ERROR' : 'ERROR';
248
281
  if (this.#currentTest > -1) {
249
282
  if (!this.#quiet) process.stdout.write(`\n`);
250
- // Register this error
251
- // Always print out errors despite #quiet
252
283
  const description = this.#reports[this.#currentTest].description;
253
284
  const executed = this.#reports[this.#currentTest].executed;
254
285
  if (executed) {
255
286
  process.stdout.write(`\x1b[31m-- ${ERR} Test: ${this.#currentTest}. ${description} --\x1b[0m\n`);
256
287
  } else {
257
- // Can been thrown from an other test
258
288
  process.stdout.write(`\x1b[31m-- ${ERR} --\x1b[0m\n`);
259
289
  }
260
290
  console.error(err);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@j-o-r/sh",
3
3
  "author": "Jorrit Duin <j-o-r@duin.work>",
4
4
  "type": "module",
5
- "version": "1.1.28",
5
+ "version": "1.1.31",
6
6
  "description": "Execute shell commands on Linux-based systems from javascript",
7
7
  "main": "lib/SH.js",
8
8
  "types": "types/SH.d.ts",
@@ -49,4 +49,4 @@
49
49
  "process-promise",
50
50
  "process-output"
51
51
  ]
52
- }
52
+ }
@@ -1,66 +1,100 @@
1
1
  export default AsyncTracker;
2
- /**
3
- * Represents an item in the async hooks tracking.
4
- */
5
2
  export type AsyncHookItem = {
6
3
  /**
7
- * - The unique identifier for the async operation.
4
+ * - Unique async ID.
8
5
  */
9
6
  key: number;
10
7
  /**
11
- * - The type of the async operation, e.g., 'PROMISE'.
8
+ * - Async type (e.g., 'PROMISE').
12
9
  */
13
10
  type: string;
14
11
  /**
15
- * - The async ID of the resource that triggered this operation.
12
+ * - Triggering async ID.
16
13
  */
17
14
  triggerAsyncId: number;
18
15
  /**
19
- * - The call stack trace when the async operation was initialized.
16
+ * - Call stack at init.
20
17
  */
21
18
  stack: string;
22
19
  /**
23
- * - The Promise object associated with this operation, including its state and async IDs.-
20
+ * - Associated resource (e.g., Promise).
24
21
  */
25
- resource: SystemTypes;
22
+ resource: any;
26
23
  };
24
+ export type SystemTypes = "PROMISE" | "TIMEOUT" | "PROCESSNEXTTICK" | "TICKOBJECT" | "SCRIPT" | "QUERYWRAP" | "FILEHANDLE" | "HTTP2SESSION" | "HTTP2STREAM" | "ZLIB" | "UDPSENDWRAP" | "WRITEWRAP" | "SHUTDOWNWRAP" | "PROMISEEXECUTOR" | "TCPCONNECTWRAP" | "GETADDRINFOREQWRAP" | "GETNAMEINFOREQWRAP" | "IMMEDIATE" | "TCPWRAP" | "TCPSERVERWRAP" | "UDPWRAP" | "FSREQCALLBACK" | "HTTPPARSER" | "PIPEWRAP" | "PIPECONNECTWRAP" | "STREAMWRAP" | "TTYWRAP" | "PROCESS" | "SIGNALWRAP" | "TIMERWRAP";
27
25
  /**
28
- * /**
26
+ * Tracks unresolved async operations using Node's async_hooks.
27
+ *
28
+ * Detects leaks like hanging Promises, Timers. Used by {@link Test}.
29
+ *
30
+ * @example
31
+ * const tracker = new AsyncTracker();
32
+ * tracker.enable('PROMISE');
33
+ * // Run code...
34
+ * console.log(tracker.report(true)); // Unresolved count + details
29
35
  */
30
- export type SystemTypes = "PROMISE" | "TIMEOUT" | "PROCESSNEXTTICK" | "TICKOBJECT" | "SCRIPT" | "QUERYWRAP" | "FILEHANDLE" | "HTTP2SESSION" | "HTTP2STREAM" | "ZLIB" | "UDPSENDWRAP" | "WRITEWRAP" | "SHUTDOWNWRAP" | "PROMISEEXECUTOR" | "TCPCONNECTWRAP" | "GETADDRINFOREQWRAP" | "GETNAMEINFOREQWRAP" | "IMMEDIATE" | "TCPWRAP" | "TCPSERVERWRAP" | "UDPWRAP" | "FSREQCALLBACK" | "HTTPPARSER" | "PIPEWRAP" | "PIPECONNECTWRAP" | "STREAMWRAP" | "TTYWRAP" | "PROCESS" | "SIGNALWRAP" | "TIMERWRAP";
31
36
  declare class AsyncTracker {
32
37
  /**
33
- * Enables tracking of asynchronous methods. Optionally, only methods of a specified type can be tracked.
34
- * @param {SystemTypes} [type] - optional only register async methods for a specific type
35
- */
38
+ * Enables tracking (optionally filter by type).
39
+ *
40
+ * @param {SystemTypes} [type] - Filter to specific type (e.g., 'PROMISE').
41
+ * @throws {Error} Unknown type.
42
+ * @example
43
+ * tracker.enable('PROMISE');
44
+ */
36
45
  enable(type?: SystemTypes): void;
37
46
  /**
38
- * Disables the tracking of asynchronous methods.
39
- */
47
+ * Disables tracking.
48
+ *
49
+ * @example
50
+ * tracker.disable();
51
+ */
40
52
  disable(): void;
41
53
  /**
42
- * Clears all tracked asynchronous methods.
43
- */
54
+ * Clears tracked items.
55
+ *
56
+ * @example
57
+ * tracker.reset();
58
+ */
44
59
  reset(): void;
60
+ /**
61
+ * Reports unresolved count (+ verbose details).
62
+ *
63
+ * @param {boolean} [verbose=false] - Print stack/resource per item.
64
+ * @returns {number} Unresolved count.
65
+ * @example
66
+ * if (tracker.report(true) > 0) console.error('Leaks!');
67
+ */
45
68
  report(verbose?: boolean): number;
46
69
  /**
47
- * Returns an array of unresolved asynchronous methods, optionally filtered by type
48
- * @param {SystemTypes} [type] - The type of async methods to filter by
49
- * @returns {AsyncHookItem[]}
50
- */
70
+ * Gets unresolved items (filtered).
71
+ *
72
+ * Temporarily disables hook during query.
73
+ *
74
+ * @param {SystemTypes} [type] - Filter type.
75
+ * @returns {AsyncHookItem[]} Array of items.
76
+ * @example
77
+ * const leaks = tracker.getUnresolved('PROMISE');
78
+ */
51
79
  getUnresolved(type?: SystemTypes): AsyncHookItem[];
52
80
  /**
53
- * Returns a description of the specified asynchronous method type
54
- * @param {SystemTypes} type - The type of asynchronous method.
55
- * @returns {string}
56
- */
81
+ * Gets description for type.
82
+ *
83
+ * @param {SystemTypes} type - Type.
84
+ * @returns {string} Description.
85
+ * @throws {Error} Unknown.
86
+ * @example
87
+ * tracker.getTypeDescription('PROMISE'); // 'Promise'
88
+ */
57
89
  getTypeDescription(type: SystemTypes): string;
58
90
  /**
59
- * Adds or overwrites a custom type for asynchronous methods.
60
- * @param {string} type - The type of asynchronous method.
61
- * @param {string} description - the description
62
- * @returns {void}
63
- */
91
+ * Registers custom type description.
92
+ *
93
+ * @param {string} type - Type key (uppercased).
94
+ * @param {string} description - Description.
95
+ * @example
96
+ * tracker.addCustomType('MYTYPE', 'My async op');
97
+ */
64
98
  addCustomType(type: string, description: string): void;
65
99
  #private;
66
100
  }