testilo 15.2.2 → 16.0.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
@@ -257,7 +257,7 @@ Suppose you ask for a merger of the above batch and script, **without** the isol
257
257
 
258
258
  ```javaScript
259
259
  {
260
- id: '7is3i-ts99-acme',
260
+ id: '231120T1550-ts99-acme',
261
261
  what: 'Axe on account page',
262
262
  strict: true,
263
263
  timeLimit: 60,
@@ -315,14 +315,14 @@ Suppose you ask for a merger of the above batch and script, **without** the isol
315
315
  requester: 'you@yourdomain.tld'
316
316
  },
317
317
  creationTime: '2023-11-20T15:50:27',
318
- timeStamp: '7is3i'
318
+ timeStamp: '231120T1550'
319
319
  }
320
320
  ```
321
321
 
322
322
  Testilo has substituted the `private` acts from the `acme` target of the batch for the placeholder when creating the job. Testilo also has:
323
323
  - let the script determine the browser type of the `launch` act.
324
- - added a unique timestamp to the job.
325
324
  - added the creation time to the job.
325
+ - added a unique timestamp to the job (a more compact representation of the creation time).
326
326
  - given the job an ID that combines the timestamp with the script ID and the batch ID.
327
327
  - inserted a `sources` property into the job, recording facts about the script, the batch, the target, and the email address given by the user who requested the merger.
328
328
 
@@ -360,24 +360,70 @@ A user can invoke `merge` in this way:
360
360
 
361
361
  - Create a script and save it as a JSON file in the `scripts` subdirectory of the `process.env.SPECDIR` directory.
362
362
  - Create a batch and save it as a JSON file in the `batches` subdirectory of the `process.env.SPECDIR` directory.
363
- - In the Testilo project directory, execute one of these statements:
364
- - `node call merge s b e true`
365
- - `node call merge s b e false`
366
- - `node call merge s b e`
363
+ - In the Testilo project directory, execute this statement:
364
+ - `node call merge s b e i t`
367
365
 
368
- In these statements, replace `s` and `b` with the base names of the script and batch files, respectively. For example, if the script file is named `ts99.json`, then replace `s` with `ts99`. Replace `e` with an email address, or with an empty string if the environment variable `process.env.REQUESTER` exists and you want to use it.
369
-
370
- The first statement will cause a merger **with** isolation.
371
- The second and third statements will cause a merger **without* isolation.
366
+ In these statements, replace:
367
+ - `s` with the base name of the script file
368
+ - `b` with the base name of the batch file
369
+ - `e` with an email address, or with an empty string if the environment variable `process.env.REQUESTER` exists and you want to use it
370
+ - `i` with `true` if you want test isolation or `false` if not
371
+ - `t` 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 `process.env.JOBDIR` directory.
372
372
 
373
373
  The `call` module will retrieve the named script and batch from their respective directories.
374
- The `merge` module will create an array of jobs.
375
- The `call` module will save the jobs as JSON files in the `todo` subdirectory of the `process.env.JOBDIR` directory.
374
+ The `merge` module will create an array of jobs, with or without test isolation.
375
+ The `call` module will save the jobs as JSON files in the `todo` or `pending` subdirectory of the `process.env.JOBDIR` directory.
376
376
 
377
377
  #### Validation
378
378
 
379
379
  To test the `merge` module, in the project directory you can execute the statement `node validation/merge/validate`. If `merge` is valid, all logging statements will begin with “Success” and none will begin with “ERROR”.
380
380
 
