testaro 32.3.5 → 32.4.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
@@ -150,7 +150,7 @@ Here is an example of a job:
150
150
 
151
151
  ```javascript
152
152
  {
153
- id: 'be76p-ts25-w3c',
153
+ id: '241213T1200-ts25-w3c',
154
154
  what: 'Test W3C with 2 alfa rules',
155
155
  strict: true,
156
156
  timeLimit: 65,
@@ -178,8 +178,8 @@ Here is an example of a job:
178
178
  },
179
179
  requester: 'user@domain.org'
180
180
  },
181
- creationTime: '2023-05-26T14:28',
182
- timeStamp: 'be76p'
181
+ creationTime: '2024-12-10T14:28Z',
182
+ timeStamp: '241213T1200'
183
183
  }
184
184
  ```
185
185
 
@@ -198,8 +198,8 @@ Job properties:
198
198
  - `batch` (optional): a set of targets (URLs) from which the target of this job was drawn.
199
199
  - `target` (optional): an object describing the target being tested by this job.
200
200
  - `requester` (optional): the email address that should receive a notice of completion of the job.
201
- - `creationTime`: the time when the job was created.
202
- - `timeStamp`: a string unique to this job.
201
+ - `creationTime`: the time in ISO 8601 format when the job was created.
202
+ - `timeStamp`: a string representing the date and time before which the job is not to be performed.
203
203
 
204
204
  ### Reports
205
205
 
@@ -595,7 +595,7 @@ When no string pertains to a module, then QualWeb will test for all of the rules
595
595
 
596
596
  Thus, when the `rules` argument is omitted, QualWeb will test for all of the rules in all of these modules.
597
597
 
