@j-o-r/sh 1.0.8 → 1.1.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/README.md CHANGED
@@ -66,6 +66,9 @@ The module also provides additional utilities for common tasks:
66
66
  - `readIn()`: Read from standard input.
67
67
  - `within(callback)`: Create an async context in a sync block.
68
68
  - `expBackoff(max, rand)`: Generate intervals for exponential backoff.
69
+ - `jsType(any)`: Get the 'real' javascript variable type
70
+ - `assert.`: Node assert library
71
+ - `new Test()`: A small sync/async minimal test framework
69
72
 
70
73
  ## SHDispatch
71
74
 
@@ -125,7 +128,7 @@ This class is returned by the `SH` function. Here's a summary of its methods and
125
128
  /**
126
129
  * Copy text to the clipboard
127
130
  * @param {string} text
128
- * @retruns {Promise<string>}
131
+ * @returns {Promise<string>}
129
132
  */
130
133
  const copyToClipboard = async (text) => {
131
134
  const prams = [
@@ -140,6 +143,31 @@ This class is returned by the `SH` function. Here's a summary of its methods and
140
143
  ```javascript
141
144
  SH`vim`.options({stdio: 'inherit'}).runSync();
142
145
  ```
146
+
147
+ - Create and run a test
148
+ ```javascript
149
+ import { assert, jsType, Test} from '@j-o-r/sh';
150
+
151
+ const test = new Test();
152
+ test.add('Test is test in sync', () => {
153
+ assert.strictEqual(jsType(test), 'Test');
154
+ });
155
+ test.add('Test an Array in sync', () => {
156
+ assert.strictEqual(jsType([]), 'Array');
157
+ });
158
+ test.add('Test is test in async', async () => {
159
+ assert.strictEqual(jsType(test), 'Test');
160
+ });
161
+ test.add('Test is test in a promise',
162
+ new Promise((resolve, _reject) => {
163
+ assert.strictEqual(jsType(test), 'Test');
164
+ resolve();
165
+ })
166
+ )
167
+
168
+ await test.run();
169
+ ```
170
+
143
171
  ## License
144
172
 
145
173
  This project is licensed under the Apache License, Version 2.0.
package/lib/SH.js CHANGED
@@ -28,7 +28,7 @@
28
28
  // Modified by: jorrit.duin+sh[AT]gmail.com
29
29
  import assert from 'node:assert';
30
30
  import SHDispatch from './SHDispatch.js';
31
-
31
+ import Test from './Test.js'
32
32
  /**
33
33
  * Creates a new SHDispatch object that represents a command to be executed.
34
34
  *
@@ -238,6 +238,19 @@ const parseArgs = (args) => {
238
238
  }
239
239
  return result;
240
240
  }
241
+
242
+ /**
243
+ * Determine a javascript type
244
+ *
245
+ * @param {any} fn - Any let type
246
+ * @returns {string} The "real" object / typeof name
247
+ */
248
+ const jsType = (fn) => {
249
+ if (fn === undefined) return 'undefined';
250
+ const type = Object.prototype.toString.call(fn).slice(8, -1);
251
+ return type === 'Object' ? fn.constructor.name : type;
252
+ };
253
+
241
254
  export {
242
255
  SH,
243
256
  cd,
@@ -246,5 +259,8 @@ export {
246
259
  readIn,
247
260
  within,
248
261
  expBackoff,
249
- parseArgs
262
+ parseArgs,
263
+ jsType,
264
+ Test,
265
+ assert
250
266
  }
package/lib/Test.js ADDED
@@ -0,0 +1,213 @@
1
+ import assert from 'node:assert/strict';
2
+ import { jsType } from './SH.js'
3
+ const FNC = ['Function', 'AsyncFunction', 'Promise'];
4
+ // Settle async calls in a SYNC function
5
+ const SETTLE_ASYNC = 100;
6
+
7
+ /**
8
+ * @typedef {Object} testDefinition
9
+ * @prop {string} description
10
+ * @prop {function|asyncfunction} callback
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} testReport
15
+ * @prop {string} description
16
+ * @prop {number} duration - start time in MS
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} Report
21
+ * @prop {number} tests
22
+ * @prop {number} duration - start time in MS
23
+ * @prop {number} errors - number of errors
24
+ * @prop {number} executed - number of tests executed
25
+ */
26
+ /**
27
+ * Get the current time
28
+ * @returns {number}
29
+ */
30
+ function getNow() {
31
+ return new Date().getTime();
32
+ }
33
+
34
+ /**
35
+ * Get the duration
36
+ * @param {number} start - start time
37
+ * @returns {number}
38
+ */
39
+ function getDuration(start) {
40
+ return new Date().getTime() - start;
41
+ }
42
+
43
+ class Test {
44
+ #catchErrors = false;
45
+ #currentTest = -1;
46
+ /** @type {testDefinition[]} */
47
+ #tests = [];
48
+ /** @type {testReport[]} */
49
+ #reports = [];
50
+ #errors = [];
51
+ /** verbosed **/
52
+ #quite = false;
53
+ /** Timeout in ms to settle async code blocks called from sync methods */
54
+ #TO = SETTLE_ASYNC;
55
+ /**
56
+ * @param {boolean} [quiet] - does not output a report when true, default `false`
57
+ */
58
+ constructor(quiet = false) {
59
+ if (quiet) {
60
+ this.#quite = true;
61
+ }
62
+ }
63
+ /**
64
+ * Set the timeout when a synced function is called.
65
+ * This settles async code used in a sync function
66
+ * and give some time to catch errors (#HACK)
67
+ * @param {number} timeout - in MS, default 100
68
+ */
69
+ syncTimeout(timeout) {
70
+ this.#TO = timeout;
71
+ }
72
+
73
+ /**
74
+ * Activate a listener on errors outside the call stack scope
75
+ * @param {boolean} active - register / unregister
76
+ */
77
+ #detectErrors(active = true) {
78
+ const errListener = (err) => {
79
+ this._handleError(err)
80
+ }
81
+ if (active) {
82
+ if (!this.#catchErrors) {
83
+ process.on('uncaughtException', errListener);
84
+ this.catchErrors = true;
85
+ }
86
+ } else {
87
+ if (this.#catchErrors) {
88
+ process.removeListener('uncaughtException', errListener);
89
+ this.catchErrors = false;
90
+ }
91
+ }
92
+
93
+ }
94
+
95
+ /**
96
+ *
97
+ * @param {string} description
98
+ * @param {Function|AsyncFunction|Promise} callback
99
+ * @throws Error when conditions are not met
100
+ */
101
+ add(description, callback) {
102
+ if (jsType(description) !== 'String') {
103
+ throw new Error(`'description' should be a string`)
104
+ }
105
+ if (!FNC.includes(jsType(callback))) {
106
+ throw new Error(`'callback' should be a (async) function or Promise`)
107
+ }
108
+ this.#tests.push({ description, callback });
109
+ }
110
+ /**
111
+ * Execute tests
112
+ * @param {number[]} [execute] - limit the execution tests
113
+ * @returns {Promise<Report>}
114
+ */
115
+ async run(execute) {
116
+ // Detect errors outside the call stack
117
+ this.#detectErrors(true);
118
+ let errors = false;
119
+ let i = 0;
120
+ const len = this.#tests.length;
121
+ for (; i < len; i++) {
122
+ this.#currentTest = i;
123
+ if (execute && !execute.includes(i)) continue;
124
+ let start = getNow();
125
+ const cb = this.#tests[i];
126
+ const type = jsType(cb.callback);
127
+ let error;
128
+ try {
129
+ if (type === 'Function') {
130
+ cb.callback();
131
+ // To settle async calls in sync functions
132
+ // (catching errors outside this call stack, that may throw later)
133
+ await new Promise(resolve => setTimeout(resolve, this.#TO)); // This will pause the the current loop.
134
+ // add timeout for an honest execution time
135
+ start = start + this.#TO;
136
+ } else if (type === 'Promise') {
137
+ await Promise.all([cb.callback]);
138
+ } else {
139
+ await cb.callback();
140
+ }
141
+ } catch (e) {
142
+ if (!errors) {
143
+ errors = true
144
+ }
145
+ error = e;
146
+ }
147
+ const duration = getDuration(start);
148
+ if (!this.#quite) process.stdout.write(`${i}. ${cb.description} : ${duration} ms\n`);
149
+ if (error) {
150
+ this._handleError(error);
151
+ }
152
+ this.#reports[i] = { description: cb.description, duration };
153
+ }
154
+ this.#currentTest = -1;
155
+ this.#detectErrors(false);
156
+ return this.#report();
157
+ }
158
+ /**
159
+ * @returns {Report}
160
+ */
161
+ #report() {
162
+ let duration = 0;
163
+ let executed = 0;
164
+ let errors = 0;
165
+ const tests = this.#tests.length;
166
+ let i = 0;
167
+ const len = this.#reports.length;
168
+ for (; i < len; i++) {
169
+ const r = this.#reports[i];
170
+ if (r) {
171
+ duration = r.duration + duration;
172
+ executed = 1 + executed;
173
+ const E = this.#errors[i];
174
+ if (E) {
175
+ errors = 1 + errors;
176
+ }
177
+ }
178
+
179
+ }
180
+ if (tests !== executed) {
181
+ if (!this.#quite) console.log('** Not all tests have been executed **');
182
+ }
183
+ if (!this.#quite) {
184
+ console.log('--------------------------------------------------');
185
+ console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
186
+ }
187
+ return {tests, executed, duration, errors};
188
+ }
189
+
190
+ /**
191
+ * Empty tests
192
+ */
193
+ reset() {
194
+ this.#tests = [];
195
+ this.#reports = [];
196
+ this.#errors = [];
197
+ }
198
+ /**
199
+ * Handle an error outside the callstack
200
+ * for the current test
201
+ */
202
+ _handleError(err) {
203
+ if (this.#currentTest > -1) {
204
+ // Register this error
205
+ // Always print out errors
206
+ // Despite #quite
207
+ console.error(err);
208
+ this.#errors[this.#currentTest] = err;
209
+ }
210
+ }
211
+ }
212
+
213
+ export default Test;
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.0.8",
5
+ "version": "1.1.0",
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",
@@ -22,13 +22,14 @@
22
22
  },
23
23
  "license": "Apache License, Version 2.0",
24
24
  "dependencies": {},
25
- "devDependencies": {},
26
25
  "bugs": {
27
26
  "url": "https://codeberg.org/duin/sh/issues"
28
27
  },
29
28
  "homepage": "https://codeberg.org/duin",
30
29
  "keywords": [
31
30
  "shell",
31
+ "typeof",
32
+ "test",
32
33
  "posix",
33
34
  "linux",
34
35
  "command-line",
@@ -46,4 +47,4 @@
46
47
  "process-promise",
47
48
  "process-output"
48
49
  ]
49
- }
50
+ }
package/types/SH.d.ts CHANGED
@@ -83,4 +83,13 @@ export function expBackoff(max?: string | undefined, rand?: string | undefined):
83
83
  * - The `_` property contains an array of unbound arguments.
84
84
  */
85
85
  export function parseArgs(args: string[]): object;
86
+ /**
87
+ * Determine a javascript type
88
+ *
89
+ * @param {any} fn - Any let type
90
+ * @returns {string} The object / let type name
91
+ */
92
+ export function jsType(fn: any): string;
93
+ import Test from './Test.js';
86
94
  import SHDispatch from './SHDispatch.js';
95
+ export { Test, assert };
@@ -0,0 +1,64 @@
1
+ export default Test;
2
+ export type testDefinition = {
3
+ description: string;
4
+ callback: Function | asyncfunction;
5
+ };
6
+ export type testReport = {
7
+ description: string;
8
+ /**
9
+ * - start time in MS
10
+ */
11
+ duration: number;
12
+ };
13
+ export type Report = {
14
+ tests: number;
15
+ /**
16
+ * - start time in MS
17
+ */
18
+ duration: number;
19
+ /**
20
+ * - number of errors
21
+ */
22
+ errors: number;
23
+ /**
24
+ * - number of tests executed
25
+ */
26
+ executed: number;
27
+ };
28
+ declare class Test {
29
+ /**
30
+ * @param {boolean} [quiet] - does not output a report when true, default `false`
31
+ */
32
+ constructor(quiet?: boolean | undefined);
33
+ /**
34
+ * Set the timeout when a synced function is called.
35
+ * This settles async code used in a sync function
36
+ * and give some time to catch errors (#HACK)
37
+ * @param {number} timeout - in MS, default 100
38
+ */
39
+ syncTimeout(timeout: number): void;
40
+ catchErrors: boolean | undefined;
41
+ /**
42
+ *
43
+ * @param {string} description
44
+ * @param {Function|AsyncFunction|Promise} callback
45
+ * @throws Error when conditions are not met
46
+ */
47
+ add(description: string, callback: Function | AsyncFunction | Promise<any>): void;
48
+ /**
49
+ * Execute tests
50
+ * @param {number[]} [execute] - limit the execution tests
51
+ * @returns {Promise<Report>}
52
+ */
53
+ run(execute?: number[] | undefined): Promise<Report>;
54
+ /**
55
+ * Empty tests
56
+ */
57
+ reset(): void;
58
+ /**
59
+ * Handle an error outside the callstack
60
+ * for the current test
61
+ */
62
+ _handleError(err: any): void;
63
+ #private;
64
+ }