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 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 3 items:
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
- ['w3c', 'World Wide Web Consortium', 'https://www.w3.org/'],
78
- ['moz', 'Mozilla Foundation', 'https://foundation.mozilla.org/en/']
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
- w3c→World Wide Web Consortium→https://www.w3.org/
86
- moz→Mozilla Foundation→https://foundation.mozilla.org/en/
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(listID, what, targets);
214
+ const batchObj = batch(id, what, targets);
229
215
  ```
230
216
 
231
- This invocation references `listID`, `what`, and `targets` variables that the module must have already defined. The `listID` 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 3 items (ID, description, and URL) defining one target.
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: '2023-11-20T15:50:27'
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 9 arguments to create jobs from a script and a batch.
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, or an empty string if there is a `REQUESTER` environment variable to be used.
439
- - `isolate`: `true` if test isolation is to be performed, or `false` if not.
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 scriptName batchName requester isolate standard observe todoDir urlPrefix urlSuffix`.
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
- - `scriptName` with the base name of the script file.
460
- - `batchName` with the base name of the batch file.
461
- - `requester` with an email address, or with an empty string if the environment variable `REQUESTER` exists and you want to use it.
462
- - `isolate` with `true` if you want test isolation or `false` if not.
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, with or without test isolation.
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
- // ########## FUNCTIONS
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 valid target:
31
- targetList.forEach(target => {
32
- if (target.length === 3 && target.every(item => item.length)) {
33
- // Add it to the batch.
34
- batch.targets.push({
35
- id: target[0],
36
- which: target[2],
37
- what: target[1],
38
- acts: {
39
- main: [
40
- {
41
- type: 'launch',
42
- url: target[2],
43
- what: target[1]
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 (listID, what) => {
46
+ const callBatch = async (id, what) => {
47
47
  // Get the target list.
48
- const listString = await fs.readFile(`${specDir}/targetLists/${listID}.tsv`, 'utf8');
49
- const list = listString.split('\n').map(target => target.split('\t'));
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(listID, what, list);
54
+ const batchObj = batch(id, what, list);
52
55
  // Save the batch.
53
- const batchJSON = JSON.stringify(batchObj, null, 2);
54
- await fs.writeFile(`${specDir}/batches/${listID}.json`, `${batchJSON}\n`);
55
- console.log(`Target list ${listID} converted to a batch and saved in ${specDir}/batches`);
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
- withIsolation,
76
- standard,
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 destination = todo === 'true' ? 'todo' : 'pending';
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(`${jobDir}/${destination}/${job.id}.json`, `${jobJSON}\n`);
94
+ await fs.writeFile(`${subdir}/${job.id}.json`, `${jobJSON}\n`);
94
95
  }
95
- const {timeStamp} = jobs[0];
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 === 9) {
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
- script, batch, requester, isolate, standard, observe, timeStamp, urlPrefix, urlSuffix
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 timestamp was specified:
39
+ // Otherwise, i.e. if no time stamp was specified:
90
40
  else {
91
41
  // Create one for the job.
92
- timeStamp = stampTime(new Date());
42
+ timeStamp = getNowStamp();
93
43
  }
94
- // If the requester is blank or unspecified, make it the standard requester.
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
- // Make the timestamp and a random string the start of the job ID.
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
- which: '',
109
- what: ''
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.creationTime = creationTime;
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
- // Configure the job for it.
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
- for (const target of targets) {
94
+ targets.forEach((target, index) => {
148
95
  // If the target has the required identifiers:
149
- const {id, which, what} = target;
150
- if (id && which && what) {
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
- // Append the target ID to the job ID.
154
- job.id += target.id;
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 = 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.url = `${urlPrefix}${job.id}${urlSuffix}`;
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, which, or what property');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "23.0.0",
3
+ "version": "23.1.0",
4
4
  "description": "Prepares and processes Testaro reports",
5
5
  "main": "aim.js",
6
6
  "scripts": {
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 specifications to the act.
101
+ // Add any needed option defaults to the act.
103
102
  if (toolID === 'axe') {
104
103
  toolAct.detailLevel = 2;
105
104
  }