testaro 28.1.8 → 28.2.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 +2 -3
- package/call.js +2 -2
- package/package.json +1 -1
- package/procs/nav.js +0 -1
- package/procs/standardize.js +119 -101
- package/run.js +33 -9
- package/tests/testaro.js +12 -10
- package/validation/executors/watchDir.js +0 -1
- package/validation/executors/watchNet.js +2 -4
- package/watch.js +133 -74
package/README.md
CHANGED
|
@@ -826,13 +826,12 @@ You may store environment variables in an untracked `.env` file if you wish, and
|
|
|
826
826
|
```conf
|
|
827
827
|
URL_INJECT=yes
|
|
828
828
|
WAVE_KEY=yourwavekey
|
|
829
|
-
|
|
830
|
-
JOB_URL=yourserver.tld/job
|
|
831
|
-
REPORT_URL=yourserver.tld/report
|
|
829
|
+
JOB_URLs=https://yourserver.tld/job+http://localhost:3004/testapp
|
|
832
830
|
JOBDIR=../testing/jobs/ThisWorkstation
|
|
833
831
|
REPORTDIR=../testing/reports
|
|
834
832
|
AGENT=ThisWorkstation
|
|
835
833
|
DEBUG=false
|
|
834
|
+
WAITS=0
|
|
836
835
|
```
|
|
837
836
|
|
|
838
837
|
## Validation
|
package/call.js
CHANGED
|
@@ -70,14 +70,14 @@ const callWatch = async (isDirWatch, isForever, interval, watchee = null) => {
|
|
|
70
70
|
if (fn === 'run' && fnArgs.length === 1) {
|
|
71
71
|
callRun(fnArgs)
|
|
72
72
|
.then(() => {
|
|
73
|
-
console.log('Execution completed');
|
|
73
|
+
console.log('Execution completed\n');
|
|
74
74
|
process.exit(0);
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
else if (fn === 'watch' && fnArgs.length === 3) {
|
|
78
78
|
callWatch(... fnArgs)
|
|
79
79
|
.then(() => {
|
|
80
|
-
console.log('Execution completed');
|
|
80
|
+
console.log('Execution completed\n');
|
|
81
81
|
process.exit(0);
|
|
82
82
|
});
|
|
83
83
|
}
|
package/package.json
CHANGED
package/procs/nav.js
CHANGED
package/procs/standardize.js
CHANGED
|
@@ -41,91 +41,91 @@ const getIdentifiers = code => {
|
|
|
41
41
|
};
|
|
42
42
|
// Specifies conversions of rule IDs of aslint based on what substrings.
|
|
43
43
|
const aslintData = {
|
|
44
|
-
'
|
|
45
|
-
['not needed', '
|
|
44
|
+
'misused_required_attribute': [
|
|
45
|
+
['not needed', 'misused_required_attributeR']
|
|
46
46
|
],
|
|
47
|
-
'
|
|
48
|
-
['associated', '
|
|
49
|
-
['tabindex', '
|
|
47
|
+
'accessible_svg': [
|
|
48
|
+
['associated', 'accessible_svgI'],
|
|
49
|
+
['tabindex', 'accessible_svgT']
|
|
50
50
|
],
|
|
51
|
-
'
|
|
52
|
-
['track', '
|
|
53
|
-
['alternative', '
|
|
54
|
-
['bgsound', '
|
|
51
|
+
'audio_alternative': [
|
|
52
|
+
['track', 'audio_alternativeT'],
|
|
53
|
+
['alternative', 'audio_alternativeA'],
|
|
54
|
+
['bgsound', 'audio_alternativeB']
|
|
55
55
|
],
|
|
56
|
-
'
|
|
57
|
-
['describedby', 'associated', '
|
|
58
|
-
['labeledby', 'associated', '
|
|
59
|
-
['caption', 'not been defined', '
|
|
60
|
-
['summary', 'empty', '
|
|
61
|
-
['describedby', 'empty', '
|
|
62
|
-
['labeledby', 'empty', '
|
|
63
|
-
['caption', 'no content', '
|
|
56
|
+
'table_missing_description': [
|
|
57
|
+
['describedby', 'associated', 'table_missing_descriptionDM'],
|
|
58
|
+
['labeledby', 'associated', 'table_missing_descriptionLM'],
|
|
59
|
+
['caption', 'not been defined', 'table_missing_descriptionC'],
|
|
60
|
+
['summary', 'empty', 'table_missing_descriptionS'],
|
|
61
|
+
['describedby', 'empty', 'table_missing_descriptionDE'],
|
|
62
|
+
['labeledby', 'empty', 'table_missing_descriptionLE'],
|
|
63
|
+
['caption', 'no content', 'table_missing_descriptionE']
|
|
64
64
|
],
|
|
65
|
-
'
|
|
66
|
-
['only whice spaces', '
|
|
67
|
-
['more than one', '
|
|
65
|
+
'label_implicitly_associated': [
|
|
66
|
+
['only whice spaces', 'label_implicitly_associatedW'],
|
|
67
|
+
['more than one', 'label_implicitly_associatedM']
|
|
68
68
|
],
|
|
69
|
-
'
|
|
70
|
-
['Missing', '
|
|
71
|
-
['non-form', '
|
|
69
|
+
'label_inappropriate_association': [
|
|
70
|
+
['Missing', 'label_inappropriate_associationM'],
|
|
71
|
+
['non-form', 'label_inappropriate_associationN']
|
|
72
72
|
],
|
|
73
|
-
'
|
|
74
|
-
['headers', '
|
|
75
|
-
['Content', '
|
|
76
|
-
['head of the columns', '
|
|
73
|
+
'table_row_and_column_headers': [
|
|
74
|
+
['headers', 'table_row_and_column_headersRC'],
|
|
75
|
+
['Content', 'table_row_and_column_headersB'],
|
|
76
|
+
['head of the columns', 'table_row_and_column_headersH']
|
|
77
77
|
],
|
|
78
|
-
'
|
|
79
|
-
['position: fixed', '
|
|
80
|
-
['transparent', '
|
|
81
|
-
['least 3:1', '
|
|
82
|
-
['least 4.5:1', '
|
|
78
|
+
'color_contrast_state_pseudo_classes_abstract': [
|
|
79
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
80
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
81
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
82
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
83
83
|
],
|
|
84
|
-
'
|
|
85
|
-
['position: fixed', '
|
|
86
|
-
['transparent', '
|
|
87
|
-
['least 3:1', '
|
|
88
|
-
['least 4.5:1', '
|
|
84
|
+
'color_contrast_state_pseudo_classes_active': [
|
|
85
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
86
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
87
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
88
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
89
89
|
],
|
|
90
|
-
'
|
|
91
|
-
['position: fixed', '
|
|
92
|
-
['transparent', '
|
|
93
|
-
['least 3:1', '
|
|
94
|
-
['least 4.5:1', '
|
|
90
|
+
'color_contrast_state_pseudo_classes_focus': [
|
|
91
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
92
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
93
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
94
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
95
95
|
],
|
|
96
|
-
'
|
|
97
|
-
['position: fixed', '
|
|
98
|
-
['transparent', '
|
|
99
|
-
['least 3:1', '
|
|
100
|
-
['least 4.5:1', '
|
|
96
|
+
'color_contrast_state_pseudo_classes_hover': [
|
|
97
|
+
['position: fixed', 'color_contrast_state_pseudo_classes_abstractF'],
|
|
98
|
+
['transparent', 'color_contrast_state_pseudo_classes_abstractB'],
|
|
99
|
+
['least 3:1', 'color_contrast_state_pseudo_classes_abstract3'],
|
|
100
|
+
['least 4.5:1', 'color_contrast_state_pseudo_classes_abstract4']
|
|
101
101
|
],
|
|
102
|
-
'
|
|
103
|
-
['transparent', '
|
|
104
|
-
['least 4.5:1', '
|
|
105
|
-
['least 7:1', '
|
|
102
|
+
'color_contrast_aaa': [
|
|
103
|
+
['transparent', 'color_contrast_aaaB'],
|
|
104
|
+
['least 4.5:1', 'color_contrast_aaa4'],
|
|
105
|
+
['least 7:1', 'color_contrast_aaa7']
|
|
106
106
|
],
|
|
107
107
|
'animation': [
|
|
108
108
|
['duration', 'animationD'],
|
|
109
109
|
['iteration', 'animationI'],
|
|
110
110
|
['mechanism', 'animationM']
|
|
111
111
|
],
|
|
112
|
-
'
|
|
113
|
-
['empty', '
|
|
114
|
-
['not identify', '
|
|
112
|
+
'page_title': [
|
|
113
|
+
['empty', 'page_titleN'],
|
|
114
|
+
['not identify', 'page_titleU']
|
|
115
115
|
],
|
|
116
|
-
'
|
|
117
|
-
['exist', '
|
|
118
|
-
['empty', '
|
|
116
|
+
'aria_labelledby_association': [
|
|
117
|
+
['exist', 'aria_labelledby_associationN'],
|
|
118
|
+
['empty', 'aria_labelledby_associationE']
|
|
119
119
|
],
|
|
120
|
-
'
|
|
121
|
-
['parameters', '
|
|
122
|
-
['nothing', '
|
|
123
|
-
['empty', '
|
|
120
|
+
'html_lang_attr': [
|
|
121
|
+
['parameters', 'html_lang_attrP'],
|
|
122
|
+
['nothing', 'html_lang_attrN'],
|
|
123
|
+
['empty', 'html_lang_attrE']
|
|
124
124
|
],
|
|
125
|
-
'
|
|
126
|
-
['associated', '
|
|
127
|
-
['defined', '
|
|
128
|
-
['multiple labels', '
|
|
125
|
+
'missing_label': [
|
|
126
|
+
['associated', 'missing_labelI'],
|
|
127
|
+
['defined', 'missing_labelN'],
|
|
128
|
+
['multiple labels', 'missing_labelM']
|
|
129
129
|
],
|
|
130
130
|
'orientation': [
|
|
131
131
|
['loaded', 'orientationT']
|
|
@@ -353,47 +353,65 @@ const convert = (toolName, result, standardResult) => {
|
|
|
353
353
|
}
|
|
354
354
|
// aslint
|
|
355
355
|
else if (toolName === 'aslint' && result.summary && result.summary.byIssueType) {
|
|
356
|
+
// Get the totals.
|
|
356
357
|
standardResult.totals = [
|
|
357
358
|
result.summary.byIssueType.warning, 0, 0, result.summary.byIssueType.error
|
|
358
359
|
];
|
|
360
|
+
// For each rule:
|
|
359
361
|
Object.keys(result.rules).forEach(ruleID => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
362
|
+
// If it has a valid issue type:
|
|
363
|
+
const {issueType} = result.rules[ruleID];
|
|
364
|
+
if (issueType && ['warning', 'error'].includes(issueType)) {
|
|
365
|
+
// If there are any violations:
|
|
366
|
+
const ruleResults = result.rules[ruleID].results;
|
|
367
|
+
if (ruleResults && ruleResults.length) {
|
|
368
|
+
// For each violation:
|
|
369
|
+
ruleResults.forEach(ruleResult => {
|
|
370
|
+
// If it has a description:
|
|
371
|
+
if (
|
|
372
|
+
ruleResult.message
|
|
373
|
+
&& ruleResult.message.actual
|
|
374
|
+
&& ruleResult.message.actual.description
|
|
375
|
+
) {
|
|
376
|
+
|
|
377
|
+
const what = ruleResult.message.actual.description;
|
|
378
|
+
// Get its differentiated ID if any.
|
|
379
|
+
const ruleData = aslintData[ruleID];
|
|
380
|
+
let finalRuleID = ruleID;
|
|
381
|
+
if (ruleData) {
|
|
382
|
+
const changer = ruleData.find(
|
|
383
|
+
specs => specs.slice(0, -1).every(matcher => what.includes(matcher))
|
|
384
|
+
);
|
|
385
|
+
if (changer) {
|
|
386
|
+
finalRuleID = changer[changer.length - 1];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const xpath = ruleResult.element && ruleResult.element.xpath || '';
|
|
390
|
+
const tagName = xpath
|
|
391
|
+
&& xpath.replace(/^.*\//, '').replace(/[^-\w].*$/, '').toUpperCase()
|
|
392
|
+
|| '';
|
|
393
|
+
const excerpt = ruleResult.element && ruleResult.element.html || '';
|
|
394
|
+
const idDraft = excerpt && excerpt.replace(/^[^[>]+id="/, 'id=').replace(/".*$/, '');
|
|
395
|
+
const id = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
|
|
396
|
+
? idDraft.slice(3)
|
|
397
|
+
: '';
|
|
398
|
+
const instance = {
|
|
399
|
+
ruleID: finalRuleID,
|
|
400
|
+
what,
|
|
401
|
+
ordinalSeverity: ['warning', 0, 0, 'error'].indexOf(issueType),
|
|
402
|
+
tagName,
|
|
403
|
+
id,
|
|
404
|
+
location: {
|
|
405
|
+
doc: 'dom',
|
|
406
|
+
type: 'xpath',
|
|
407
|
+
spec: xpath
|
|
408
|
+
},
|
|
409
|
+
excerpt
|
|
410
|
+
};
|
|
411
|
+
standardResult.instances.push(instance);
|
|
380
412
|
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
ruleID,
|
|
384
|
-
what,
|
|
385
|
-
ordinalSeverity: ['warning', 0, 0, 'error'].indexOf(issueType),
|
|
386
|
-
tagName,
|
|
387
|
-
id,
|
|
388
|
-
location: {
|
|
389
|
-
doc: 'dom',
|
|
390
|
-
type: 'xpath',
|
|
391
|
-
spec: xpath
|
|
392
|
-
},
|
|
393
|
-
excerpt
|
|
394
|
-
};
|
|
395
|
-
standardResult.instances.push(instance);
|
|
396
|
-
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
397
415
|
}
|
|
398
416
|
});
|
|
399
417
|
}
|
package/run.js
CHANGED
|
@@ -13,6 +13,9 @@ const {actSpecs} = require('./actSpecs');
|
|
|
13
13
|
const {browserClose, getNonce, goTo, launch} = require('./procs/nav');
|
|
14
14
|
// Module to standardize report formats.
|
|
15
15
|
const {standardize} = require('./procs/standardize');
|
|
16
|
+
// HTTP and HTTPS clients.
|
|
17
|
+
const http = require('http');
|
|
18
|
+
const https = require('https');
|
|
16
19
|
|
|
17
20
|
// ########## CONSTANTS
|
|
18
21
|
|
|
@@ -45,8 +48,10 @@ const tests = {
|
|
|
45
48
|
testaro: 'Testaro',
|
|
46
49
|
wave: 'WAVE',
|
|
47
50
|
};
|
|
48
|
-
//
|
|
49
|
-
const
|
|
51
|
+
// Observation items.
|
|
52
|
+
const httpClient = require('http');
|
|
53
|
+
const httpsClient = require('https');
|
|
54
|
+
const agent = process.env.AGENT;
|
|
50
55
|
|
|
51
56
|
// ########## VARIABLES
|
|
52
57
|
|
|
@@ -448,12 +453,6 @@ const doActs = async (report, actIndex, page) => {
|
|
|
448
453
|
// Report this.
|
|
449
454
|
console.log('ERROR: Job aborted');
|
|
450
455
|
};
|
|
451
|
-
process.on('message', message => {
|
|
452
|
-
if (message === 'interrupt') {
|
|
453
|
-
console.log('ERROR: Terminal interrupted doActs');
|
|
454
|
-
process.exit();
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
456
|
const {acts} = report;
|
|
458
457
|
// If any more acts are to be performed:
|
|
459
458
|
if (actIndex > -1 && actIndex < acts.length) {
|
|
@@ -471,7 +470,26 @@ const doActs = async (report, actIndex, page) => {
|
|
|
471
470
|
}
|
|
472
471
|
}
|
|
473
472
|
const whichSuffix = actInfo ? ` (${actInfo})` : '';
|
|
474
|
-
|
|
473
|
+
// If granular reporting has been specified:
|
|
474
|
+
let granularSuffix = '';
|
|
475
|
+
if (report.observe) {
|
|
476
|
+
// Notify the server of the act.
|
|
477
|
+
const observer = report.sources.sendReportTo.replace(/report$/, 'granular');
|
|
478
|
+
const whoParams = `agent=${agent}&jobID=${report.id || ''}`;
|
|
479
|
+
const actParams = `act=${act.type}&which=${act.which || ''}`;
|
|
480
|
+
const wholeURL = `${observer}?${whoParams}&${actParams}`;
|
|
481
|
+
const client = wholeURL.startsWith('https://') ? httpsClient : httpClient;
|
|
482
|
+
const request = client.request(wholeURL);
|
|
483
|
+
// If the notification threw an error:
|
|
484
|
+
request.on('error', error => {
|
|
485
|
+
// Report the error.
|
|
486
|
+
const errorMessage = `ERROR notifying the server of an act`;
|
|
487
|
+
console.log(`${errorMessage} (${error.message})`);
|
|
488
|
+
});
|
|
489
|
+
request.end();
|
|
490
|
+
granularSuffix = ' with notice to server';
|
|
491
|
+
console.log(`>>>> ${act.type}${whichSuffix}${granularSuffix}`);
|
|
492
|
+
}
|
|
475
493
|
// Increment the count of acts performed.
|
|
476
494
|
actCount++;
|
|
477
495
|
act.startTime = Date.now();
|
|
@@ -1266,6 +1284,12 @@ exports.doJob = async report => {
|
|
|
1266
1284
|
report.jobData.presses = 0;
|
|
1267
1285
|
report.jobData.amountRead = 0;
|
|
1268
1286
|
report.jobData.toolTimes = {};
|
|
1287
|
+
process.on('message', message => {
|
|
1288
|
+
if (message === 'interrupt') {
|
|
1289
|
+
console.log('ERROR: Terminal interrupted the job');
|
|
1290
|
+
process.exit();
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1269
1293
|
// Recursively perform the acts.
|
|
1270
1294
|
await doActs(report, 0, null);
|
|
1271
1295
|
// Add the end time and duration to the report.
|
package/tests/testaro.js
CHANGED
|
@@ -12,17 +12,25 @@ const fs = require('fs/promises');
|
|
|
12
12
|
|
|
13
13
|
// ######## CONSTANTS
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const futureEvalRules = {
|
|
16
16
|
adbID: 'elements with ambiguous or missing referenced descriptions',
|
|
17
|
+
altScheme: 'img elements with alt attributes having URLs as their entire values',
|
|
18
|
+
captionLoc: 'caption elements that are not first children of table elements',
|
|
19
|
+
datalistRef: 'elements with ambiguous or missing referenced datalist elements',
|
|
20
|
+
imageLink: 'links with image files as their destinations',
|
|
21
|
+
legendLoc: 'legend elements that are not first children of fieldset elements',
|
|
22
|
+
optRoleSel: 'Non-option elements with option roles that have no aria-selected attributes',
|
|
23
|
+
phOnly: 'input elements with placeholders but no accessible names',
|
|
24
|
+
secHeading: 'headings that violate the logical level order in their sectioning containers',
|
|
25
|
+
textSem: 'semantically vague elements i, b, and/or small',
|
|
26
|
+
};
|
|
27
|
+
const evalRules = {
|
|
17
28
|
allCaps: 'leaf elements with entirely upper-case text longer than 7 characters',
|
|
18
29
|
allHidden: 'page that is entirely or mostly hidden',
|
|
19
30
|
allSlanted: 'leaf elements with entirely italic or oblique text longer than 39 characters',
|
|
20
|
-
altScheme: 'img elements with alt attributes having URLs as their entire values',
|
|
21
31
|
autocomplete: 'name and email inputs without autocomplete attributes',
|
|
22
32
|
bulk: 'large count of visible elements',
|
|
23
33
|
buttonMenu: 'nonstandard keyboard navigation between items of button-controlled menus',
|
|
24
|
-
captionLoc: 'caption elements that are not first children of table elements',
|
|
25
|
-
datalistRef: 'elements with ambiguous or missing referenced datalist elements',
|
|
26
34
|
distortion: 'distorted text',
|
|
27
35
|
docType: 'document without a doctype property',
|
|
28
36
|
dupAtt: 'elements with duplicate attributes',
|
|
@@ -37,9 +45,7 @@ const evalRules = {
|
|
|
37
45
|
hover: 'hover-caused content changes',
|
|
38
46
|
hovInd: 'hover indication nonstandard',
|
|
39
47
|
hr: 'hr element instead of styles used for vertical segmentation',
|
|
40
|
-
imageLink: 'links with image files as their destinations',
|
|
41
48
|
labClash: 'labeling inconsistencies',
|
|
42
|
-
legendLoc: 'legend elements that are not first children of fieldset elements',
|
|
43
49
|
lineHeight: 'text with a line height less than 1.5 times its font size',
|
|
44
50
|
linkExt: 'links that automatically open new windows',
|
|
45
51
|
linkAmb: 'links with identical texts but different destinations',
|
|
@@ -51,16 +57,12 @@ const evalRules = {
|
|
|
51
57
|
motion: 'motion without user request',
|
|
52
58
|
nonTable: 'table elements used for layout',
|
|
53
59
|
opFoc: 'Operable elements that are not Tab-focusable',
|
|
54
|
-
optRoleSel: 'Non-option elements with option roles that have no aria-selected attributes',
|
|
55
|
-
phOnly: 'input elements with placeholders but no accessible names',
|
|
56
60
|
pseudoP: 'adjacent br elements suspected of nonsemantically simulating p elements',
|
|
57
61
|
radioSet: 'radio buttons not grouped into standard field sets',
|
|
58
62
|
role: 'invalid and native-replacing explicit roles',
|
|
59
|
-
secHeading: 'headings that violate the logical level order in their sectioning containers',
|
|
60
63
|
styleDiff: 'style inconsistencies',
|
|
61
64
|
tabNav: 'nonstandard keyboard navigation between elements with the tab role',
|
|
62
65
|
targetSize: 'buttons, inputs, and non-inline links smaller than 44 pixels wide and high',
|
|
63
|
-
textSem: 'semantically vague elements i, b, and/or small',
|
|
64
66
|
titledEl: 'title attributes on inappropriate elements',
|
|
65
67
|
zIndex: 'non-default Z indexes'
|
|
66
68
|
};
|
|
@@ -8,7 +8,6 @@ const fs = require('fs/promises');
|
|
|
8
8
|
// CONSTANTS
|
|
9
9
|
|
|
10
10
|
// Override cycle environment variables with validation-specific ones.
|
|
11
|
-
process.env.PROTOCOL = 'http';
|
|
12
11
|
process.env.JOBDIR = `${__dirname}/../watch`;
|
|
13
12
|
process.env.REPORTDIR = `${__dirname}/../../temp`;
|
|
14
13
|
const jobID = '00000-simple-example';
|
|
@@ -8,13 +8,11 @@ const fs = require('fs/promises');
|
|
|
8
8
|
// CONSTANTS
|
|
9
9
|
|
|
10
10
|
// Override cycle environment variables with validation-specific ones.
|
|
11
|
-
process.env.PROTOCOL = 'http';
|
|
12
11
|
const jobDir = `${__dirname}/../jobs/todo`;
|
|
13
|
-
process.env.JOB_URL = 'localhost:3007/api/job';
|
|
14
|
-
process.env.REPORT_URL = 'localhost:3007/api';
|
|
12
|
+
process.env.JOB_URL = 'http://localhost:3007/api/job';
|
|
15
13
|
process.env.AGENT = 'testarauth';
|
|
16
14
|
const {cycle} = require('../../watch');
|
|
17
|
-
const client = require(
|
|
15
|
+
const client = require('http');
|
|
18
16
|
const jobID = '00000-simple-example';
|
|
19
17
|
|
|
20
18
|
// OPERATION
|
package/watch.js
CHANGED
|
@@ -11,15 +11,17 @@ require('dotenv').config();
|
|
|
11
11
|
const fs = require('fs/promises');
|
|
12
12
|
// Module to perform tests.
|
|
13
13
|
const {doJob} = require('./run');
|
|
14
|
+
// HTTP and HTTPS clients.
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const https = require('https');
|
|
14
17
|
|
|
15
18
|
// ########## CONSTANTS
|
|
16
19
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
+
const httpClient = require('http');
|
|
21
|
+
const httpsClient = require('https');
|
|
22
|
+
const jobURLs = process.env.JOB_URLs;
|
|
20
23
|
const agent = process.env.AGENT;
|
|
21
24
|
const jobDir = process.env.JOBDIR;
|
|
22
|
-
const reportURL = process.env.REPORT_URL;
|
|
23
25
|
const reportDir = process.env.REPORTDIR;
|
|
24
26
|
|
|
25
27
|
// ########## FUNCTIONS
|
|
@@ -55,48 +57,94 @@ const checkDirJob = async watchee => {
|
|
|
55
57
|
return {};
|
|
56
58
|
}
|
|
57
59
|
};
|
|
58
|
-
// Checks for and returns a network job.
|
|
60
|
+
// Checks for and, if obtained, returns a network job.
|
|
59
61
|
const checkNetJob = async watchee => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
let watchJobURLs = [watchee];
|
|
63
|
+
if (! watchJobURLs[0]) {
|
|
64
|
+
watchJobURLs = jobURLs
|
|
65
|
+
.split('+')
|
|
66
|
+
.map(url => [Math.random(), url])
|
|
67
|
+
.sort((a, b) => a[0] - b[0])
|
|
68
|
+
.map(pair => pair[1]);
|
|
69
|
+
}
|
|
70
|
+
// For each watchee:
|
|
71
|
+
for (const watchJobURL of watchJobURLs) {
|
|
72
|
+
const job = await new Promise(resolve => {
|
|
73
|
+
// Request a job from it.
|
|
74
|
+
const wholeURL = `${watchJobURL}?agent=${agent}`;
|
|
75
|
+
const client = wholeURL.startsWith('https://') ? httpsClient : httpClient;
|
|
76
|
+
const request = client.request(wholeURL, {timeout: 1000}, response => {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
response.on('data', chunk => {
|
|
79
|
+
chunks.push(chunk);
|
|
80
|
+
})
|
|
81
|
+
// When response arrives:
|
|
82
|
+
.on('end', () => {
|
|
83
|
+
// If the response was JSON-formatted:
|
|
84
|
+
try {
|
|
85
|
+
const jobJSON = chunks.join('');
|
|
86
|
+
const job = JSON.parse(jobJSON);
|
|
87
|
+
// Make it the response of the watchee.
|
|
88
|
+
return resolve(job);
|
|
89
|
+
}
|
|
90
|
+
// Otherwise, i.e. if the response was not JSON-formatted:
|
|
91
|
+
catch(error) {
|
|
92
|
+
// Make an error report the response of the watchee.
|
|
93
|
+
const errorMessage = `ERROR: Response of ${watchJobURL} was not JSON`;
|
|
94
|
+
console.log(errorMessage);
|
|
95
|
+
return resolve({
|
|
96
|
+
error: errorMessage,
|
|
97
|
+
message: error.message,
|
|
98
|
+
status: response.statusCode
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
.on('error', error => {
|
|
103
|
+
return resolve({
|
|
104
|
+
error: 'ERROR getting network job',
|
|
78
105
|
message: error.message,
|
|
79
106
|
status: response.statusCode
|
|
80
107
|
});
|
|
81
|
-
}
|
|
108
|
+
});
|
|
82
109
|
});
|
|
110
|
+
// If the check threw an error:
|
|
111
|
+
request.on('error', error => {
|
|
112
|
+
// Make an error report the response of the watchee.
|
|
113
|
+
const errorMessage = `ERROR checking ${watchJobURL} for a network job`;
|
|
114
|
+
console.log(`${errorMessage} (${error.message})`);
|
|
115
|
+
return resolve({
|
|
116
|
+
error: errorMessage,
|
|
117
|
+
message: error.message
|
|
118
|
+
});
|
|
119
|
+
})
|
|
120
|
+
.on('timeout', () => {
|
|
121
|
+
const errorMessage = `ERROR: Request to ${watchJobURL} timed out`;
|
|
122
|
+
console.log(errorMessage);
|
|
123
|
+
return resolve({
|
|
124
|
+
error: errorMessage
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
request.end();
|
|
83
128
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
// If the watchee sent a job:
|
|
130
|
+
if (job.id) {
|
|
131
|
+
// Stop checking and return it.
|
|
132
|
+
console.log(`Network job ${job.id} received from ${watchJobURL} (${nowString()})`);
|
|
133
|
+
return job;
|
|
134
|
+
}
|
|
135
|
+
// Otherwise, if the watchee sent a message:
|
|
136
|
+
else if (job.message) {
|
|
137
|
+
// Report it and continue checking.
|
|
138
|
+
console.log(job.message);
|
|
139
|
+
}
|
|
140
|
+
// Otherwise, i.e. if the watchee sent neither a job nor a message:
|
|
141
|
+
else {
|
|
142
|
+
// Report this and continue checking.
|
|
143
|
+
console.log(`No network job at ${watchJobURL} to do (${nowString()})`);
|
|
144
|
+
}
|
|
98
145
|
}
|
|
99
|
-
return
|
|
146
|
+
// If no watchee sent a job, return this.
|
|
147
|
+
return {};
|
|
100
148
|
};
|
|
101
149
|
// Writes a directory report.
|
|
102
150
|
const writeDirReport = async report => {
|
|
@@ -121,49 +169,60 @@ const writeDirReport = async report => {
|
|
|
121
169
|
const writeNetReport = async report => {
|
|
122
170
|
const ack = await new Promise(resolve => {
|
|
123
171
|
if (report.sources) {
|
|
124
|
-
//
|
|
172
|
+
// If the report specifies where to send it:
|
|
125
173
|
const destination = report.sources.sendReportTo;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
174
|
+
if (destination) {
|
|
175
|
+
// Send it.
|
|
176
|
+
const client = destination.startsWith('https://') ? https : http;
|
|
177
|
+
const request = client.request(destination, {method: 'POST'}, response => {
|
|
178
|
+
const chunks = [];
|
|
179
|
+
response.on('data', chunk => {
|
|
180
|
+
chunks.push(chunk);
|
|
181
|
+
});
|
|
182
|
+
response.on('end', () => {
|
|
183
|
+
const content = chunks.join('');
|
|
184
|
+
try {
|
|
185
|
+
resolve(JSON.parse(content));
|
|
186
|
+
}
|
|
187
|
+
catch(error) {
|
|
188
|
+
resolve({
|
|
189
|
+
error: 'ERROR: Response was not JSON',
|
|
190
|
+
message: error.message,
|
|
191
|
+
status: response.statusCode,
|
|
192
|
+
content: content.slice(0, 3000)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
146
196
|
});
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
report.jobData.agent = agent;
|
|
198
|
+
request.on('error', error => {
|
|
199
|
+
console.log(`ERROR submitting job report (${error.message})`);
|
|
200
|
+
resolve({
|
|
201
|
+
error: 'ERROR: Job report submission failed',
|
|
202
|
+
message: error.message
|
|
203
|
+
});
|
|
154
204
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
205
|
+
const reportJSON = JSON.stringify(report, null, 2);
|
|
206
|
+
request.end(reportJSON);
|
|
207
|
+
console.log(`Report ${report.id} submitted (${nowString()})`);
|
|
208
|
+
}
|
|
209
|
+
// Otherwise, i.e. if the report does not specify where to send it:
|
|
210
|
+
else {
|
|
211
|
+
// Report this.
|
|
212
|
+
console.log('ERROR: Report specifies no submission destination');
|
|
213
|
+
}
|
|
161
214
|
}
|
|
162
215
|
else {
|
|
163
216
|
console.log('ERROR: Report has no sources property');
|
|
164
217
|
}
|
|
165
218
|
});
|
|
166
219
|
// Return the server response.
|
|
220
|
+
if (ack) {
|
|
221
|
+
return ack.message || ack;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
167
226
|
return ack;
|
|
168
227
|
};
|
|
169
228
|
// Archives a job.
|
|
@@ -198,7 +257,7 @@ const runJob = async (job, isDirWatch) => {
|
|
|
198
257
|
else {
|
|
199
258
|
// Send the report to the server.
|
|
200
259
|
const ack = await writeNetReport(job);
|
|
201
|
-
console.log(
|
|
260
|
+
console.log(ack);
|
|
202
261
|
}
|
|
203
262
|
}
|
|
204
263
|
// If the job failed:
|