381
+ ### Series
382
+
383
+ If you want to monitor a web resource by performing identical jobs repeatedly and comparing the results, you can use the `series` module to create a series of identical jobs.
384
+
385
+ The jobs in a series differ from one another only in the timestamp segments of their `id` properties. For example, if the first job had the `id` value `240528T1316-mon-mozilla` and the events in the series occurred at intervals of 12 hours, then the second job would have the `id` value `240529T0116-mon-mozilla`.
386
+
387
+ The `series` module adds a `sources.series` property to each job in the series. The value of that property is the `id` value of the first job in the series.
388
+
389
+ To support monitoring, a server that receives job requests from testing agents can perform a time check on the first job in the queue. If the time specified by the `id` of the first job is in the future, the server can reply that there is no job to do.
390
+
391
+ #### Invocation
392
+
393
+ There are two ways to use the `series` module.
394
+
395
+ ##### By a module
396
+
397
+ A module can invoke `series` in this way:
398
+
399
+ ```javaScript
400
+ const {series} = require('testilo/series');
401
+ const jobs = series(job, count, interval);
402
+ ```
403
+
404
+ This invocation references a `job` variable, whose value is a job object. The `count` variable is an integer, 2 or greater, specifying how many events the series consists of. The `interval` variable is an integer, 1 or greater, specifying how many minutes are to elapse after each event before the next event. The `series()` function of the `series` module generates an array of job objects and returns the array. The invoking module can further dispose of the jobs as needed.
405
+
406
+ ##### By a user
407
+
408
+ A user can invoke `series` in this way:
409
+
410
+ - Create a job and save it as a JSON file in the `todo` subdirectory of the `process.env.JOBDIR` directory.
411
+ - In the Testilo project directory, execute this statement:
412
+ - `node call series j c i`
413
+
414
+ In this statement, replace:
415
+ - `j` with a string that the filename of the starting job begins with
416
+ - `c` with a count
417
+ - `i` with an interval in minutes
418
+
419
+ The `call` module will retrieve the first job that matches `j` from the `pending` subdirectory of the `process.env.JOBDIR` directory.
420
+ The `series` module will create an array of jobs.
421
+ The `call` module will save the jobs as JSON files in the `todo` subdirectory of the `process.env.JOBDIR` directory.
422
+
423
+ #### Validation
424
+
425
+ To test the `series` module, in the project directory you can execute the statement `node validation/series/validate`. If `series` is valid, all logging statements will begin with “Success” and none will begin with “ERROR”.
426
+
381
427
  ## Report scoring
382
428
 
383
429
  ### Introduction
package/call.js CHANGED
@@ -24,6 +24,8 @@ const {batch} = require('./batch');
24
24
  const {script} = require('./script');
25
25
  // Function to process a merger.
26
26
  const {merge} = require('./merge');
27
+ // Function to generate a job series.
28
+ const {series} = require('./series');
27
29
  // Function to score reports.
28
30
  const {score} = require('./score');
29
31
  // Function to digest reports.
@@ -68,7 +70,7 @@ const callScript = async (scriptID, classificationID = null, ... issueIDs) => {
68
70
  console.log(`Script ${scriptID} created and saved in ${specDir}/scripts`);
69
71
  };
70
72
  // Fulfills a merging request.
71
- const callMerge = async (scriptID, batchID, requester, withIsolation = false) => {
73
+ const callMerge = async (scriptID, batchID, requester, withIsolation, todo = true) => {
72
74
  // Get the script and the batch.
73
75
  const scriptJSON = await fs.readFile(`${specDir}/scripts/${scriptID}.json`, 'utf8');
74
76
  const script = JSON.parse(scriptJSON);
@@ -79,14 +81,38 @@ const callMerge = async (scriptID, batchID, requester, withIsolation = false) =>
79
81
  // Save the jobs.
80
82
  for (const job of jobs) {
81
83
  const jobJSON = JSON.stringify(job, null, 2);
82
- await fs.writeFile(`${jobDir}/todo/${job.id}.json`, jobJSON);
84
+ const destination = todo ? 'todo' : 'pending';
85
+ await fs.writeFile(`${jobDir}/${destination}/${job.id}.json`, jobJSON);
83
86
  }
84
87
  const {timeStamp} = jobs[0];
85
88
  console.log(
86
- `Script ${scriptID} and batch ${batchID} merged; jobs ${timeStamp} saved in ${jobDir}/todo`
89
+ `Script ${scriptID} and batch ${batchID} merged as ${timeStamp}-… in ${jobDir}/${destination}`
87
90
  );
88
91
  };
89
- // Get selected reports.
92
+ // Fulfills a series request.
93
+ const callSeries = async (idStart, count, interval) => {
94
+ // Get the initial job.
95
+ const jobNames = await fs.readdir(`${jobDir}/pending`);
96
+ const seriesJobName = jobNames.find(jobName => jobName.startsWith(idStart));
97
+ // If it exists:
98
+ if (seriesJobName) {
99
+ // Generate a job series.
100
+ const jobJSON = await fs.readFile(`${jobDir}/todo/${seriesJobName}`, 'utf8');
101
+ const job = JSON.parse(jobJSON);
102
+ const jobSeries = series(job, Number.parseInt(count), Number.parseInt(interval));
103
+ // Save the jobs.
104
+ for (const item of jobSeries) {
105
+ await fs.writeFile(`${jobDir}/todo/${item.id}.json`, JSON.stringify(item, null, 2));
106
+ }
107
+ console.log(`Series of ${jobSeries.length} jobs generated and saved in ${jobDir}/todo`);
108
+ }
109
+ // Otherwise, i.e. if it does not exist:
110
+ else {
111
+ // Report this.
112
+ console.log('ERROR: No matching to-do job found');
113
+ }
114
+ };
115
+ // Gets selected reports.
90
116
  const getReports = async (type, selector = '') => {
91
117
  const allFileNames = await fs.readdir(`${reportDir}/${type}`);
92
118
  const reportIDs = allFileNames
@@ -215,6 +241,12 @@ else if (fn === 'merge' && fnArgs.length > 2 && fnArgs.length < 5) {
215
241
  console.log('Execution completed');
216
242
  });
217
243
  }
