@j-o-r/sh 1.0.8 → 1.1.1

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,34 @@ 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 async, returning a promise', async () => {
162
+ return new Promise((resolve, _reject) => {
163
+ assert.strictEqual(jsType(test), 'Test');
164
+ resolve();
165
+ });
166
+ });
167
+ const report = await test.run();
168
+ if (report.errors > 0) {
169
+ process.exit(1);
170
+ }
171
+ // await test.run([0,3]); // only run test 0 and 3
172
+ ```
173
+
143
174
  ## License
144
175
 
145
176
  This project is licensed under the Apache License, Version 2.0.
package/lib/SH.js CHANGED
@@ -23,12 +23,12 @@
23
23
  // Changes Made:
24
24
  // - The code has been or is being reformatted to comply with ES2020 standards.
25
25
  // - Some methods were added and existing ones were modified or deleted to enhance usability.
26
- // - Most methods were deleted,
27
26
  // - The namespace has been changed from '$' to 'SH'.
28
27
  // Modified by: jorrit.duin+sh[AT]gmail.com
28
+ /** @type {assert} */
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,218 @@
1
+ import assert from 'node:assert/strict';
2
+ import { jsType } from './SH.js'
3
+ const FNC = ['Function', 'AsyncFunction'];
4
+ // Settle async calls in a SYNC function
5
+ const SETTLE_ASYNC = 50;
6
+
7
+ /**
8
+ * @typedef {(function(): Promise<any>)} AsyncFunction
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} testDefinition
13
+ * @prop {string} description
14
+ * @prop {Function|AsyncFunction} callback - syc/ async function
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} testReport
19
+ * @prop {string} description
20
+ * @prop {number} duration - start time in MS
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} Report
25
+ * @prop {number} tests
26
+ * @prop {number} duration - start time in MS
27
+ * @prop {number} errors - number of errors
28
+ * @prop {number} executed - number of tests executed
29
+ */
30
+ /**
31
+ * Get the current time
32
+ * used for calculating a duration
33
+ * @returns {number}
34
+ */
35
+ function getNow() {
36
+ return new Date().getTime();
37
+ }
38
+
39
+ /**
40
+ * Get the duration based on a previous gathered start time
41
+ * @param {number} start - start time
42
+ * @returns {number}
43
+ */
44
+ function getDuration(start) {
45
+ return new Date().getTime() - start;
46
+ }
47
+
48
+ class Test {
49
+ #catchErrors = false;
50
+ #currentTest = -1;
51
+ /** @type {testDefinition[]} */
52
+ #tests = [];
53
+ /** @type {testReport[]} */
54
+ #reports = [];
55
+ #errors = [];
56
+ /** verbosed **/
57
+ #quite = false;
58
+ /** Timeout in ms to settle async code blocks called from sync methods */
59
+ #TO = SETTLE_ASYNC;
60
+ /**
61
+ * @param {boolean} [quiet] - does not output a report when true, default `false`
62
+ */
63
+ constructor(quiet = false) {
64
+ if (quiet) {
65
+ this.#quite = true;
66
+ }
67
+ }
68
+ /**
69
+ * Set the timeout when a synced function is called.
70
+ * This settles async code used in a sync function
71
+ * and give some time to catch errors (#HACK)
72
+ * @param {number} timeout - in MS, default 100
73
+ */
74
+ syncTimeout(timeout) {
75
+ this.#TO = timeout;
76
+ }
77
+
78
+ /**
79
+ * Activate a listener on errors outside the call stack scope
80
+ * @param {boolean} active - register / unregister
81
+ */
82
+ #detectErrors(active = true) {
83
+ const errListener = (err) => {
84
+ this.#handleError(err)
85
+ }
86
+ if (active) {
87
+ if (!this.#catchErrors) {
88
+ process.on('uncaughtException', errListener);
89
+ this.#catchErrors = true;
90
+ }
91
+ } else {
92
+ if (this.#catchErrors) {
93
+ process.removeListener('uncaughtException', errListener);
94
+ this.#catchErrors = false;
95
+ }
96
+ }
97
+
98
+ }
99
+
100
+ /**
101
+ *
102
+ * @param {string} description
103
+ * @param {Function|AsyncFunction} callback - sync / async function
104
+ * @throws Error when conditions are not met
105
+ */
106
+ add(description, callback) {
107
+ if (jsType(description) !== 'String') {
108
+ throw new Error(`'description' should be a string`)
109
+ }
110
+ if (!FNC.includes(jsType(callback))) {
111
+ throw new Error(`'callback' should be a (async) Function`)
112
+ }
113
+ this.#tests.push({ description, callback });
114
+ }
115
+ /**
116
+ * Execute tests
117
+ * @param {number[]} [execute] - limit the execution tests
118
+ * @returns {Promise<Report>}
119
+ */
120
+ async run(execute) {
121
+ // Detect errors outside the call stack
122
+ this.#detectErrors(true);
123
+ let errors = false;
124
+ let i = 0;
125
+ const len = this.#tests.length;
126
+ for (; i < len; i++) {
127
+ this.#currentTest = i;
128
+ if (execute && !execute.includes(i)) continue;
129
+ let start = getNow();
130
+ const cb = this.#tests[i];
131
+ const type = jsType(cb.callback);
132
+ let error;
133
+ try {
134
+ if (type === 'Function') {
135
+ cb.callback();
136
+ // To settle async calls in sync functions
137
+ // (catching errors outside this call stack, that may throw later)
138
+ await new Promise(resolve => setTimeout(resolve, this.#TO)); // This will pause the the current loop.
139
+ // add timeout for an honest execution time
140
+ start = start + this.#TO;
141
+ } else {
142
+ await cb.callback();
143
+ }
144
+ } catch (e) {
145
+ if (!errors) {
146
+ errors = true
147
+ }
148
+ error = e;
149
+ }
150
+ const duration = getDuration(start);
151
+ if (!this.#quite) process.stdout.write(`${i}. ${cb.description} : ${duration} ms\n`);
152
+ if (error) {
153
+ this.#handleError(error);
154
+ }
155
+ this.#reports[i] = { description: cb.description, duration };
156
+ }
157
+ this.#currentTest = -1;
158
+ this.#detectErrors(false);
159
+ return this.#report();
160
+ }
161
+ /**
162
+ * @returns {Report}
163
+ */
164
+ #report() {
165
+ let duration = 0;
166
+ let executed = 0;
167
+ let errors = 0;
168
+ const tests = this.#tests.length;
169
+ let i = 0;
170
+ const len = this.#reports.length;
171
+ for (; i < len; i++) {
172
+ const r = this.#reports[i];
173
+ if (r) {
174
+ duration = r.duration + duration;
175
+ executed = 1 + executed;
176
+ const E = this.#errors[i];
177
+ if (E) {
178
+ errors = 1 + errors;
179
+ }
180
+ }
181
+
182
+ }
183
+ if (tests !== executed) {
184
+ if (!this.#quite) console.log('** Not all tests have been executed **');
185
+ }
186
+ if (!this.#quite) {
187
+ console.log('--------------------------------------------------');
188
+ console.log(`Total: ${tests} tests, executed: ${executed} in ${duration} ms - errors: ${errors}`);
189
+ }
190
+ return {tests, executed, duration, errors};
191
+ }
192
+
193
+ /**
194
+ * Empty tests
195
+ */
196
+ reset() {
197
+ this.#tests = [];
198
+ this.#reports = [];
199
+ this.#errors = [];
200
+ }
201
+ /**
202
+ * @private
203
+ * Handle an error outside the callstack
204
+ * for the current test
205
+ * @param {Error}
206
+ */
207
+ #handleError(err) {
208
+ if (this.#currentTest > -1) {
209
+ // Register this error
210
+ // Always print out errors
211
+ // Despite #quite
212
+ console.error(err);
213
+ this.#errors[this.#currentTest] = err;
214
+ }
215
+ }
216
+ }
217
+
218
+ 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.1",
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",
@@ -21,14 +21,17 @@
21
21
  "url": "https://codeberg.org/duin/sh"
22
22
  },
23
23
  "license": "Apache License, Version 2.0",
24
- "dependencies": {},
25
- "devDependencies": {},
24
+ "dependencies": {
25
+ "@types/node": "^22.10.10"
26
+ },
26
27
  "bugs": {
27
28
  "url": "https://codeberg.org/duin/sh/issues"
28
29
  },
29
30
  "homepage": "https://codeberg.org/duin",
30
31
  "keywords": [
31
32
  "shell",
33
+ "typeof",
34
+ "test",
32
35
  "posix",
33
36
  "linux",
34
37
  "command-line",
package/types/SH.d.ts CHANGED
@@ -83,4 +83,14 @@ 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 "real" object / typeof name
91
+ */
92
+ export function jsType(fn: any): string;
93
+ import Test from './Test.js';
94
+ import assert from 'node:assert';
86
95
  import SHDispatch from './SHDispatch.js';
96
+ export { Test, assert };
@@ -36,7 +36,7 @@ export type SHOptions = {
36
36
  /**
37
37
  * - The environment variables.
38
38
  */
39
- env?: any;
39
+ env?: NodeJS.ProcessEnv | undefined;
40
40
  /**
41
41
  * - The shell to use for execution.
42
42
  */
@@ -9,7 +9,7 @@ declare class SHExecute {
9
9
  * @param {string} [payload] - data to write
10
10
  * @retuns {Promise<object>}
11
11
  */
12
- runSync(payload?: string | undefined): any;
12
+ runSync(payload?: string | undefined): import("child_process").SpawnSyncReturns<Buffer> & import("child_process").SpawnSyncReturns<string> & import("child_process").SpawnSyncReturns<string | Buffer>;
13
13
  /**
14
14
  * @param {string} [payload] - data to write
15
15
  * @retuns {Promise<string>}
@@ -0,0 +1,62 @@
1
+ export default Test;
2
+ export type AsyncFunction = (() => Promise<any>);
3
+ export type testDefinition = {
4
+ description: string;
5
+ /**
6
+ * - syc/ async function
7
+ */
8
+ callback: Function | AsyncFunction;
9
+ };
10
+ export type testReport = {
11
+ description: string;
12
+ /**
13
+ * - start time in MS
14
+ */
15
+ duration: number;
16
+ };
17
+ export type Report = {
18
+ tests: number;
19
+ /**
20
+ * - start time in MS
21
+ */
22
+ duration: number;
23
+ /**
24
+ * - number of errors
25
+ */
26
+ errors: number;
27
+ /**
28
+ * - number of tests executed
29
+ */
30
+ executed: number;
31
+ };
32
+ declare class Test {
33
+ /**
34
+ * @param {boolean} [quiet] - does not output a report when true, default `false`
35
+ */
36
+ constructor(quiet?: boolean | undefined);
37
+ /**
38
+ * Set the timeout when a synced function is called.
39
+ * This settles async code used in a sync function
40
+ * and give some time to catch errors (#HACK)
41
+ * @param {number} timeout - in MS, default 100
42
+ */
43
+ syncTimeout(timeout: number): void;
44
+ /**
45
+ *
46
+ * @param {string} description
47
+ * @param {Function|AsyncFunction} callback - sync / async function
48
+ * @throws Error when conditions are not met
49
+ */
50
+ add(description: string, callback: Function | AsyncFunction): void;
51
+ /**
52
+ * Execute tests
53
+ * @param {number[]} [execute] - limit the execution tests
54
+ * @returns {Promise<Report>}
55
+ */
56
+ run(execute?: number[] | undefined): Promise<Report>;
57
+ /**
58
+ * Empty tests
59
+ */
60
+ reset(): void;
61
+ #private;
62
+ }