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 +59 -13
- package/call.js +36 -4
- package/merge.js +2 -1
- package/package.json +1 -1
- package/series.js +72 -0
- package/validation/series/job.json +44 -0
- package/validation/series/validate.js +78 -0
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: '
|
|
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: '
|
|
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
|
|
364
|
-
- `node call merge s b e
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
89
|
+
`Script ${scriptID} and batch ${batchID} merged as ${timeStamp}-… in ${jobDir}/${destination}`
|
|
87
90
|
);
|
|
88
91
|
};
|
|
89
|
-
//
|
|
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
|
|
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
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();
|