598
- **Notice**: As of 2023-09-02, any attempt to perform best-practices tests of QualWeb caused QualWeb to throw an error. This issue was [reported to QualWeb](https://github.com/qualweb/core/issues/29). Until this issue is resolved, Testaro does not try to run any QualWeb best-practices tests, regardless of the value of the `rules` argument.
598
+ The target can be provided to QualWeb either as an existing page or as a URL. Experience indicates that the results can differ between these methods, with each method reporting some rule violations or some instances that the other method does not report.
599
599
 
600
600
  ###### Testaro
601
601
 
@@ -774,7 +774,7 @@ Testaro can watch for a job in a directory, with the `dirWatch` function, which
774
774
  ###### By a module
775
775
 
776
776
  ```javaScript
777
- const {dirWatch} = require('./watch');
777
+ const {dirWatch} = require('./dirWatch');
778
778
  dirWatch(true, 300);
779
779
  ```
780
780
 
@@ -786,20 +786,12 @@ Testaro creates a report for each job and saves the report in the directory spec
786
786
 
787
787
  ###### By a user
788
788
 
789
- A user can choose between two methods:
790
-
791
789
  ```javaScript
792
790
  node call dirWatch true 300
793
791
  ```
794
792
 
795
- ```javaScript
796
- node dirWatch true 300
797
- ```
798
-
799
793
  The arguments and behaviors described above for execution by a module apply here, too.
800
794
 
801
- The second, shorter method spawns a new watch subprocess after each job performance, to decrease the risk of process corruption involving bogus timeout messages from Playwright during jobs. That method requires you to enter `CTRL-c` to stop the watching.
802
-
803
795
  ##### Network watch
804
796
 
805
797
  Testaro can poll servers for jobs to be performed.
@@ -808,21 +800,27 @@ An instance of Testaro is an _agent_ and has an identifier specified by `process
808
800
 
809
801
  The URLs polled by Testaro are specified by `process.env.JOB_URLS`. The format of that environment variable is a `+`-delimited list of URLs, including schemes. If one of the URLs is `https://testrunner.org/a11ytest/api/job`, and if a Testaro instance has the agent ID `tester3`, then a job request is a `GET` request to `https://testrunner.org/a11ytest/api/job?agent=tester3`.
810
802
 
811
- Once a Testaro instance obtains a network job, the report is sent in a `POST` request to the URL specified by the `sources.sendReportTo` property of the job.
803
+ Once a Testaro instance obtains a network job, Testaro performs it and adds the result data to the job, which then becomes the job report. Testaro sends the report in a `POST` request to the URL specified by the `sources.sendReportTo` property of the job.
804
+
805
+ Network watching can be repeated or 1-job. One-job watching stops after 1 job has been performed.
806
+
807
+ After checking all the URLs in succession without getting a job from any of them, Testaro waits for a prescribed time before continuing to check.
812
808
 
813
809
  ###### By a module
814
810
 
815
811
  ```javaScript
816
- const {netWatch} = require('./watch');
817
- netWatch(true, 300);
812
+ const {netWatch} = require('./netWatch');
813
+ netWatch(true, 300, true);
818
814
  ```
819
815
 
820
816
  In this example, a module asks Testaro to check the servers for a job every 300 seconds, to perform any jobs obtained from the servers, and then to continue checking until the process is stopped. If the first argument is `false`, Testaro will stop checking after performing 1 job.
821
817
 
818
+ The third argument specifies whether Testaro should be certificate-tolerant. A `true` value makes Testaro accept SSL certificates that fail verification against a list of certificate authorities. This allows testing of `https` targets that, for example, use self-signed certificates. If the third argument is omitted, the default for that argument is implemented. The default is `true`.
819
+
822
820
  ###### By a user
823
821
 
824
822
  ```javaScript
825
- node call netWatch true 300
823
+ node call netWatch true 300 true
826
824
  ```
827
825
 
828
826
  The arguments and behaviors described above for execution by a module apply here, too. If the first argument is `true`, you can terminate the process by entering `CTRL-c`.
package/actSpecs.js CHANGED
@@ -145,7 +145,7 @@ exports.actSpecs = {
145
145
  'Perform tests of a tool',
146
146
  {
147
147
  which: [true, 'string', 'isTest', 'tool name'],
148
- rules: [false, 'array', 'areStrings', 'rule IDs or specifications, if not all']
148
+ rules: [false, 'array', 'areStrings', 'rule IDs or (for nuVal) specifications, if not all']
149
149
  }
150
150
  ],
151
151
  text: [
package/call.js CHANGED
@@ -41,7 +41,8 @@ const fs = require('fs/promises');
41
41
  // Function to process a testing request.
42
42
  const {doJob} = require('./run');
43
43
  // Function to watch for jobs.
44
- const {dirWatch, netWatch} = require('./watch');
44
+ const {dirWatch} = require('./dirWatch');
45
+ const {netWatch} = require('./netWatch');
45
46
 
46
47
  // CONSTANTS
47
48
 
@@ -81,12 +82,17 @@ const callRun = async jobIDStart => {
81
82
  }
82
83
  };
83
84
  // Starts a directory watch, converting the interval argument to a number.
84
- const callDirWatch = async (isForever, interval) => {
85
- await dirWatch(isForever === 'true', Math.max(5, Number.parseInt(interval, 10)));
85
+ const callDirWatch = async (isForever, intervalInSeconds) => {
86
+ await dirWatch(isForever === 'true', Math.max(5, Number.parseInt(intervalInSeconds, 10)));
86
87
  };
87
88
  // Starts a network watch, converting the interval argument to a number.
88
- const callNetWatch = async(isForever, interval) => {
89
- netWatch(isForever === 'true', Number.parseInt(interval, 10));
89
+ const callNetWatch = async (isForever, intervalInSeconds, isCertTolerant) => {
90
+ await netWatch(
91
+ isForever === 'true',
92
+ Number.parseInt(intervalInSeconds, 10),
93
+ isCertTolerant ? isCertTolerant === 'true' : undefined
94
+ );
95
+ console.log('netWatch run');
90
96
  };
91
97
 
92
98
  // OPERATION
@@ -100,11 +106,20 @@ if (fn === 'run' && fnArgs.length < 2) {
100
106
  });
101
107
  }
102
108
  else if (fn === 'dirWatch' && fnArgs.length === 2) {
103
- callDirWatch(... fnArgs);
109
+ callDirWatch(... fnArgs)
110
+ .then(() => {
111
+ console.log('Directory watch ended');
112
+ process.exit(0);
113
+ });
104
114
  }
105
- else if (fn === 'netWatch' && fnArgs.length === 2) {
106
- callNetWatch(... fnArgs);
115
+ else if (fn === 'netWatch' && [2, 3].includes(fnArgs.length)) {
116
+ callNetWatch(... fnArgs)
117
+ .then(() => {
118
+ console.log('Network watch ended');
119
+ process.exit(0);
120
+ });
107
121
  }
108
122
  else {
109
123
  console.log('ERROR: Invalid statement');
124
+ process.exit(1);
110
125
  }
package/dirWatch.js CHANGED
@@ -22,48 +22,137 @@
22
22
 
23
23
  /*
24
24
  dirWatch.js
25
- Module for launching a repeated one-time directory watch.
25
+ Module for watching a directory for jobs.
26
26
  */
27
27
 
28
28
  // ########## IMPORTS
29
29
 
30
- // Module to spawn a child process.
31
- const {spawn} = require('node:child_process');
30
+ // Module to keep secrets.
31
+ require('dotenv').config();
32
+ // Module to read and write files.
33
+ const fs = require('fs/promises');
34
+ // Module to perform jobs.
35
+ const {doJob} = require('./run');
32
36
 
33
37
  // ########## CONSTANTS
34
38
 
35
- const interval = process.argv[2];
39
+ const jobDir = process.env.JOBDIR;
40
+ const reportDir = process.env.REPORTDIR;
36
41
 
37
42
  // ########## FUNCTIONS
38
43
 
39
- // Spawns a one-time directory watch.
40
- const spawnWatch = (command, args) => spawn(command, args, {stdio: ['inherit', 'inherit', 'pipe']});
41
- // Repeatedly spawns a one-time directory watch.
42
- const reWatch = () => {
43
- const watcher = spawnWatch('node', ['call', 'watch', 'false', interval]);
44
- let error = '';
45
- watcher.stderr.on('data', data => {
46
- error += data.toString();
44
+ // Gets a segment of a timestamp.
45
+ const tsPart = (timeStamp, startIndex) => timeStamp.slice(startIndex, startIndex + 2);
46
+ // Returns a string representing the date and time.
47
+ const nowString = () => (new Date()).toISOString().slice(0, 16);
48
+ // Gets date of a timestamp.
49
+ const dateOf = ts => {
50
+ const dateString = `20${tsPart(ts, 0)}-${tsPart(ts, 2)}-${tsPart(ts, 4)}`;
51
+ const timeString = `${tsPart(ts, 7)}:${tsPart(ts, 9)}:00`;
52
+ const dateTimeString = `${dateString}T${timeString}Z`;
53
+ return new Date(dateTimeString);
54
+ };
55
+ // Writes a directory report.
56
+ const writeDirReport = async report => {
57
+ const jobID = report && report.id;
58
+ if (jobID) {
59
+ try {
60
+ const reportJSON = JSON.stringify(report, null, 2);
61
+ const reportName = `${jobID}.json`;
62
+ await fs.mkdir(`${reportDir}/raw`, {recursive: true});
63
+ await fs.writeFile(`${reportDir}/raw/${reportName}`, `${reportJSON}\n`);
64
+ console.log(`Report ${jobID} saved in ${reportDir}/raw`);
65
+ }
66
+ catch(error) {
67
+ console.log(`ERROR: Failed to save report ${jobID} in ${reportDir}/raw (${error.message})`);
68
+ }
69
+ }
70
+ else {
71
+ console.log('ERROR: Job has no ID');
72
+ }
73
+ };
74
+ // Archives a job.
75
+ const archiveJob = async (job, isFile) => {
76
+ // Save the job in the done subdirectory.
77
+ const {id} = job;
78
+ const jobJSON = JSON.stringify(job, null, 2);
79
+ await fs.mkdir(`${jobDir}/done`, {recursive: true});
80
+ await fs.writeFile(`${jobDir}/done/${id}.json`, `${jobJSON}\n`);
81
+ // If the job had been saved as a file in the todo subdirectory:
82
+ if (isFile) {
83
+ // Delete the file.
84
+ await fs.rm(`${jobDir}/todo/${id}.json`);
85
+ }
86
+ console.log(`Job ${id} archived in ${jobDir}/done (${nowString()})`);
87
+ };
88
+ // Waits.
89
+ const wait = ms => {
90
+ return new Promise(resolve => {
91
+ setTimeout(() => {
92
+ resolve('');
93
+ }, ms);
47
94
  });
48
- watcher.on('close', async code => {
49
- if (error) {
50
- if (error.startsWith('Navigation timeout of 30000 ms exceeded')) {
51
- console.log('ERROR: Playwright claims 30-second timeout exceeded');
95
+ };
96
+ /*
97
+ Checks for a directory job and, when found, performs and reports it.
98
+ Arguments:
99
+ 0. Whether to continue watching after a job is run.
100
+ 1: interval in seconds from a no-job check to the next check.
101
+ */
102
+ exports.dirWatch = async (isForever, intervalInSeconds) => {
103
+ console.log(`Starting to watch directory ${jobDir}/todo for jobs`);
104
+ let notYetRun = true;
105
+ // As long as watching as to continue:
106
+ while (isForever || notYetRun) {
107
+ try {
108
+ // If there are any jobs in the watched directory:
109
+ const toDoFileNames = await fs.readdir(`${jobDir}/todo`);
110
+ const jobFileNames = toDoFileNames.filter(fileName => fileName.endsWith('.json'));
111
+ if (jobFileNames.length) {
112
+ // If the first one is ready to do:
113
+ const firstJobTimeStamp = jobFileNames[0].replace(/-.+$/, '');
114
+ if (Date.now() > dateOf(firstJobTimeStamp)) {
115
+ // Get it.
116
+ const jobJSON = await fs.readFile(`${jobDir}/todo/${jobFileNames[0]}`, 'utf8');
117
+ try {
118
+ const job = JSON.parse(jobJSON);
119
+ const report = JSON.parse(jobJSON);
120
+ const {id} = job;
121
+ console.log(`Directory job ${id} ready to do (${nowString()})`);
122
+ // Perform it.
123
+ await doJob(report);
124
+ console.log(`Job ${id} finished (${nowString()})`);
125
+ // Report it.
126
+ await writeDirReport(report);
127
+ // Archive it.
128
+ await archiveJob(job, true);
129
+ }
130
+ catch(error) {
131
+ console.log(`ERROR processing directory job (${error.message})`);
132
+ }
133
+ notYetRun = false;
134
+ }
135
+ // Otherwise, i.e. if the first one is not yet ready to do:
136
+ else {
137
+ // Report this.
138
+ console.log(`All jobs in ${jobDir} not yet ready to do (${nowString()})`);
139
+ // Wait for the specified interval.
140
+ await wait(1000 * intervalInSeconds);
141
+ }
52
142
  }
143
+ // Otherwise, i.e. if there are no jobs in the watched directory:
53
144
  else {
54
- console.log(`ERROR: ${error.slice(0, 200)}`);
145
+ console.log(`No job in ${jobDir} (${nowString()})`);
146
+ // Wait for the specified interval.
147
+ await wait(1000 * intervalInSeconds);
55
148
  }
56
149
  }
57
- if (code === 0) {
58
- console.log('Watcher exited successfully\n');
59
- reWatch();
60
- }
61
- else {
62
- console.log(`Watcher exited with error code ${code}`);
150
+ // If a fatal error was thrown:
151
+ catch(error) {
152
+ // Report this.
153
+ console.log(`ERROR: Directory watching failed (${error.message}); watching aborted`);
154
+ // Quit watching.
155
+ break;
63
156
  }
64
- });
157
+ }
65
158
  };
66
-
67
- // ########## OPERATION
68
-
69
- reWatch();
@@ -0,0 +1,397 @@
1
+ /*
2
+ © 2022–2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ /*
24
+ netWatch.js
25
+ Module for watching for a network job and running it when found.
26
+ */
27
+
28
+ // ########## IMPORTS
29
+
30
+ // Module to keep secrets.
31
+ require('dotenv').config();
32
+ // Module to read and write files.
33
+ const fs = require('fs/promises');
34
+ // Module to make requests to servers.
35
+ const httpClient = require('http');
36
+ const httpsClient = require('https');
37
+ // Module to perform jobs.
38
+ const {doJob} = require('./run');
39
+
40
+ // CONSTANTS
41
+
42
+ const jobDir = process.env.JOBDIR;
43
+ const jobURLs = process.env.JOB_URLS;
44
+ const reportDir = process.env.REPORTDIR;
45
+ const agent = process.env.AGENT;
46
+
47
+ // ########## FUNCTIONS
48
+
49
+ // Returns a string representing the date and time.
50
+ const nowString = () => (new Date()).toISOString().slice(0, 19);
51
+ // Waits.
52
+ const wait = ms => {
53
+ return new Promise(resolve => {
54
+ setTimeout(() => {
55
+ resolve('');
56
+ }, ms);
57
+ });
58
+ };
59
+ // Serves an object in JSON format.
60
+ const serveObject = (object, response) => {
61
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
62
+ response.end(JSON.stringify(object));
63
+ };
64
+ // Writes a directory report.
65
+ const writeDirReport = async report => {
66
+ const jobID = report && report.id;
67
+ if (jobID) {
68
+ try {
69
+ const reportJSON = JSON.stringify(report, null, 2);
70
+ const reportName = `${jobID}.json`;
71
+ await fs.mkdir(reportDir, {recursive: true});
72
+ await fs.writeFile(`${reportDir}/${reportName}`, reportJSON);
73
+ console.log(`Report ${reportName} saved in ${reportDir}`);
74
+ }
75
+ catch(error) {
76
+ console.log(`ERROR: Failed to write report ${jobID} in ${reportDir} (${error.message})`);
77
+ }
78
+ }
79
+ else {
80
+ console.log('ERROR: Job has no ID');
81
+ }
82
+ };
83
+ // Archives a job.
84
+ const archiveJob = async (job, isFile) => {
85
+ // Save the job in the done subdirectory.
86
+ const {id} = job;
87
+ const jobJSON = JSON.stringify(job, null, 2);
88
+ await fs.mkdir(`${jobDir}/done`, {recursive: true});
89
+ await fs.writeFile(`${jobDir}/done/${id}.json`, jobJSON);
90
+ // If the job had been saved as a file in the todo subdirectory:
91
+ if (isFile) {
92
+ // Delete the file.
93
+ await fs.rm(`${jobDir}/todo/${id}.json`);
94
+ }
95
+ };
96
+ // Checks for a directory job and, if found, performs and reports it, once or repeatedly.
97
+ const checkDirJob = async (isForever, interval) => {
98
+ try {
99
+ // If there are any jobs in the watched directory:
100
+ const toDoFileNames = await fs.readdir(`${jobDir}/todo`);
101
+ const jobFileNames = toDoFileNames.filter(fileName => fileName.endsWith('.json'));
102
+ if (jobFileNames.length) {
103
+ // If the first one is ready to do:
104
+ const firstJobTime = jobFileNames[0].replace(/-.+$/, '');
105
+ if (Date.now() > dateOf(firstJobTime)) {
106
+ // Perform it.
107
+ const jobJSON = await fs.readFile(`${jobDir}/todo/${jobFileNames[0]}`, 'utf8');
108
+ try {
109
+ const job = JSON.parse(jobJSON, null, 2);
110
+ const {id} = job;
111
+ console.log(`Directory job ${id} found (${nowString()})`);
112
+ await doJob(job);
113
+ console.log(`Job ${id} finished (${nowString()})`);
114
+ // Report it.
115
+ await writeDirReport(job);
116
+ // Archive it.
117
+ await archiveJob(job, true);
118
+ console.log(`Job ${id} archived in ${jobDir} (${nowString()})`);
119
+ // If watching is repetitive:
120
+ if (isForever) {
121
+ // Wait 2 seconds.
122
+ await wait(2000);
123
+ // Check the directory again.
124
+ checkDirJob(true, interval);
125
+ }
126
+ }
127
+ catch(error) {
128
+ console.log(`ERROR processing directory job (${error.message})`);
129
+ }
130
+ }
131
+ // Otherwise, i.e. if the first one is not yet ready to do:
132
+ else {
133
+ // Report this.
134
+ console.log(`All jobs in ${jobDir} not yet ready to do (${nowString()})`);
135
+ // Wait for the specified interval.
136
+ await wait(1000 * interval);
137
+ // Check the directory again.
138
+ await checkDirJob(true, interval);
139
+ }
140
+ }
141
+ // Otherwise, i.e. if there are no more jobs in the watched directory:
142
+ else {
143
+ console.log(`No job in ${jobDir} (${nowString()})`);
144
+ // Wait for the specified interval.
145
+ await wait(1000 * interval);
146
+ // Check the directory again.
147
+ await checkDirJob(true, interval);
148
+ }
149
+ }
150
+ catch(error) {
151
+ console.log(`ERROR: Directory watching failed (${error.message})`);
152
+ }
153
+ };
154
+ // Checks servers for a network job.
155
+ const checkNetJob = async (servers, serverIndex, isForever, interval, noJobCount) => {
156
+ // If all servers are jobless:
157
+ if (noJobCount === servers.length) {
158
+ // Wait for the specified interval.
159
+ await wait(1000 * interval);
160
+ // Reset the count of jobless servers.
161
+ noJobCount = 0;
162
+ }
163
+ // Otherwise, i.e. if any server may still have a job:
164
+ else {
165
+ // Wait 2 seconds.
166
+ await wait(2000);
167
+ }
168
+ // If the last server has been checked:
169
+ serverIndex = serverIndex % servers.length;
170
+ if (serverIndex === 0) {
171
+ // Report this.
172
+ console.log('--');
173
+ }
174
+ // Check the next server.
175
+ const server = servers[serverIndex];
176
+ const client = server.startsWith('https://') ? httpsClient : httpClient;
177
+ const fullURL = `${server}?agent=${agent}`;
178
+ const logStart = `Requested job from server ${server} and got `;
179
+ // Tolerate unrecognized certificate authorities if the environment specifies.
180
+ const ruOpt = process.env.REJECT_UNAUTHORIZED === 'false' ? {rejectUnauthorized: false} : {};
181
+ client.request(fullURL, ruOpt, response => {
182
+ const chunks = [];
183
+ response
184
+ // If the response to the job request threw an error:
185
+ .on('error', async error => {
186
+ // Report it.
187
+ console.log(`${logStart}error message ${error.message}`);
188
+ // Check the next server.
189
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
190
+ })
191
+ .on('data', chunk => {
192
+ chunks.push(chunk);
193
+ })
194
+ // When the response arrives:
195
+ .on('end', async () => {
196
+ const content = chunks.join('');
197
+ try {
198
+ // If there was no job to do:
199
+ let contentObj = JSON.parse(content);
200
+ if (! Object.keys(contentObj).length) {
201
+ // Report this.
202
+ console.log(`No job to do at ${server}`);
203
+ // Check the next server.
204
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
205
+ }
206
+ // Otherwise, i.e. if there was a job or a message:
207
+ else {
208
+ const {message, id, sources} = contentObj;
209
+ // If the server sent a message, not a job:
210
+ if (message) {
211
+ // Report it.
212
+ console.log(`${logStart}${message}`);
213
+ // Check the next server.
214
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
215
+ }
216
+ // Otherwise, if the server sent a valid job:
217
+ else if (id && sources && sources.target && sources.target.which) {
218
+ // Add the agent to it.
219
+ sources.agent = agent;
220
+ // If the job specifies a report destination:
221
+ const {sendReportTo} = sources;
222
+ if (sendReportTo) {
223
+ // Perform the job, adding result data to it.
224
+ const testee = sources.target.which;
225
+ console.log(
226
+ `${logStart}job ${id} (${nowString()})\n>> It will test ${testee}\n>> It will send report to ${sendReportTo}\n`
227
+ );
228
+ await doJob(contentObj);
229
+ let reportJSON = JSON.stringify(contentObj, null, 2);
230
+ console.log(`Job ${id} finished (${nowString()})`);
231
+ // Send the report to the specified server.
232
+ console.log(`Sending report ${id} to ${sendReportTo}`);
233
+ const reportClient = sendReportTo.startsWith('https://') ? httpsClient : httpClient;
234
+ const reportLogStart = `Sent report ${id} to ${sendReportTo} and got `;
235
+ reportClient.request(sendReportTo, {method: 'POST'}, repResponse => {
236
+ const chunks = [];
237
+ repResponse
238
+ // If the response to the report threw an error:
239
+ .on('error', async error => {
240
+ // Report this.
241
+ console.log(`${reportLogStart}error message ${error.message}\n`);
242
+ // Check the next server.
243
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
244
+ })
245
+ .on('data', chunk => {
246
+ chunks.push(chunk);
247
+ })
248
+ // When the response arrives:
249
+ .on('end', async () => {
250
+ const content = chunks.join('');
251
+ try {
252
+ // If the server sent a message, as expected:
253
+ const ackObj = JSON.parse(content);
254
+ const {message} = ackObj;
255
+ if (message) {
256
+ // Report it.
257
+ console.log(`${reportLogStart}${message}\n`);
258
+ // Free the memory used by the report.
259
+ reportJSON = '';
260
+ contentObj = {};
261
+ // Check the next server.
262
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, 0);
263
+ }
264
+ // Otherwise, i.e. if the server sent anything else:
265
+ else {
266
+ // Report it.
267
+ console.log(
268
+ `ERROR: ${reportLogStart}status ${repResponse.statusCode} and error message ${JSON.stringify(ackObj, null, 2)}\n`
269
+ );
270
+ // Check the next server, disregarding the failed job.
271
+ await checkNetJob(
272
+ servers, serverIndex + 1, isForever, interval, noJobCount + 1
273
+ );
274
+ }
275
+ }
276
+ // If processing the report threw an error:
277
+ catch(error) {
278
+ // Report it.
279
+ console.log(
280
+ `ERROR: ${reportLogStart}status ${repResponse.statusCode}, error message ${error.message}, and response ${content.slice(0, 1000)}\n`
281
+ );
282
+ // Check the next server, disregarding the failed job.
283
+ await checkNetJob(
284
+ servers, serverIndex + 1, isForever, interval, noJobCount + 1
285
+ );
286
+ }
287
+ });
288
+ })
289
+ // If the report submission throws an error:
290
+ .on('error', async error => {
291
+ // Report this.
292
+ console.log(`ERROR: ${reportLogStart}error message ${error.message}\n`);
293
+ // Check the next server, disregarding the failed job.
294
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
295
+ })
296
+ // Finish submitting the report.
297
+ .end(reportJSON);
298
+ }
299
+ // Otherwise, i.e. if the job specifies no report destination:
300
+ else {
301
+ // Report this.
302
+ const message = `ERROR: ${logStart}job with no report destination`;
303
+ serveObject({message}, response);
304
+ console.log(message);
305
+ // Check the next server, disregarding the defective job.
306
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
307
+ }
308
+ }
309
+ // Otherwise, if the server sent an invalid job:
310
+ else {
311
+ // Report this.
312
+ const message
313
+ = `ERROR: ${logStart}invalid job:\n${JSON.stringify(contentObj, null, 2)}`;
314
+ console.log(message);
315
+ serveObject({message}, response);
316
+ // Check the next server, disregarding the defective job.
317
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
318
+ }
319
+ }
320
+ }
321
+ // If an error is thrown:
322
+ catch(error) {
323
+ // Report this.
324
+ console.log(`ERROR: ${error.message} (response ${content.slice(0, 1000)})`);
325
+ // Check the next server.
326
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
327
+ }
328
+ });
329
+ })
330
+ // If the job request throws an error:
331
+ .on('error', async error => {
332
+ // If it was a refusal to connect:
333
+ const {message} = error;
334
+ if (message.includes('ECONNREFUSED')) {
335
+ // Report this.
336
+ console.log(`${logStart}no connection`);
337
+ // Check the next server.
338
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
339
+ }
340
+ // Otherwise, if it was a DNS failure:
341
+ else if (message.includes('ENOTFOUND')) {
342
+ // Report this.
343
+ console.log(`${logStart}no domain name resolution`);
344
+ // Check the next server.
345
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
346
+ }
347
+ // Otherwise, i.e. if it was any other error:
348
+ else {
349
+ // Report this.
350
+ console.log(
351
+ `ERROR: ${logStart}no response, but got error message ${error.message.slice(0, 200)}`
352
+ );
353
+ // Check the next server.
354
+ await checkNetJob(servers, serverIndex + 1, isForever, interval, noJobCount + 1);
355
+ }
356
+ })
357
+ // Finish sending the request.
358
+ .end();
359
+ };
360
+ // Composes an interval description.
361
+ const intervalSpec = interval => {
362
+ if (interval > -1) {
363
+ return `repeatedly, with ${interval}-second intervals `;
364
+ }
365
+ else {
366
+ return '';
367
+ }
368
+ };
369
+ // Checks for a directory job, performs it, and submits a report, once or repeatedly.
370
+ exports.dirWatch = async (isForever, interval = 300) => {
371
+ console.log(`Directory watching started ${intervalSpec(interval)}(${nowString()})\n`);
372
+ // Start the checking.
373
+ await checkDirJob(isForever, interval);
374
+ };
375
+ // Checks for a network job, performs it, and submits a report, once or repeatedly.
376
+ exports.netWatch = async (isForever, interval = 300) => {
377
+ console.log('Starting netWatch');
378
+ // If the servers to be checked are valid:
379
+ const servers = jobURLs
380
+ .split('+')
381
+ .map(url => [Math.random(), url])
382
+ .sort((a, b) => a[0] - b[0])
383
+ .map(pair => pair[1]);
384
+ if (
385
+ servers
386
+ && servers.length
387
+ && servers
388
+ .every(server => ['http://', 'https://'].some(prefix => server.startsWith(prefix)))
389
+ ) {
390
+ console.log(`Network watching started ${intervalSpec(interval)}(${nowString()})\n`);
391
+ // Start checking.
392
+ await checkNetJob(servers, 0, isForever, interval, 0);
393
+ }
394
+ else {
395
+ console.log('ERROR: List of job URLs invalid');
396
+ }
397
+ };
package/netWatch.js ADDED
@@ -0,0 +1,303 @@
1
+ /*
2
+ © 2022–2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ /*
24
+ netWatch.js
25
+ Module for watching for a network job and running it when found.
26
+ */
27
+
28
+ // IMPORTS
29
+
30
+ // Module to keep secrets.
31
+ require('dotenv').config();
32
+ // Module to read and write files.
33
+ const fs = require('fs/promises');
34
+ // Modules to make requests to servers.
35
+ const httpClient = require('http');
36
+ const httpsClient = require('https');
37
+ // Module to perform jobs.
38
+ const {doJob} = require('./run');
39
+
40
+ // CONSTANTS
41
+
42
+ const jobURLSpec = process.env.JOB_URLS;
43
+ const agent = process.env.AGENT;
44
+
45
+ // FUNCTIONS
46
+
47
+ // Returns a string representing the date and time.
48
+ const nowString = () => (new Date()).toISOString().slice(2, 15);
49
+ // Waits.
50
+ const wait = ms => {
51
+ return new Promise(resolve => {
52
+ setTimeout(() => {
53
+ resolve('');
54
+ }, ms);
55
+ });
56
+ };
57
+ // Serves an object in JSON format.
58
+ const serveObject = (object, response) => {
59
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
60
+ response.end(JSON.stringify(object));
61
+ };
62
+ /*
63
+ Requests a network job and, when found, performs and reports it.
64
+ Arguments:
65
+ 0. whether to continue watching after a job is run.
66
+ 1: interval in seconds from a cycle of no-job checks to the next cycle.
67
+ */
68
+ exports.netWatch = async (isForever, intervalInSeconds, isCertTolerant = true) => {
69
+ const urls = jobURLSpec
70
+ .split('+')
71
+ .map(url => [Math.random(), url])
72
+ .sort((a, b) => a[0] - b[0])
73
+ .map(pair => pair[1]);
74
+ const urlCount = urls.length;
75
+ // If the job URLs exist and are valid:
76
+ if (
77
+ urls
78
+ && urlCount
79
+ && urls.every(url => ['http://', 'https://'].some(prefix => url.startsWith(prefix)))
80
+ ) {
81
+ // Configure the watch.
82
+ let cycleIndex = -1;
83
+ let urlIndex = -1;
84
+ let noJobYet = true;
85
+ let abort = false;
86
+ const certOpt = isCertTolerant ? {rejectUnauthorized: false} : {};
87
+ const certInfo = `Certificate-${isCertTolerant ? '' : 'in'}tolerant`;
88
+ const foreverInfo = isForever ? 'repeating' : 'one-job';
89
+ const intervalInfo = `with ${intervalInSeconds}-second intervals`;
90
+ console.log(
91
+ `${certInfo} ${foreverInfo} network watching started ${intervalInfo} (${nowString()})\n`
92
+ );
93
+ // As long as watching is to continue:
94
+ while ((isForever || noJobYet) && ! abort) {
95
+ // If the cycle is complete:
96
+ if (cycleIndex === urlCount - 1) {
97
+ // Wait for the specified interval.
98
+ await wait(1000 * intervalInSeconds);
99
+ // Log the start of a cycle.
100
+ console.log('--');
101
+ }
102
+ // Otherwise, i.e. if the cycle is incomplete:
103
+ else {
104
+ // Wait briefly.
105
+ await wait(1000);
106
+ }
107
+ // Configure the next check.
108
+ cycleIndex = ++cycleIndex % urlCount;
109
+ urlIndex = ++urlIndex % urlCount;
110
+ const url = urls[urlIndex];
111
+ const logStart = `Requested job from server ${url} and got `;
112
+ const fullURL = `${url}?agent=${agent}`;
113
+ // Perform it.
114
+ await new Promise(resolve => {
115
+ try {
116
+ const client = url.startsWith('https://') ? httpsClient : httpClient;
117
+ // Request a job.
118
+ client.request(fullURL, certOpt, response => {
119
+ const chunks = [];
120
+ response
121
+ // If the response throws an error:
122
+ .on('error', async error => {
123
+ // Report it.
124
+ console.log(`${logStart}error message ${error.message}`);
125
+ resolve(true);
126
+ })
127
+ .on('data', chunk => {
128
+ chunks.push(chunk);
129
+ })
130
+ // When the response arrives:
131
+ .on('end', async () => {
132
+ const content = chunks.join('');
133
+ try {
134
+ // If there was no job to do:
135
+ let contentObj = JSON.parse(content);
136
+ if (! Object.keys(contentObj).length) {
137
+ // Report this.
138
+ console.log(`No job to do at ${url}`);
139
+ resolve(true);
140
+ }
141
+ // Otherwise, i.e. if there was a job or a message:
142
+ else {
143
+ const {message, id, sources} = contentObj;
144
+ // If the server sent a message, not a job:
145
+ if (message) {
146
+ // Report it.
147
+ console.log(`${logStart}${message}`);
148
+ resolve(true);
149
+ }
150
+ // Otherwise, if the server sent a valid job:
151
+ else if (id && sources && sources.target && sources.target.which) {
152
+ // Restart the cycle.
153
+ cycleIndex = -1;
154
+ // Prevent further watching, if unwanted.
155
+ noJobYet = false;
156
+ // Add the agent to the job.
157
+ sources.agent = agent;
158
+ // If the job specifies a report destination:
159
+ const {sendReportTo} = sources;
160
+ if (sendReportTo) {
161
+ // Perform the job, adding result data to it.
162
+ const target = sources.target.which;
163
+ console.log(`${logStart}job ${id} (${nowString()}`);
164
+ console.log(`>> It will test ${target}`);
165
+ console.log(`>> It will send report to ${sendReportTo}`);
166
+ await doJob(contentObj);
167
+ let reportJSON = JSON.stringify(contentObj, null, 2);
168
+ console.log(`Job ${id} finished (${nowString()})`);
169
+ // Send the report to the specified server.
170
+ console.log(`Sending report ${id} to ${sendReportTo}`);
171
+ const reportClient = sendReportTo.startsWith('https://') ? httpsClient : httpClient;
172
+ const reportLogStart = `Sent report ${id} to ${sendReportTo} and got `;
173
+ reportClient.request(sendReportTo, {method: 'POST'}, repResponse => {
174
+ const chunks = [];
175
+ repResponse
176
+ // If the response to the report threw an error:
177
+ .on('error', async error => {
178
+ // Report this.
179
+ console.log(`${reportLogStart}error message ${error.message}\n`);
180
+ resolve(true);
181
+ })
182
+ .on('data', chunk => {
183
+ chunks.push(chunk);
184
+ })
185
+ // When the response arrives:
186
+ .on('end', async () => {
187
+ const content = chunks.join('');
188
+ try {
189
+ // If the server sent a message, as expected:
190
+ const ackObj = JSON.parse(content);
191
+ const {message} = ackObj;
192
+ if (message) {
193
+ // Report it.
194
+ console.log(`${reportLogStart}message ${message}\n`);
195
+ // Free the memory used by the report.
196
+ reportJSON = '';
197
+ contentObj = {};
198
+ resolve(true);
199
+ }
200
+ // Otherwise, i.e. if the server sent anything else:
201
+ else {
202
+ // Report it.
203
+ console.log(
204
+ `ERROR: ${reportLogStart}status ${repResponse.statusCode} and error message ${JSON.stringify(ackObj, null, 2)}\n`
205
+ );
206
+ resolve(true);
207
+ }
208
+ }
209
+ // If processing the server message throws an error:
210
+ catch(error) {
211
+ // Report it.
212
+ console.log(
213
+ `ERROR: ${reportLogStart}status ${repResponse.statusCode}, error message ${error.message}, and response ${content.slice(0, 1000)}\n`
214
+ );
215
+ resolve(true);
216
+ }
217
+ });
218
+ })
219
+ // If the report submission throws an error:
220
+ .on('error', async error => {
221
+ // Report this.
222
+ console.log(`ERROR: ${reportLogStart}error message ${error.message}\n`);
223
+ resolve(true);
224
+ })
225
+ // Finish submitting the report.
226
+ .end(reportJSON);
227
+ }
228
+ // Otherwise, i.e. if the job specifies no report destination:
229
+ else {
230
+ // Report this.
231
+ const message = `ERROR: ${logStart}job with no report destination`;
232
+ serveObject({message}, response);
233
+ console.log(message);
234
+ resolve(true);
235
+ }
236
+ }
237
+ // Otherwise, i.e. if the server sent an invalid job:
238
+ else {
239
+ // Report this.
240
+ const message
241
+ = `ERROR: ${logStart}invalid job:\n${JSON.stringify(contentObj, null, 2)}`;
242
+ console.log(message);
243
+ serveObject({message}, response);
244
+ resolve(true);
245
+ }
246
+ }
247
+ }
248
+ // If processing the server response throws an error:
249
+ catch(error) {
250
+ // Report this.
251
+ console.log(`ERROR: ${error.message} (response ${content.slice(0, 1000)})`);
252
+ resolve(true);
253
+ }
254
+ });
255
+ })
256
+ // If the job request throws an error:
257
+ .on('error', async error => {
258
+ // If it is a refusal to connect:
259
+ if (error.code && error.code.includes('ECONNREFUSED')) {
260
+ // Report this.
261
+ console.log(`${logStart}no connection`);
262
+ }
263
+ // Otherwise, if it was a DNS failure:
264
+ else if (error.code && error.code.includes('ENOTFOUND')) {
265
+ // Report this.
266
+ console.log(`${logStart}no domain name resolution`);
267
+ }
268
+ // Otherwise, if it was any other error with a message:
269
+ else if (error.message) {
270
+ // Report this.
271
+ console.log(`ERROR: ${logStart}got error message ${error.message.slice(0, 200)}`);
272
+ // Abort the watch.
273
+ abort = true;
274
+ }
275
+ // Otherwise, i.e. if it was any other error with no message:
276
+ else {
277
+ // Report this.
278
+ console.log(`ERROR: ${logStart}got an error with no message`);
279
+ // Abort the watch.
280
+ abort = true;
281
+ }
282
+ resolve(true);
283
+ })
284
+ // Finish sending the job request.
285
+ .end();
286
+ }
287
+ // If requesting a job throws an error:
288
+ catch(error) {
289
+ // Report this.
290
+ console.log(`ERROR requesting a network job (${error.message})`);
291
+ abort = true;
292
+ resolve(true);
293
+ }
294
+ });
295
+ }
296
+ console.log('Watching complete');
297
+ }
298
+ // Otherwise, i.e. if the job URLs do not exist or are invalid:
299
+ else {
300
+ // Report this.
301
+ console.log('ERROR: List of job URLs invalid');
302
+ }
303
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "32.3.5",
3
+ "version": "32.4.0",
4
4
  "description": "Run 900 web accessibility tests from 9 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -282,7 +282,7 @@ const doQualWeb = (result, standardResult, ruleClassName) => {
282
282
  const instance = {
283
283
  ruleID,
284
284
  what: item.description,
285
- ordinalSeverity: severities[ruleClassName][item.verdict],
285
+ ordinalSeverity: severities[ruleClassName][item.verdict] || 0,
286
286
  tagName: identifiers[0],
287
287
  id: identifiers[1],
288
288
  location: {
package/run.js CHANGED
@@ -277,14 +277,10 @@ const isValidReport = report => {
277
277
  if (typeof sources.script !== 'string') {
278
278
  return 'Bad source script';
279
279
  }
280
- if (
281
- ! creationTime
282
- || typeof creationTime !== 'string'
283
- || ! /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(creationTime)
284
- ) {
280
+ if (! (creationTime && typeof creationTime === 'string' && Date.parse(creationTime))) {
285
281
  return 'bad job creation time';
286
282
  }
287
- if (! timeStamp || typeof timeStamp !== 'string') {
283
+ if (! (timeStamp && typeof timeStamp === 'string')) {
288
284
  return 'bad report timestamp';
289
285
  }
290
286
  return '';
@@ -772,12 +768,18 @@ const doActs = async (report, actIndex, page) => {
772
768
  actInfo = act.which;
773
769
  }
774
770
  }
771
+ const message = `>>>> ${act.type}: ${actInfo}`;
775
772
  // If granular reporting has been specified:
776
773
  if (report.observe) {
777
- // Notify the observer of the act.
774
+ // Notify the observer of the act and log it.
778
775
  const whichParam = act.which ? `&which=${act.which}` : '';
779
776
  const messageParams = `act=${act.type}${whichParam}`;
780
- tellServer(report, messageParams, `>>>> ${act.type}: ${actInfo}`);
777
+ tellServer(report, messageParams, message);
778
+ }
779
+ // Otherwise, i.e. if granular reporting has not been specified:
780
+ else {
781
+ // Log the act.
782
+ console.log(message);
781
783
  }
782
784
  // Increment the count of acts performed.
783
785
  actCount++;
package/tests/qualWeb.js CHANGED
@@ -114,15 +114,11 @@ exports.reporter = async (page, options) => {
114
114
  else {
115
115
  const bestPractices = bestSpec.slice(5).split(',').map(num => `QW-BP${num}`);
116
116
  qualWebOptions['best-practices'] = {bestPractices};
117
- // qualWebOptions.execute.bp = true;
118
- // Temporarily disable best practices, because they crash QualWeb.
119
- qualWebOptions.execute.bp = false;
117
+ qualWebOptions.execute.bp = true;
120
118
  }
121
119
  }
122
120
  else {
123
- // qualWebOptions.execute.bp = true;
124
- // Temporarily disable best practices, because they crash QualWeb.
125
- qualWebOptions.execute.bp = false;
121
+ qualWebOptions.execute.bp = true;
126
122
  }
127
123
  // Get the report.
128
124
  let actReports = await qualWeb.evaluate(qualWebOptions);
package/watch-old.js ADDED
@@ -0,0 +1,77 @@
1
+ /*
2
+ © 2023 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ /*
24
+ dirWatch.js
25
+ Module for launching a one-time directory watch.
26
+ Argument:
27
+ 1: interval in seconds from a no-job check to the next check.
28
+ */
29
+
30
+ // ########## IMPORTS
31
+
32
+ // Module to spawn a child process.
33
+ const {spawn} = require('node:child_process');
34
+
35
+ // ########## CONSTANTS
36
+
37
+ const repeat = process.argv[2];
38
+ const interval = process.argv[3];
39
+
40
+ // ########## FUNCTIONS
41
+
42
+ // Spawns a one-time directory watch.
43
+ const spawnWatch = () => spawn(
44
+ 'node', 'dirWatch', 'false', interval, {stdio: ['inherit', 'inherit', 'pipe']}
45
+ );
46
+ // Repeatedly spawns a one-time directory watch.
47
+ const reWatch = () => {
48
+ const watcher = spawnWatch('node', ['call', 'dirWatch', 'false', interval]);
49
+ let error = '';
50
+ watcher.stderr.on('data', data => {
51
+ error += data.toString();
52
+ });
53
+ watcher.on('close', async code => {
54
+ if (error) {
55
+ if (error.startsWith('Navigation timeout of 30000 ms exceeded')) {
56
+ console.log('ERROR: Playwright claims 30-second timeout exceeded');
57
+ }
58
+ else {
59
+ console.log(`ERROR watching: ${error.slice(0, 200)}`);
60
+ }
61
+ }
62
+ if (! error && code === 0) {
63
+ console.log('Watcher exited successfully\n');
64
+ reWatch();
65
+ }
66
+ else if (code) {
67
+ console.log(`Watcher exited with error code ${code}`);
68
+ }
69
+ else {
70
+ console.log('Watch aborted');
71
+ }
72
+ });
73
+ };
74
+
75
+ // ########## OPERATION
76
+
77
+ reWatch();
package/watch.js CHANGED
@@ -96,39 +96,51 @@ const archiveJob = async (job, isFile) => {
96
96
  // Checks for a directory job and, if found, performs and reports it, once or repeatedly.
97
97
  const checkDirJob = async (isForever, interval) => {
98
98
  try {
99
- // If there are any jobs to do in the watched directory:
99
+ // If there are any jobs in the watched directory:
100
100
  const toDoFileNames = await fs.readdir(`${jobDir}/todo`);
101
101
  const jobFileNames = toDoFileNames.filter(fileName => fileName.endsWith('.json'));
102
102
  if (jobFileNames.length) {
103
- // Get the first one.
104
- const jobJSON = await fs.readFile(`${jobDir}/todo/${jobFileNames[0]}`, 'utf8');
105
- try {
106
- const job = JSON.parse(jobJSON, null, 2);
107
- const {id} = job;
103
+ // If the first one is ready to do:
104
+ const firstJobTime = jobFileNames[0].replace(/-.+$/, '');
105
+ if (Date.now() > dateOf(firstJobTime)) {
108
106
  // Perform it.
109
- console.log(`Directory job ${id} found (${nowString()})`);
110
- await doJob(job);
111
- console.log(`Job ${id} finished (${nowString()})`);
112
- // Report it.
113
- await writeDirReport(job);
114
- // Archive it.
115
- await archiveJob(job, true);
116
- console.log(`Job ${id} archived in ${jobDir} (${nowString()})`);
117
- // If watching is repetitive:
118
- if (isForever) {
119
- // Wait 2 seconds.
120
- await wait(2000);
121
- // Check the directory again.
122
- checkDirJob(true, interval);
107
+ const jobJSON = await fs.readFile(`${jobDir}/todo/${jobFileNames[0]}`, 'utf8');
108
+ try {
109
+ const job = JSON.parse(jobJSON, null, 2);
110
+ const {id} = job;
111
+ console.log(`Directory job ${id} found (${nowString()})`);
112
+ await doJob(job);
113
+ console.log(`Job ${id} finished (${nowString()})`);
114
+ // Report it.
115
+ await writeDirReport(job);
116
+ // Archive it.
117
+ await archiveJob(job, true);
118
+ console.log(`Job ${id} archived in ${jobDir} (${nowString()})`);
119
+ // If watching is repetitive:
120
+ if (isForever) {
121
+ // Wait 2 seconds.
122
+ await wait(2000);
123
+ // Check the directory again.
124
+ checkDirJob(true, interval);
125
+ }
126
+ }
127
+ catch(error) {
128
+ console.log(`ERROR processing directory job (${error.message})`);
123
129
  }
124
130
  }
125
- catch(error) {
126
- console.log(`ERROR processing directory job (${error.message})`);
131
+ // Otherwise, i.e. if the first one is not yet ready to do:
132
+ else {
133
+ // Report this.
134
+ console.log(`All jobs in ${jobDir} not yet ready to do (${nowString()})`);
135
+ // Wait for the specified interval.
136
+ await wait(1000 * interval);
137
+ // Check the directory again.
138
+ await checkDirJob(true, interval);
127
139
  }
128
140
  }
129
- // Otherwise, i.e. if there are no more jobs to do in the watched directory:
141
+ // Otherwise, i.e. if there are no more jobs in the watched directory:
130
142
  else {
131
- console.log(`No job to do in ${jobDir} (${nowString()})`);
143
+ console.log(`No job in ${jobDir} (${nowString()})`);
132
144
  // Wait for the specified interval.
133
145
  await wait(1000 * interval);
134
146
  // Check the directory again.