testilo 23.0.0 → 23.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 +48 -70
- package/batch.js +24 -21
- package/call.js +20 -21
- package/merge.js +38 -87
- package/package.json +1 -1
- package/procs/test.js +20 -0
- package/procs/util.js +63 -0
- package/script.js +1 -2
package/README.md
CHANGED
|
@@ -65,8 +65,7 @@ Testilo can, however, make job preparation more efficient in these scenarios:
|
|
|
65
65
|
|
|
66
66
|
The simplest version of a list of targets is a _target list_. It is an array of arrays defining 1 or more targets. It can be stored as a tab-delimited text file.
|
|
67
67
|
|
|
68
|
-
A target is defined by
|
|
69
|
-
- An ID
|
|
68
|
+
A target is defined by 2 items:
|
|
70
69
|
- A description
|
|
71
70
|
- A URL
|
|
72
71
|
|
|
@@ -74,16 +73,16 @@ For example, a target list might be:
|
|
|
74
73
|
|
|
75
74
|
```javaScript
|
|
76
75
|
[
|
|
77
|
-
['
|
|
78
|
-
['
|
|
76
|
+
['World Wide Web Consortium', 'https://www.w3.org/'],
|
|
77
|
+
['Mozilla Foundation', 'https://foundation.mozilla.org/en/']
|
|
79
78
|
]
|
|
80
79
|
```
|
|
81
80
|
|
|
82
81
|
If this target list were stored as a file, its content would be this (with “→” representing the Tab character):
|
|
83
82
|
|
|
84
83
|
```text
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
World Wide Web Consortium→https://www.w3.org/
|
|
85
|
+
Mozilla Foundation→https://foundation.mozilla.org/en/
|
|
87
86
|
```
|
|
88
87
|
|
|
89
88
|
### Batches
|
|
@@ -97,27 +96,21 @@ Targets can be specified in a more complex way, too. That allows you to create j
|
|
|
97
96
|
targets: [
|
|
98
97
|
{
|
|
99
98
|
id: 'acme',
|
|
100
|
-
which: 'https://acmeclothes.com/',
|
|
101
99
|
what: 'Acme Clothes',
|
|
100
|
+
which: 'https://acmeclothes.com/',
|
|
102
101
|
acts: {
|
|
103
102
|
public: [
|
|
104
103
|
{
|
|
105
|
-
type: 'launch'
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
type: 'url',
|
|
109
|
-
which: 'https://acmeclothes.com/',
|
|
110
|
-
what: 'Acme Clothes home page'
|
|
104
|
+
type: 'launch',
|
|
105
|
+
what: 'Acme Clothes home page',
|
|
106
|
+
url: 'https://acmeclothes.com/'
|
|
111
107
|
}
|
|
112
108
|
],
|
|
113
109
|
private: [
|
|
114
110
|
{
|
|
115
|
-
type: 'launch'
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
type: 'url',
|
|
119
|
-
which: 'https://acmeclothes.com/login.html',
|
|
120
|
-
what: 'Acme Clothes login page'
|
|
111
|
+
type: 'launch',
|
|
112
|
+
what: 'Acme Clothes login page',
|
|
113
|
+
url: 'https://acmeclothes.com/login.html'
|
|
121
114
|
},
|
|
122
115
|
{
|
|
123
116
|
type: 'text',
|
|
@@ -166,10 +159,6 @@ Here is a script:
|
|
|
166
159
|
timeLimit: 60,
|
|
167
160
|
standard: 'also',
|
|
168
161
|
observe: false,
|
|
169
|
-
timeStamp: '240115T1200',
|
|
170
|
-
requester: 'you@yourdomain.com',
|
|
171
|
-
urlPrefix: '/file/reports',
|
|
172
|
-
urlSuffix: '.json',
|
|
173
162
|
acts: [
|
|
174
163
|
{
|
|
175
164
|
type: 'placeholder',
|
|
@@ -199,17 +188,14 @@ A script has several properties that specify facts about the jobs to be created.
|
|
|
199
188
|
- `what`: a description of the script.
|
|
200
189
|
- `strict`: `true` if Testaro is to abort jobs when a target redirects a request to a URL differing substantially from the one specified. If `false` Testaro is to allow redirection. All differences are considered substantial unless the URLs differ only in the presence and absence of a trailing slash.
|
|
201
190
|
- `isolate`: If `true`, Testilo, before creating a job, will isolate test acts, as needed, from effects of previous test acts, by inserting a copy of the latest placeholder after each target-modifying test act other than the final act. If `false`, placeholders will not be duplicated.
|
|
191
|
+
- `timeLimit`: This specifies the maximum duration, in seconds, of a job. Testaro will abort jobs that are not completed within that time.
|
|
202
192
|
- `standard`: If `also`, jobs will tell Testaro to include in its reports both the original results of the tests of tools and the Testaro-standardized results. If `only`, reports are to include only the standardized test results. If `no`, reports are to include only the original results, without standardization.
|
|
203
193
|
- `observe`: If `true`, jobs will tell Testaro to allow granular observation of job progress. If `false`, jobs will tell Testaro not to permit granular observation, but only to send the report to the server when the report is completed. It is generally user-friendly to allow granular observation, and for user applications to implement it, if they make users wait while jobs are assigned and performed, since that process typically takes about 3 minutes.
|
|
204
|
-
- `timeStamp`: This string specifies a UTC date and time when jobs created with the script are to be permitted to be assigned to agents. Thus, jobs can be created from a script for later performance. The value of `timeStamp` is a compact representation in the format `yymmddThhMM`. If the value is an empty string, Testilo will make the date and time equal to the time when jobs are created.
|
|
205
|
-
- `requester`: the email address that any notices of job completion can be sent to, or an empty string if there is a `REQUESTER` environment variable and it is to be used.
|
|
206
|
-
- `urlPrefix`: the start of a URL that the Testaro reports will be retrievable at.
|
|
207
|
-
- `urlSuffix`: the end of that URL. The job ID will be inserted between the `urlPrefix` and the `urlSuffix`.
|
|
208
194
|
- `acts`: an array of acts.
|
|
209
195
|
|
|
210
196
|
The first act in this example script is a placeholder, whose `which` property is `'private'`. If the above batch were merged with this script, in each job the placeholder would be replaced with the `private` acts of a target. For example, the first act of the first job would launch a Chromium browser, navigate to the Acme login page, complete and submit the login form, wait for the account page to load, run the Axe tests, and then run the QualWeb tests. If the batch contained additional targets, additional jobs would be created, with the login actions for each target specified in the `private` array of the `acts` object of that target.
|
|
211
197
|
|
|
212
|
-
As shown in this example, when a browser is launched by placeholder substitution, the script can determine the browser type (`chromium`, `firefox`, or `webkit`) by assigning a value to a `launch` property of the placeholder.
|
|
198
|
+
As shown in this example, when a browser is launched by placeholder substitution, the script can determine the browser type (`chromium`, `firefox`, or `webkit`) by assigning a value to a `launch` property of the placeholder. This allows a script to ensure that the tests it requires are performed with appropriate browser types. Some browser types are incompatible with some tests.
|
|
213
199
|
|
|
214
200
|
### Target list to batch
|
|
215
201
|
|
|
@@ -225,13 +211,15 @@ A module can invoke `batch` in this way:
|
|
|
225
211
|
|
|
226
212
|
```javaScript
|
|
227
213
|
const {batch} = require('testilo/batch');
|
|
228
|
-
const batchObj = batch(
|
|
214
|
+
const batchObj = batch(id, what, targets);
|
|
229
215
|
```
|
|
230
216
|
|
|
231
|
-
This invocation references `
|
|
217
|
+
This invocation references `id`, `what`, and `targets` variables that the module must have already defined. The `id` variable is a unique identifier for the target list. The `what` variable describes the target list. The `targets` variable is an array of arrays, with each array containing the 2 items (description and URL) defining one target.
|
|
232
218
|
|
|
233
219
|
The `batch()` function of the `batch` module generates a batch and returns it as an object. The invoking module can further dispose of the batch as needed.
|
|
234
220
|
|
|
221
|
+
The ID assigned to each target by the `batch()` function is a sequential (base-62 alphanumeric) string, rather than a mnemonic like the one (`'acme'`) in the above example.
|
|
222
|
+
|
|
235
223
|
##### By a user
|
|
236
224
|
|
|
237
225
|
A user can invoke `batch` in this way:
|
|
@@ -299,6 +287,8 @@ To create a script without any issue restrictions, a user can execute the statem
|
|
|
299
287
|
|
|
300
288
|
#### Options
|
|
301
289
|
|
|
290
|
+
The `script` module will use the value of the `SEND_REPORT_TO` environment variable as the value of the `sendReportTo` property of the script, if that variable exists, and otherwise will leave that property with an empty-string value.
|
|
291
|
+
|
|
302
292
|
When the `script` module creates a script for you, it does not ask you for all of the options that the script may require. Instead, it chooses default options. After you invoke `script`, you can edit the script that it creates to revise options.
|
|
303
293
|
|
|
304
294
|
### Merge
|
|
@@ -317,16 +307,14 @@ Suppose you ask for a merger of the above batch and script. Then the first job p
|
|
|
317
307
|
timeLimit: 60,
|
|
318
308
|
standard: 'also',
|
|
319
309
|
observe: false,
|
|
310
|
+
sendReportTo: 'https://ourdomain.com/testman/api/report'
|
|
320
311
|
timeStamp: '240115T1200',
|
|
321
312
|
acts: [
|
|
322
313
|
{
|
|
323
314
|
type: 'launch',
|
|
324
|
-
which: 'chromium'
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
type: 'url',
|
|
328
|
-
which: 'https://acmeclothes.com/login.html',
|
|
329
|
-
what: 'Acme Clothes login page'
|
|
315
|
+
which: 'chromium',
|
|
316
|
+
what: 'Acme Clothes login page',
|
|
317
|
+
url: 'https://acmeclothes.com/login.html'
|
|
330
318
|
},
|
|
331
319
|
{
|
|
332
320
|
type: 'text',
|
|
@@ -357,12 +345,9 @@ Suppose you ask for a merger of the above batch and script. Then the first job p
|
|
|
357
345
|
},
|
|
358
346
|
{
|
|
359
347
|
type: 'launch',
|
|
360
|
-
which: 'chromium'
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
type: 'url',
|
|
364
|
-
which: 'https://acmeclothes.com/login.html',
|
|
365
|
-
what: 'Acme Clothes login page'
|
|
348
|
+
which: 'chromium',
|
|
349
|
+
what: 'Acme Clothes login page',
|
|
350
|
+
url: 'https://acmeclothes.com/login.html'
|
|
366
351
|
},
|
|
367
352
|
{
|
|
368
353
|
type: 'text',
|
|
@@ -399,19 +384,18 @@ Suppose you ask for a merger of the above batch and script. Then the first job p
|
|
|
399
384
|
id: 'acme',
|
|
400
385
|
what: 'Acme Clothes'
|
|
401
386
|
},
|
|
402
|
-
requester: 'you@yourdomain.tld'
|
|
403
|
-
url: '/file/reports/240115T1200-4Rw-acme.json'
|
|
387
|
+
requester: 'you@yourdomain.tld'
|
|
404
388
|
},
|
|
405
|
-
creationTime: '
|
|
389
|
+
creationTime: '241120T1550'
|
|
406
390
|
}
|
|
407
391
|
```
|
|
408
392
|
|
|
409
393
|
Testilo has substituted the `private` acts from the `acme` target of the batch for the placeholder when creating the job. Testilo also has:
|
|
410
394
|
- inserted a copy of those same acts after the `axe` test act, because `axe` is a target-modifying tool.
|
|
411
395
|
- let the script determine the browser type of the `launch` act.
|
|
412
|
-
- added the creation time to the job.
|
|
413
396
|
- given the job an ID that combines the time stamp with a differentiator and the batch ID.
|
|
414
397
|
- inserted a `sources` property into the job, recording facts about the script, the batch, the target, the requester, and the report URL.
|
|
398
|
+
- added a time stamp describing the creation time to the job.
|
|
415
399
|
|
|
416
400
|
This is a valid Testaro job.
|
|
417
401
|
|
|
@@ -425,25 +409,16 @@ A module can invoke `merge` in this way:
|
|
|
425
409
|
|
|
426
410
|
```javaScript
|
|
427
411
|
const {merge} = require('testilo/merge');
|
|
428
|
-
const jobs = merge(
|
|
429
|
-
script, batch, requester, isolate, standard, isGranular, timeStamp, 'file/reports/', '.json'
|
|
430
|
-
);
|
|
412
|
+
const jobs = merge(script, batch, requester, timeStamp);
|
|
431
413
|
```
|
|
432
414
|
|
|
433
|
-
The `merge` module uses these
|
|
415
|
+
The `merge` module uses these 4 arguments to create jobs from a script and a batch.
|
|
434
416
|
|
|
435
417
|
The arguments are:
|
|
436
418
|
- `script`: a script.
|
|
437
419
|
- `batch`: a batch.
|
|
438
|
-
- `requester`: an email address
|
|
439
|
-
- `
|
|
440
|
-
- `standard`: one of the standardization options (`'also'`, `'only'`, or `'no'`).
|
|
441
|
-
- `isGranular`: `true` if Testaro is to allow granular observation of the job when performed under a network watch, or `false` if not.
|
|
442
|
-
- `timeStamp`: a time stamp in the format `240115T1200`.
|
|
443
|
-
- `urlPnrefix`: the start of an absolute or relative report URL.
|
|
444
|
-
- `urlSuffix`: the end of a report URL.
|
|
445
|
-
|
|
446
|
-
In this case, a job with ID `240115T1200-4Rw-acme` will produce a report that can be retrieved at the relative URL `/file/reports/240115T1200-4Rw-acme.json`.
|
|
420
|
+
- `requester`: an email address.
|
|
421
|
+
- `timeStamp`: the earliest UTC date and time when the jobs may be assigned (format `240415T1230`), or an empty string if now.
|
|
447
422
|
|
|
448
423
|
The `merge()` function of the `merge` module generates jobs and returns them in an array. The invoking module can further dispose of the jobs as needed.
|
|
449
424
|
|
|
@@ -453,21 +428,16 @@ A user can invoke `merge` in this way:
|
|
|
453
428
|
|
|
454
429
|
- Create a script and save it as a JSON file in the `scripts` subdirectory of the `SPECDIR` directory.
|
|
455
430
|
- Create a batch and save it as a JSON file in the `batches` subdirectory of the `SPECDIR` directory.
|
|
456
|
-
- In the Testilo project directory, execute the statement `node call merge
|
|
431
|
+
- In the Testilo project directory, execute the statement `node call merge scriptID batchID requester timeStamp todoDir`.
|
|
457
432
|
|
|
458
433
|
In this statement, replace:
|
|
459
|
-
- `
|
|
460
|
-
- `
|
|
461
|
-
- `requester`
|
|
462
|
-
- `
|
|
463
|
-
- `standard` with `'also'`, `'only'`, or `'no'` to specify the treatment of standard-format results.
|
|
464
|
-
- `observe` with `true` if granular observation is to be permitted, or `false` if not.
|
|
465
|
-
- `todoDir` with `true` if the job is to be saved in the `todo` subdirectory, or `false` if it is to be saved in the `pending` subdirectory, of the `JOBDIR` directory.
|
|
466
|
-
- `urlPrefix` with the pre-ID part of the report URL.
|
|
467
|
-
- `urlSuffix` with the post-ID part of the report URL.
|
|
434
|
+
- `scriptID` with the ID (which is also the base of the file name) of the script.
|
|
435
|
+
- `batchID` with the ID (which is also the base of the file name) of the batch.
|
|
436
|
+
- `requester` and `timeStamp` as described above.
|
|
437
|
+
- `todoDir`: `true` if the jobs are to be saved in the `todo` subdirectory, or `false` if they are to be saved in the `pending` subdirectory, of the `JOBDIR` directory.
|
|
468
438
|
|
|
469
439
|
The `call` module will retrieve the named script and batch from their respective directories.
|
|
470
|
-
The `merge` module will create an array of jobs
|
|
440
|
+
The `merge` module will create an array of jobs.
|
|
471
441
|
The `call` module will save the jobs as JSON files in the `todo` or `pending` subdirectory of the `JOBDIR` directory.
|
|
472
442
|
|
|
473
443
|
#### Validation
|
|
@@ -733,3 +703,11 @@ The third argument to `call` (`23pl` in this example) is optional. If it is omit
|
|
|
733
703
|
### Validation
|
|
734
704
|
|
|
735
705
|
To test the `compare` module, in the project directory you can execute the statement `node validation/compare/validate`. If `compare` is valid, all logging statements will begin with “Success” and none will begin with “ERROR”.
|
|
706
|
+
|
|
707
|
+
## Origin
|
|
708
|
+
|
|
709
|
+
Work on the functionalities of Testaro and Testilo began in 2017. It was named [Autotest](https://github.com/jrpool/autotest) in early 2021 and then partitioned into the more single-purpose packages Testaro and Testilo in January 2022.
|
|
710
|
+
|
|
711
|
+
## Etymology
|
|
712
|
+
|
|
713
|
+
“Testilo” means “testing tool” in Esperanto.
|
package/batch.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
Converts a target list to a batch.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {alphaNumOf} = require('./procs/util');
|
|
9
|
+
|
|
10
|
+
// FUNCTIONS
|
|
7
11
|
|
|
8
12
|
// Converts a target list to a batch and returns the batch.
|
|
9
13
|
exports.batch = (id, what, targetList) => {
|
|
@@ -17,9 +21,9 @@ exports.batch = (id, what, targetList) => {
|
|
|
17
21
|
&& targetList.length
|
|
18
22
|
&& targetList.every(
|
|
19
23
|
target => Array.isArray(target)
|
|
24
|
+
&& target.length === 2
|
|
20
25
|
&& target.every(item => typeof item === 'string')
|
|
21
26
|
)
|
|
22
|
-
&& targetList.some(target => target.length === 3)
|
|
23
27
|
) {
|
|
24
28
|
// Initialize the batch.
|
|
25
29
|
const batch = {
|
|
@@ -27,25 +31,23 @@ exports.batch = (id, what, targetList) => {
|
|
|
27
31
|
what,
|
|
28
32
|
targets: []
|
|
29
33
|
};
|
|
30
|
-
// For each
|
|
31
|
-
targetList.forEach(target => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
});
|
|
48
|
-
}
|
|
34
|
+
// For each target:
|
|
35
|
+
targetList.forEach((target, index) => {
|
|
36
|
+
// Add it to the batch.
|
|
37
|
+
batch.targets.push({
|
|
38
|
+
id: alphaNumOf(index),
|
|
39
|
+
what: target[0],
|
|
40
|
+
which: target[1],
|
|
41
|
+
acts: {
|
|
42
|
+
main: [
|
|
43
|
+
{
|
|
44
|
+
type: 'launch',
|
|
45
|
+
what: target[0],
|
|
46
|
+
url: target[1]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
});
|
|
49
51
|
});
|
|
50
52
|
// Return the batch.
|
|
51
53
|
return batch;
|
|
@@ -53,6 +55,7 @@ exports.batch = (id, what, targetList) => {
|
|
|
53
55
|
// Otherwise, i.e. if the arguments are invalid:
|
|
54
56
|
else {
|
|
55
57
|
// Return this.
|
|
58
|
+
console.log('ERROR: information missing or invalid');
|
|
56
59
|
return null;
|
|
57
60
|
}
|
|
58
61
|
};
|
package/call.js
CHANGED
|
@@ -43,16 +43,21 @@ const fnArgs = process.argv.slice(3);
|
|
|
43
43
|
// ########## FUNCTIONS
|
|
44
44
|
|
|
45
45
|
// Converts a target list to a batch.
|
|
46
|
-
const callBatch = async (
|
|
46
|
+
const callBatch = async (id, what) => {
|
|
47
47
|
// Get the target list.
|
|
48
|
-
const listString = await fs.readFile(`${specDir}/targetLists/${
|
|
49
|
-
const list = listString
|
|
48
|
+
const listString = await fs.readFile(`${specDir}/targetLists/${id}.tsv`, 'utf8');
|
|
49
|
+
const list = listString
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter(target => target.length)
|
|
52
|
+
.map(target => target.split('\t'));
|
|
50
53
|
// Convert it to a batch.
|
|
51
|
-
const batchObj = batch(
|
|
54
|
+
const batchObj = batch(id, what, list);
|
|
52
55
|
// Save the batch.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if (batchObj) {
|
|
57
|
+
const batchJSON = JSON.stringify(batchObj, null, 2);
|
|
58
|
+
await fs.writeFile(`${specDir}/batches/${id}.json`, `${batchJSON}\n`);
|
|
59
|
+
console.log(`Target list ${id} converted to a batch and saved in ${specDir}/batches`);
|
|
60
|
+
}
|
|
56
61
|
};
|
|
57
62
|
// Fulfills a script-creation request.
|
|
58
63
|
const callScript = async (scriptID, classificationID = null, ... issueIDs) => {
|
|
@@ -72,10 +77,8 @@ const callMerge = async (
|
|
|
72
77
|
scriptID,
|
|
73
78
|
batchID,
|
|
74
79
|
requester,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
isGranular,
|
|
78
|
-
todo
|
|
80
|
+
timeStamp,
|
|
81
|
+
todoDir
|
|
79
82
|
) => {
|
|
80
83
|
// Get the script and the batch.
|
|
81
84
|
const scriptJSON = await fs.readFile(`${specDir}/scripts/${scriptID}.json`, 'utf8');
|
|
@@ -83,19 +86,15 @@ const callMerge = async (
|
|
|
83
86
|
const batchJSON = await fs.readFile(`${specDir}/batches/${batchID}.json`, 'utf8');
|
|
84
87
|
const batch = JSON.parse(batchJSON);
|
|
85
88
|
// Merge them into an array of jobs.
|
|
86
|
-
const jobs = merge(
|
|
87
|
-
script, batch, requester, withIsolation, standard, isGranular, null, urlPrefix, urlSuffix
|
|
88
|
-
);
|
|
89
|
+
const jobs = merge(script, batch, requester, timeStamp);
|
|
89
90
|
// Save the jobs.
|
|
90
|
-
const
|
|
91
|
+
const subdir = `${jobDir}/${todoDir === 'true' ? 'todo' : 'pending'}`;
|
|
91
92
|
for (const job of jobs) {
|
|
92
93
|
const jobJSON = JSON.stringify(job, null, 2);
|
|
93
|
-
await fs.writeFile(`${
|
|
94
|
+
await fs.writeFile(`${subdir}/${job.id}.json`, `${jobJSON}\n`);
|
|
94
95
|
}
|
|
95
|
-
const
|
|
96
|
-
console.log(
|
|
97
|
-
`Script ${scriptID} and batch ${batchID} merged as ${timeStamp}-… in ${jobDir}/${destination}`
|
|
98
|
-
);
|
|
96
|
+
const truncatedID = `${jobs[0].timeStamp}-${jobs[0].mergeID}-…`;
|
|
97
|
+
console.log(`Script ${scriptID} and batch ${batchID} merged as ${truncatedID} in ${subdir}`);
|
|
99
98
|
};
|
|
100
99
|
// Gets selected reports.
|
|
101
100
|
const getReports = async (type, selector = '') => {
|
|
@@ -222,7 +221,7 @@ else if (fn === 'script' && fnArgs.length) {
|
|
|
222
221
|
console.log('Execution completed');
|
|
223
222
|
});
|
|
224
223
|
}
|
|
225
|
-
else if (fn === 'merge' && fnArgs.length ===
|
|
224
|
+
else if (fn === 'merge' && fnArgs.length === 5) {
|
|
226
225
|
callMerge(... fnArgs)
|
|
227
226
|
.then(() => {
|
|
228
227
|
console.log('Execution completed');
|
package/merge.js
CHANGED
|
@@ -1,83 +1,33 @@
|
|
|
1
1
|
/*
|
|
2
2
|
merge.js
|
|
3
3
|
Merges a script and a batch and returns jobs.
|
|
4
|
-
Arguments:
|
|
5
|
-
0. script
|
|
6
|
-
1. batch
|
|
7
|
-
2. requester
|
|
8
|
-
3. whether to provide test isolation
|
|
9
|
-
4. value of the standard property
|
|
10
|
-
5. whether reporting is to be granular
|
|
11
|
-
6. date and time as a compact timestamp for job execution, if not now
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
6
|
// ########## IMPORTS
|
|
15
7
|
|
|
16
8
|
// Module to keep secrets.
|
|
17
9
|
require('dotenv').config();
|
|
10
|
+
// Module to perform common actions.
|
|
11
|
+
const {alphaNumOf, dateOf, getRandomString, getNowStamp} = require('./procs/util');
|
|
18
12
|
|
|
19
13
|
// ########## CONSTANTS
|
|
20
14
|
|
|
21
|
-
// Standard requester.
|
|
22
|
-
const stdRequester = process.env.REQUESTER || 'nobody@nodomain.tld';
|
|
23
|
-
// Length of the random part of a job ID, as a string.
|
|
24
|
-
const randomIDLength = process.env.RANDOM_ID_LENGTH || '3';
|
|
25
15
|
// Tools that alter the page.
|
|
26
16
|
const contaminantNames = new Set([
|
|
27
17
|
'alfa',
|
|
28
18
|
'aslint',
|
|
29
19
|
'axe',
|
|
20
|
+
'ed11y',
|
|
30
21
|
'htmlcs',
|
|
31
22
|
'testaro'
|
|
32
23
|
]);
|
|
33
|
-
const randomIDChars = (() => {
|
|
34
|
-
const digits = Array(10).fill('').map((digit, index) => index.toString());
|
|
35
|
-
const uppers = Array(26).fill('').map((letter, index) => String.fromCodePoint(65 + index));
|
|
36
|
-
const lowers = Array(26).fill('').map((letter, index) => String.fromCodePoint(97 + index));
|
|
37
|
-
return digits.concat(uppers, lowers);
|
|
38
|
-
})();
|
|
39
24
|
|
|
40
25
|
|
|
41
26
|
// ########## FUNCTIONS
|
|
42
27
|
|
|
43
|
-
// Inserts a character periodically in a string.
|
|
44
|
-
const punctuate = (string, insertion, chunkSize) => {
|
|
45
|
-
const segments = [];
|
|
46
|
-
let startIndex = 0;
|
|
47
|
-
while (startIndex < string.length) {
|
|
48
|
-
segments.push(string.slice(startIndex, startIndex + chunkSize));
|
|
49
|
-
startIndex += chunkSize;
|
|
50
|
-
}
|
|
51
|
-
return segments.join(insertion);
|
|
52
|
-
};
|
|
53
|
-
// Converts a compact timestamp to a date.
|
|
54
|
-
const dateOf = timeStamp => {
|
|
55
|
-
if (/^\d{6}T\d{4}$/.test(timeStamp)) {
|
|
56
|
-
const dateString = punctuate(timeStamp.slice(0, 6), '-', 2);
|
|
57
|
-
const timeString = punctuate(timeStamp.slice(7, 11), ':', 2);
|
|
58
|
-
return new Date(`20${dateString}T${timeString}Z`);
|
|
59
|
-
} else {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
// Converts a date and time to a compact timestamp.
|
|
64
|
-
const stampTime = date => date.toISOString().replace(/[-:]/g, '').slice(2, 13);
|
|
65
|
-
// Generates a random string.
|
|
66
|
-
const getRandomID = length => {
|
|
67
|
-
const chars = [];
|
|
68
|
-
for (let i = 0; i < length; i++) {
|
|
69
|
-
chars.push(randomIDChars[Math.floor(62 * Math.random())]);
|
|
70
|
-
}
|
|
71
|
-
return chars.join('');
|
|
72
|
-
};
|
|
73
28
|
// Merges a script and a batch and returns jobs.
|
|
74
|
-
exports.merge = (
|
|
75
|
-
|
|
76
|
-
) => {
|
|
77
|
-
if (isolate === 'false') {
|
|
78
|
-
isolate = false;
|
|
79
|
-
}
|
|
80
|
-
// If a timestamp was specified:
|
|
29
|
+
exports.merge = (script, batch, requester, timeStamp) => {
|
|
30
|
+
// If a time stamp was specified:
|
|
81
31
|
if (timeStamp) {
|
|
82
32
|
// If it is invalid:
|
|
83
33
|
if (! dateOf(timeStamp)) {
|
|
@@ -86,77 +36,77 @@ exports.merge = (
|
|
|
86
36
|
return [];
|
|
87
37
|
}
|
|
88
38
|
}
|
|
89
|
-
// Otherwise, i.e. if no
|
|
39
|
+
// Otherwise, i.e. if no time stamp was specified:
|
|
90
40
|
else {
|
|
91
41
|
// Create one for the job.
|
|
92
|
-
timeStamp =
|
|
42
|
+
timeStamp = getNowStamp();
|
|
93
43
|
}
|
|
94
|
-
//
|
|
95
|
-
requester ||= stdRequester;
|
|
96
|
-
// Create a creation-time description.
|
|
97
|
-
const creationTime = (new Date()).toISOString().slice(0, 16);
|
|
98
|
-
// Initialize a target-independent job.
|
|
44
|
+
// Initialize a job as a copy of the script.
|
|
99
45
|
const protoJob = JSON.parse(JSON.stringify(script));
|
|
100
|
-
//
|
|
101
|
-
protoJob.id = `${timeStamp}-${getRandomID(Number.parseInt(randomIDLength, 10))}-`;
|
|
102
|
-
// Add a sources property to the job.
|
|
46
|
+
// Add an initialized sources property to it.
|
|
103
47
|
protoJob.sources = {
|
|
104
48
|
script: script.id,
|
|
105
49
|
batch: batch.id,
|
|
106
50
|
target: {
|
|
107
51
|
id: '',
|
|
108
|
-
|
|
109
|
-
|
|
52
|
+
what: '',
|
|
53
|
+
which: ''
|
|
110
54
|
},
|
|
111
|
-
requester
|
|
112
|
-
sendReportTo: process.env.REPORT_URL || '',
|
|
113
|
-
url: ''
|
|
55
|
+
requester
|
|
114
56
|
};
|
|
115
57
|
// Add properties to the job.
|
|
116
|
-
protoJob.
|
|
58
|
+
protoJob.creationTimeStamp = getNowStamp();
|
|
117
59
|
protoJob.timeStamp = timeStamp;
|
|
118
|
-
protoJob.standard = standard || 'only';
|
|
119
|
-
protoJob.observe = observe || false;
|
|
120
60
|
// If isolation was requested:
|
|
121
|
-
if (isolate) {
|
|
122
|
-
//
|
|
61
|
+
if (script.isolate) {
|
|
62
|
+
// For each act:
|
|
123
63
|
let {acts} = protoJob;
|
|
124
64
|
let lastPlaceholder = {};
|
|
125
65
|
for (const actIndexString in acts) {
|
|
66
|
+
// If it is a placeholder:
|
|
126
67
|
const actIndex = Number.parseInt(actIndexString);
|
|
127
68
|
const act = acts[actIndex];
|
|
128
|
-
const nextAct = acts[actIndex + 1];
|
|
129
69
|
if (act.type === 'placeholder') {
|
|
70
|
+
// Identify it as the current one.
|
|
130
71
|
lastPlaceholder = act;
|
|
131
72
|
}
|
|
73
|
+
// Otherwise, if it is a non-final target-modifying test act:
|
|
132
74
|
else if (
|
|
133
75
|
act.type === 'test'
|
|
134
76
|
&& contaminantNames.has(act.which)
|
|
135
77
|
&& actIndex < acts.length - 1
|
|
136
|
-
&& (nextAct.type === 'test')
|
|
137
78
|
) {
|
|
79
|
+
// Change it to an array of itself and the current placeholder.
|
|
138
80
|
acts[actIndex] = JSON.parse(JSON.stringify([act, lastPlaceholder]));
|
|
139
81
|
}
|
|
140
82
|
};
|
|
83
|
+
// Flatten the acts.
|
|
141
84
|
protoJob.acts = acts.flat();
|
|
142
85
|
}
|
|
86
|
+
// Delete the no-longer-necessary job property.
|
|
87
|
+
delete protoJob.isolate;
|
|
143
88
|
// Initialize an array of jobs.
|
|
144
89
|
const jobs = [];
|
|
90
|
+
// Get an ID for the merger.
|
|
91
|
+
const mergeID = getRandomString(2);
|
|
145
92
|
// For each target in the batch:
|
|
146
93
|
const {targets} = batch;
|
|
147
|
-
|
|
94
|
+
targets.forEach((target, index) => {
|
|
148
95
|
// If the target has the required identifiers:
|
|
149
|
-
const {id,
|
|
150
|
-
if (id &&
|
|
96
|
+
const {id, what, which} = target;
|
|
97
|
+
if (id && what && which) {
|
|
151
98
|
// Initialize a job.
|
|
152
99
|
const job = JSON.parse(JSON.stringify(protoJob));
|
|
153
|
-
//
|
|
154
|
-
|
|
100
|
+
// Make the job ID unique.
|
|
101
|
+
const targetID = alphaNumOf(index);
|
|
102
|
+
job.id = `${timeStamp}-${mergeID}-${targetID}`;
|
|
103
|
+
// Add properties to the job.
|
|
104
|
+
job.mergeID = mergeID;
|
|
105
|
+
job.sendReportTo = process.env.SEND_REPORT_TO || '';
|
|
155
106
|
// Add data to the sources property of the job.
|
|
156
|
-
job.sources.target.id =
|
|
157
|
-
job.sources.target.which = target.which;
|
|
107
|
+
job.sources.target.id = targetID;
|
|
158
108
|
job.sources.target.what = target.what;
|
|
159
|
-
job.sources.
|
|
109
|
+
job.sources.target.which = target.which;
|
|
160
110
|
// Replace each placeholder object in the job with the named replacer array of the target.
|
|
161
111
|
let {acts} = job;
|
|
162
112
|
for (const actIndex in acts) {
|
|
@@ -184,13 +134,14 @@ exports.merge = (
|
|
|
184
134
|
}
|
|
185
135
|
}
|
|
186
136
|
}
|
|
137
|
+
// Flatten the acts.
|
|
187
138
|
job.acts = acts.flat();
|
|
188
139
|
// Append the job to the array of jobs.
|
|
189
140
|
jobs.push(job);
|
|
190
141
|
}
|
|
191
142
|
else {
|
|
192
|
-
console.log('ERROR: Target in batch missing id,
|
|
143
|
+
console.log('ERROR: Target in batch missing id, what, or which property');
|
|
193
144
|
}
|
|
194
|
-
};
|
|
145
|
+
});
|
|
195
146
|
return jobs;
|
|
196
147
|
};
|
package/package.json
CHANGED
package/procs/test.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Array of 62 alphanumeric characters.
|
|
2
|
+
const alphaNumChars = (() => {
|
|
3
|
+
const digits = Array(10).fill('').map((digit, index) => index.toString());
|
|
4
|
+
const uppers = Array(26).fill('').map((letter, index) => String.fromCodePoint(65 + index));
|
|
5
|
+
const lowers = Array(26).fill('').map((letter, index) => String.fromCodePoint(97 + index));
|
|
6
|
+
return digits.concat(uppers, lowers);
|
|
7
|
+
})();
|
|
8
|
+
|
|
9
|
+
// Returns an alphanumeric representation of an integer.
|
|
10
|
+
const alphaNumOf = num => {
|
|
11
|
+
let resultDigits = [];
|
|
12
|
+
while (num) {
|
|
13
|
+
const remainder = num % 62;
|
|
14
|
+
resultDigits.unshift(alphaNumChars[remainder]);
|
|
15
|
+
num = Math.floor(num / 62);
|
|
16
|
+
}
|
|
17
|
+
return resultDigits.join('');
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
console.log(alphaNumOf(process.argv[2]));
|
package/procs/util.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*
|
|
2
|
+
util.js
|
|
3
|
+
Utility functions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// CONSTANTS
|
|
7
|
+
|
|
8
|
+
// Array of 62 alphanumeric characters.
|
|
9
|
+
const alphaNumChars = (() => {
|
|
10
|
+
const digits = Array(10).fill('').map((digit, index) => index.toString());
|
|
11
|
+
const uppers = Array(26).fill('').map((letter, index) => String.fromCodePoint(65 + index));
|
|
12
|
+
const lowers = Array(26).fill('').map((letter, index) => String.fromCodePoint(97 + index));
|
|
13
|
+
return digits.concat(uppers, lowers);
|
|
14
|
+
})();
|
|
15
|
+
|
|
16
|
+
// FUNCTIONS
|
|
17
|
+
|
|
18
|
+
// Returns a string representing a date and time.
|
|
19
|
+
const getTimeString = date => date.toISOString().slice(0, 19);
|
|
20
|
+
// Returns a string representing the date and time.
|
|
21
|
+
exports.getNowString = () => getTimeString(new Date());
|
|
22
|
+
// Returns a time stamp representing a date and time.
|
|
23
|
+
const getTimeStamp = date => getTimeString(date).replace(/[-:]/g, '').slice(2, 13);
|
|
24
|
+
// Returns a time stamp representing the date and time.
|
|
25
|
+
exports.getNowStamp = () => getTimeStamp(new Date());
|
|
26
|
+
// Inserts a character periodically in a string.
|
|
27
|
+
const punctuate = (string, insertion, chunkSize) => {
|
|
28
|
+
const segments = [];
|
|
29
|
+
let startIndex = 0;
|
|
30
|
+
while (startIndex < string.length) {
|
|
31
|
+
segments.push(string.slice(startIndex, startIndex + chunkSize));
|
|
32
|
+
startIndex += chunkSize;
|
|
33
|
+
}
|
|
34
|
+
return segments.join(insertion);
|
|
35
|
+
};
|
|
36
|
+
// Converts a compact timestamp to a date.
|
|
37
|
+
exports.dateOf = timeStamp => {
|
|
38
|
+
if (/^\d{6}T\d{4}$/.test(timeStamp)) {
|
|
39
|
+
const dateString = punctuate(timeStamp.slice(0, 6), '-', 2);
|
|
40
|
+
const timeString = punctuate(timeStamp.slice(7, 11), ':', 2);
|
|
41
|
+
return new Date(`20${dateString}T${timeString}Z`);
|
|
42
|
+
} else {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
// Returns a base-62 alphanumeric representation of an integer.
|
|
47
|
+
exports.alphaNumOf = num => {
|
|
48
|
+
let resultDigits = [];
|
|
49
|
+
while (num || ! resultDigits.length) {
|
|
50
|
+
const remainder = num % 62;
|
|
51
|
+
resultDigits.unshift(alphaNumChars[remainder]);
|
|
52
|
+
num = Math.floor(num / 62);
|
|
53
|
+
}
|
|
54
|
+
return resultDigits.join('');
|
|
55
|
+
};
|
|
56
|
+
// Returns a random string.
|
|
57
|
+
exports.getRandomString = length => {
|
|
58
|
+
const chars = [];
|
|
59
|
+
for (let i = 0; i < length; i++) {
|
|
60
|
+
chars.push(alphaNumChars[Math.floor(62 * Math.random())]);
|
|
61
|
+
}
|
|
62
|
+
return chars.join('');
|
|
63
|
+
};
|
package/script.js
CHANGED
|
@@ -73,7 +73,6 @@ exports.script = (id, issues = null, ... issueIDs) => {
|
|
|
73
73
|
timeLimit: 30 + (10 * issueIDs.length || 30 * toolIDs.length),
|
|
74
74
|
standard: 'only',
|
|
75
75
|
observe: true,
|
|
76
|
-
timeStamp: '',
|
|
77
76
|
acts: [
|
|
78
77
|
{
|
|
79
78
|
"type": "placeholder",
|
|
@@ -99,7 +98,7 @@ exports.script = (id, issues = null, ... issueIDs) => {
|
|
|
99
98
|
toolAct.rules.unshift('y');
|
|
100
99
|
}
|
|
101
100
|
}
|
|
102
|
-
// Add any needed option
|
|
101
|
+
// Add any needed option defaults to the act.
|
|
103
102
|
if (toolID === 'axe') {
|
|
104
103
|
toolAct.detailLevel = 2;
|
|
105
104
|
}
|