@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 +32 -1
- package/lib/SH.js +19 -3
- package/lib/Test.js +218 -0
- package/package.json +6 -3
- package/types/SH.d.ts +10 -0
- package/types/SHDispatch.d.ts +1 -1
- package/types/SHExecute.d.ts +1 -1
- package/types/Test.d.ts +62 -0
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
|
-
* @
|
|
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.
|
|
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
|
-
|
|
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 };
|
package/types/SHDispatch.d.ts
CHANGED
package/types/SHExecute.d.ts
CHANGED
|
@@ -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):
|
|
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>}
|
package/types/Test.d.ts
ADDED
|
@@ -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
|
+
}
|