244
+ else if (fn === 'series' && fnArgs.length === 3) {
245
+ callSeries(... fnArgs)
246
+ .then(() => {
247
+ console.log('Execution completed');
248
+ });
249
+ }
218
250
  else if (fn === 'score' && fnArgs.length > 0 && fnArgs.length < 3) {
219
251
  callScore(... fnArgs)
220
252
  .then(() => {
package/merge.js CHANGED
@@ -33,7 +33,8 @@ exports.merge = (script, batch, requester, isolate = false) => {
33
33
  // If the requester is unspecified, make it the standard requester.
34
34
  requester ||= stdRequester;
35
35
  // Create a timestamp.
36
- const timeStamp = Math.floor((Date.now() - Date.UTC(2023, 6)) / 2000).toString(36);
36
+ const now = new Date();
37
+ const timeStamp = now.toISOString().slice(2, 16).replace(/[-:]/g, '');
37
38
  // Create a time description.
38
39
  const creationTime = (new Date()).toISOString().slice(0, 19);
39
40
  // Initialize a target-independent job.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "15.2.2",
3
+ "version": "16.0.1",
4
4
  "description": "Prepares and processes Testaro reports",
5
5
  "main": "aim.js",
6
6
  "scripts": {
package/series.js ADDED
@@ -0,0 +1,72 @@
1
+ /*
2
+ series.js
3
+ Generates a series of Testaro jobs.
4
+ Arguments:
5
+ 0. Initial job.
6
+ 1. Job count.
7
+ 2. Time interval in minutes.
8
+ */
9
+
10
+ // ########## FUNCTIONS
11
+
12
+ // Scores the specified raw reports.
13
+ exports.series = (job, count, interval) => {
14
+ // If the arguments are valid:
15
+ if (
16
+ typeof job === 'object'
17
+ && count
18
+ && typeof count === 'number'
19
+ && count === Math.floor(count)
20
+ && count > 1
21
+ && interval
22
+ && typeof interval === 'number'
23
+ && interval === Math.floor(interval)
24
+ && interval > 0
25
+ ) {
26
+ // Get a copy of the initial job.
27
+ const template = JSON.parse(JSON.stringify(job));
28
+ // If it has an ID:
29
+ const jobID = template.id;
30
+ if (jobID) {
31
+ // If the ID specifies a valid time:
32
+ const s = jobID.slice(0, 11);
33
+ const dateSpec = `20${s[0]}${s[1]}-${s[2]}${s[3]}-${s[4]}${s[5]}`;
34
+ const timeSpec = `${s[7]}${s[8]}:${s[9]}${s[10]}`;
35
+ const dateTimeSpec = `${dateSpec}T${timeSpec}Z`;
36
+ const start = new Date(dateTimeSpec);
37
+ const startNum = start.valueOf();
38
+ if (startNum) {
39
+ // Initialize the series.
40
+ const series = [];
41
+ // For each job required:
42
+ for (let i = 0; i < count; i++) {
43
+ // Create it.
44
+ const nextJob = JSON.parse(JSON.stringify(template));
45
+ nextJob.sources.series = nextJob.id;
46
+ // Revise its ID.
47
+ const nextDate = new Date(startNum + i * interval * 60000);
48
+ const nextTimeStamp = nextDate.toISOString().slice(2, 16).replace(/[-:]/g, '');
49
+ nextJob.id = nextJob.id.replace(/^[^-]+/, nextTimeStamp);
50
+ // Add the job to the series.
51
+ series.push(nextJob);
52
+ }
53
+ return series;
54
+ }
55
+ // Otherwise, i.e. if it does not specify a valid time:
56
+ else {
57
+ // Report this.
58
+ console.log('ERROR: Initial job ID starts with an invalid time specification');
59
+ }
60
+ }
61
+ // Otherwise, i.e. if it has no ID:
62
+ else {
63
+ // Report this.
64
+ console.log('ERROR: Initial job has no ID');
65
+ }
66
+ }
67
+ // Otherwise, i.e. if they are invalid:
68
+ else {
69
+ // Report this.
70
+ console.log('ERROR: Arguments invalid');
71
+ }
72
+ };
@@ -0,0 +1,44 @@
1
+ {
2
+ "id": "240223T0815-mon-example",
3
+ "what": "Job for series validation",
4
+ "strict": true,
5
+ "timeLimit": 10,
6
+ "acts": [
7
+ {
8
+ "type": "launch",
9
+ "which": "chromium",
10
+ "what": "Chromium browser",
11
+ "startTime": 1662474496075,
12
+ "endTime": 1662474496453
13
+ },
14
+ {
15
+ "type": "url",
16
+ "which": "https://example.com",
17
+ "what": "Example of web page",
18
+ "startTime": 1662474496453,
19
+ "result": "https://example.com",
20
+ "endTime": 1662474501684
21
+ },
22
+ {
23
+ "type": "test",
24
+ "which": "testaro",
25
+ "url": "https://example.com",
26
+ "withItems": false,
27
+ "rules": [
28
+ "y",
29
+ "bulk"
30
+ ]
31
+ }
32
+ ],
33
+ "sources": {
34
+ "script": "mon",
35
+ "batch": "target",
36
+ "target": {
37
+ "id": "example",
38
+ "what": "Example of web page"
39
+ },
40
+ "requester": "user@domain.tld"
41
+ },
42
+ "creationTime": "2023-11-20T15:50:27",
43
+ "timeStamp": "240223T0815"
44
+ }
@@ -0,0 +1,78 @@
1
+ /*
2
+ validate.js
3
+ Validates series module.
4
+ */
5
+
6
+ // ########## IMPORTS
7
+
8
+ // Function to process files.
9
+ const fs = require('fs/promises');
10
+ // Function to generate a job series.
11
+ const {series} = require('../../series');
12
+
13
+ // ########## FUNCTIONS
14
+
15
+ // Validates the series module.
16
+ const validate = async () => {
17
+ // Get the job.
18
+ const jobJSON = await fs.readFile(`${__dirname}/job.json`, 'utf8');
19
+ const job = JSON.parse(jobJSON);
20
+ // Generate the series.
21
+ const jobs = series(job, 3, 5);
22
+ // Validate the series.
23
+ if (Array.isArray(jobs) && jobs.length === 3) {
24
+ console.log('Success: The count of jobs is correct');
25
+ }
26
+ else {
27
+ console.log('ERROR: The jobs are not an array of length 3');
28
+ return;
29
+ }
30
+ const job0 = jobs[0];
31
+ if (
32
+ job.id
33
+ && job.id === '240223T0815-mon-example'
34
+ && job0.id === job.id
35
+ && job0.sources
36
+ && job0.sources.series
37
+ && job0.sources.series === job.id
38
+ ) {
39
+ console.log('Success: The first job has the correct id and sources.series');
40
+ }
41
+ else {
42
+ console.log('ERROR: The first job has an incorrect id or sources.series');
43
+ return;
44
+ }
45
+ const job1 = jobs[1];
46
+ const job2 = jobs[2];
47
+ if (
48
+ job2.id
49
+ && job2.id === '240223T0825-mon-example'
50
+ && job2.sources
51
+ && job2.sources.series
52
+ && job2.sources.series === '240223T0815-mon-example'
53
+ ) {
54
+ console.log('Success: The third job has the correct id and sources.series');
55
+ }
56
+ else {
57
+ console.log('ERROR: The first job has an incorrect id or sources.series');
58
+ return;
59
+ }
60
+ if (
61
+ job1.acts
62
+ && job1.acts.length === 3
63
+ && job1.acts[2].rules
64
+ && job1.acts[2].rules.length === 2
65
+ && job2.acts
66
+ && job2.acts.length === 3
67
+ && job2.acts[2].rules
68
+ && job2.acts[2].rules.length === 2
69
+ && job2.acts[2].rules[1] === job1.acts[2].rules[1]
70
+ ) {
71
+ console.log('Success: The second and third jobs invoke the same rule');
72
+ }
73
+ else {
74
+ console.log('ERROR: The second and third job do not invoke the same rule');
75
+ return;
76
+ }
77
+ };
78
+ validate();