@jsenv/test 3.7.36 → 3.7.38
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/dist/jsenv_test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSupportsColor, isUnicodeSupported, stripAnsi, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_test_node_modules.js";
|
|
2
2
|
import { URL_META, createException } from "./exception.js";
|
|
3
|
-
import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFile as readFile$1, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, readFileSync, writeFileSync as writeFileSync$1, unlink, rmdir, existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFile as readFile$1, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, readFileSync, writeFileSync as writeFileSync$1, unlink, rmdir, existsSync, realpathSync, readSync } from "node:fs";
|
|
4
4
|
import { takeCoverage } from "node:v8";
|
|
5
5
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
@@ -5959,6 +5959,176 @@ const replaceUrls$1 = (source, replace) => {
|
|
|
5959
5959
|
});
|
|
5960
5960
|
};
|
|
5961
5961
|
|
|
5962
|
+
/*
|
|
5963
|
+
* Reads what a test file declares about its own execution, from the directive
|
|
5964
|
+
* prologue — the string literals a module may open with, as in "use strict" or
|
|
5965
|
+
* "use client":
|
|
5966
|
+
*
|
|
5967
|
+
* "jsenv:allocate 90s";
|
|
5968
|
+
* "jsenv:lock service-worker";
|
|
5969
|
+
*
|
|
5970
|
+
* They are read without running the file, which is what allows a lock to be
|
|
5971
|
+
* honored: an execution must not have started before we know what it takes.
|
|
5972
|
+
* Being grammar rather than convention, a directive cannot be built at runtime
|
|
5973
|
+
* and cannot move: it is the first statement or it is nothing.
|
|
5974
|
+
*
|
|
5975
|
+
* A "jsenv:" directive that cannot be read throws. Everything ignores an
|
|
5976
|
+
* unknown directive silently, so a typo would silently give the file back the
|
|
5977
|
+
* default budget; the only way it stays useful is to be loud.
|
|
5978
|
+
*/
|
|
5979
|
+
|
|
5980
|
+
|
|
5981
|
+
const DIRECTIVE_PREFIX = "jsenv:";
|
|
5982
|
+
const DIRECTIVE_NAMES = `"jsenv:allocate <duration>", "jsenv:lock <resource>"`;
|
|
5983
|
+
const DURATION_REGEX = /^(\d+)(ms|s|m)$/;
|
|
5984
|
+
const MS_PER_UNIT = { ms: 1, s: 1_000, m: 60_000 };
|
|
5985
|
+
// a directive prologue sits at the top of the file; comments can precede it but
|
|
5986
|
+
// rarely for more than a few lines, and the whole file is read when they do
|
|
5987
|
+
const HEAD_BYTE_COUNT = 4096;
|
|
5988
|
+
|
|
5989
|
+
const readJsenvDirectives = (fileUrl) => {
|
|
5990
|
+
const head = readHead(fileUrl);
|
|
5991
|
+
let scanResult = scanDirectivePrologue(head.text);
|
|
5992
|
+
if (head.partial && !scanResult.complete) {
|
|
5993
|
+
scanResult = scanDirectivePrologue(readFileSync(new URL(fileUrl), "utf8"));
|
|
5994
|
+
}
|
|
5995
|
+
|
|
5996
|
+
let allocatedMs;
|
|
5997
|
+
const lockArray = [];
|
|
5998
|
+
for (const directiveText of scanResult.directiveTextArray) {
|
|
5999
|
+
if (!directiveText.startsWith(DIRECTIVE_PREFIX)) {
|
|
6000
|
+
continue;
|
|
6001
|
+
}
|
|
6002
|
+
const body = directiveText.slice(DIRECTIVE_PREFIX.length);
|
|
6003
|
+
const spaceIndex = body.indexOf(" ");
|
|
6004
|
+
const name = spaceIndex === -1 ? body : body.slice(0, spaceIndex);
|
|
6005
|
+
const argument = spaceIndex === -1 ? "" : body.slice(spaceIndex + 1).trim();
|
|
6006
|
+
const fail = (reason, details = {}) => {
|
|
6007
|
+
throw new Error(
|
|
6008
|
+
createDetailedMessage(reason, {
|
|
6009
|
+
directive: `"${directiveText}"`,
|
|
6010
|
+
file: fileURLToPath(fileUrl),
|
|
6011
|
+
...details,
|
|
6012
|
+
}),
|
|
6013
|
+
);
|
|
6014
|
+
};
|
|
6015
|
+
|
|
6016
|
+
if (name === "allocate") {
|
|
6017
|
+
const match = DURATION_REGEX.exec(argument);
|
|
6018
|
+
if (!match) {
|
|
6019
|
+
fail(`"jsenv:allocate" expects a duration, got "${argument}"`, {
|
|
6020
|
+
["durations accepted"]: `"500ms", "90s", "2m"`,
|
|
6021
|
+
});
|
|
6022
|
+
}
|
|
6023
|
+
allocatedMs = Number(match[1]) * MS_PER_UNIT[match[2]];
|
|
6024
|
+
continue;
|
|
6025
|
+
}
|
|
6026
|
+
if (name === "lock") {
|
|
6027
|
+
if (argument === "") {
|
|
6028
|
+
fail(`"jsenv:lock" expects the name of a resource`);
|
|
6029
|
+
}
|
|
6030
|
+
lockArray.push(argument);
|
|
6031
|
+
continue;
|
|
6032
|
+
}
|
|
6033
|
+
fail(`unknown jsenv directive "${name}"`, {
|
|
6034
|
+
["directives available"]: DIRECTIVE_NAMES,
|
|
6035
|
+
});
|
|
6036
|
+
}
|
|
6037
|
+
return { allocatedMs, lockArray };
|
|
6038
|
+
};
|
|
6039
|
+
|
|
6040
|
+
const readHead = (fileUrl) => {
|
|
6041
|
+
const fileDescriptor = openSync(fileURLToPath(fileUrl), "r");
|
|
6042
|
+
try {
|
|
6043
|
+
const buffer = Buffer.allocUnsafe(HEAD_BYTE_COUNT);
|
|
6044
|
+
const byteCount = readSync(fileDescriptor, buffer, 0, HEAD_BYTE_COUNT, 0);
|
|
6045
|
+
return {
|
|
6046
|
+
text: buffer.toString("utf8", 0, byteCount),
|
|
6047
|
+
partial: byteCount === HEAD_BYTE_COUNT,
|
|
6048
|
+
};
|
|
6049
|
+
} finally {
|
|
6050
|
+
closeSync(fileDescriptor);
|
|
6051
|
+
}
|
|
6052
|
+
};
|
|
6053
|
+
|
|
6054
|
+
/*
|
|
6055
|
+
* Collects the string literals opening the module, stopping at the first token
|
|
6056
|
+
* that is neither a comment nor one of them. "complete" tells whether that
|
|
6057
|
+
* token was reached: when it was not, the source given was cut short and the
|
|
6058
|
+
* caller must read further before trusting the result.
|
|
6059
|
+
*/
|
|
6060
|
+
const scanDirectivePrologue = (source) => {
|
|
6061
|
+
const directiveTextArray = [];
|
|
6062
|
+
const length = source.length;
|
|
6063
|
+
let index = 0;
|
|
6064
|
+
if (source.startsWith("#!")) {
|
|
6065
|
+
const lineEndIndex = source.indexOf("\n");
|
|
6066
|
+
if (lineEndIndex === -1) {
|
|
6067
|
+
return { directiveTextArray, complete: false };
|
|
6068
|
+
}
|
|
6069
|
+
index = lineEndIndex + 1;
|
|
6070
|
+
}
|
|
6071
|
+
while (index < length) {
|
|
6072
|
+
const char = source[index];
|
|
6073
|
+
if (
|
|
6074
|
+
char === " " ||
|
|
6075
|
+
char === "\t" ||
|
|
6076
|
+
char === "\n" ||
|
|
6077
|
+
char === "\r" ||
|
|
6078
|
+
char === ";"
|
|
6079
|
+
) {
|
|
6080
|
+
index++;
|
|
6081
|
+
continue;
|
|
6082
|
+
}
|
|
6083
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
6084
|
+
const lineEndIndex = source.indexOf("\n", index);
|
|
6085
|
+
if (lineEndIndex === -1) {
|
|
6086
|
+
return { directiveTextArray, complete: false };
|
|
6087
|
+
}
|
|
6088
|
+
index = lineEndIndex + 1;
|
|
6089
|
+
continue;
|
|
6090
|
+
}
|
|
6091
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
6092
|
+
const commentEndIndex = source.indexOf("*/", index + 2);
|
|
6093
|
+
if (commentEndIndex === -1) {
|
|
6094
|
+
return { directiveTextArray, complete: false };
|
|
6095
|
+
}
|
|
6096
|
+
index = commentEndIndex + 2;
|
|
6097
|
+
continue;
|
|
6098
|
+
}
|
|
6099
|
+
if (char === '"' || char === "'") {
|
|
6100
|
+
const quote = char;
|
|
6101
|
+
let stringIndex = index + 1;
|
|
6102
|
+
let text = "";
|
|
6103
|
+
while (stringIndex < length) {
|
|
6104
|
+
const stringChar = source[stringIndex];
|
|
6105
|
+
if (stringChar === "\\") {
|
|
6106
|
+
text += source[stringIndex + 1];
|
|
6107
|
+
stringIndex += 2;
|
|
6108
|
+
continue;
|
|
6109
|
+
}
|
|
6110
|
+
if (stringChar === quote) {
|
|
6111
|
+
break;
|
|
6112
|
+
}
|
|
6113
|
+
if (stringChar === "\n") {
|
|
6114
|
+
// an unterminated string is not a directive, and not our problem
|
|
6115
|
+
return { directiveTextArray, complete: true };
|
|
6116
|
+
}
|
|
6117
|
+
text += stringChar;
|
|
6118
|
+
stringIndex++;
|
|
6119
|
+
}
|
|
6120
|
+
if (stringIndex >= length) {
|
|
6121
|
+
return { directiveTextArray, complete: false };
|
|
6122
|
+
}
|
|
6123
|
+
directiveTextArray.push(text);
|
|
6124
|
+
index = stringIndex + 1;
|
|
6125
|
+
continue;
|
|
6126
|
+
}
|
|
6127
|
+
return { directiveTextArray, complete: true };
|
|
6128
|
+
}
|
|
6129
|
+
return { directiveTextArray, complete: false };
|
|
6130
|
+
};
|
|
6131
|
+
|
|
5962
6132
|
const createIsInsideFragment = (fragment, total) => {
|
|
5963
6133
|
let [dividend, divisor] = fragment.split("/");
|
|
5964
6134
|
dividend = parseInt(dividend);
|
|
@@ -8377,6 +8547,9 @@ To fix this warning:
|
|
|
8377
8547
|
}
|
|
8378
8548
|
}
|
|
8379
8549
|
const filePlan = meta.testPlan;
|
|
8550
|
+
const directives = readJsenvDirectives(
|
|
8551
|
+
new URL(relativeUrl, rootDirectoryUrl),
|
|
8552
|
+
);
|
|
8380
8553
|
for (const groupName of Object.keys(filePlan)) {
|
|
8381
8554
|
const stepConfig = filePlan[groupName];
|
|
8382
8555
|
if (stepConfig === null || stepConfig === undefined) {
|
|
@@ -8398,7 +8571,7 @@ To fix this warning:
|
|
|
8398
8571
|
runtime,
|
|
8399
8572
|
runtimeParams,
|
|
8400
8573
|
allocatedMs = defaultMsAllocatedPerExecution,
|
|
8401
|
-
|
|
8574
|
+
locks,
|
|
8402
8575
|
} = stepConfig;
|
|
8403
8576
|
const params = {
|
|
8404
8577
|
measureMemoryUsage: true,
|
|
@@ -8406,7 +8579,7 @@ To fix this warning:
|
|
|
8406
8579
|
collectPerformance: false,
|
|
8407
8580
|
collectConsole: true,
|
|
8408
8581
|
allocatedMs,
|
|
8409
|
-
|
|
8582
|
+
locks,
|
|
8410
8583
|
runtime,
|
|
8411
8584
|
runtimeParams: {
|
|
8412
8585
|
rootDirectoryUrl,
|
|
@@ -8466,9 +8639,22 @@ To fix this warning:
|
|
|
8466
8639
|
? defaultMsAllocatedPerExecution
|
|
8467
8640
|
: allocatedMsResult;
|
|
8468
8641
|
}
|
|
8469
|
-
if (typeof params.
|
|
8470
|
-
const
|
|
8471
|
-
params.
|
|
8642
|
+
if (typeof params.locks === "function") {
|
|
8643
|
+
const locksResult = params.locks(execution);
|
|
8644
|
+
params.locks = locksResult;
|
|
8645
|
+
}
|
|
8646
|
+
// what the file declares about itself wins over the plan: it is
|
|
8647
|
+
// closer to the reason
|
|
8648
|
+
if (
|
|
8649
|
+
directives.allocatedMs !== undefined &&
|
|
8650
|
+
directives.allocatedMs > params.allocatedMs
|
|
8651
|
+
) {
|
|
8652
|
+
params.allocatedMs = directives.allocatedMs;
|
|
8653
|
+
}
|
|
8654
|
+
if (directives.lockArray.length > 0) {
|
|
8655
|
+
params.locks = params.locks
|
|
8656
|
+
? [...new Set([...params.locks, ...directives.lockArray])]
|
|
8657
|
+
: directives.lockArray;
|
|
8472
8658
|
}
|
|
8473
8659
|
|
|
8474
8660
|
lastExecution = execution;
|
|
@@ -8664,7 +8850,7 @@ To fix this warning:
|
|
|
8664
8850
|
|
|
8665
8851
|
const executionRemainingSet = new Set(executionStartOrderArray);
|
|
8666
8852
|
const executionExecutingSet = new Set();
|
|
8667
|
-
const
|
|
8853
|
+
const lockedResourceSet = new Set();
|
|
8668
8854
|
const start = async (execution) => {
|
|
8669
8855
|
execution.fileExecutionCount = Object.keys(
|
|
8670
8856
|
testPlanResult.results[execution.fileRelativeUrl],
|
|
@@ -8678,9 +8864,9 @@ To fix this warning:
|
|
|
8678
8864
|
execution.result.status = "skipped";
|
|
8679
8865
|
execution.result.value = execution.skipReason;
|
|
8680
8866
|
} else {
|
|
8681
|
-
if (execution.params.
|
|
8682
|
-
for (const
|
|
8683
|
-
|
|
8867
|
+
if (execution.params.locks) {
|
|
8868
|
+
for (const resourceToLock of execution.params.locks) {
|
|
8869
|
+
lockedResourceSet.add(resourceToLock);
|
|
8684
8870
|
}
|
|
8685
8871
|
}
|
|
8686
8872
|
execution.status = "executing";
|
|
@@ -8698,9 +8884,9 @@ To fix this warning:
|
|
|
8698
8884
|
});
|
|
8699
8885
|
Object.assign(execution.result, executionResult);
|
|
8700
8886
|
execution.status = "executed";
|
|
8701
|
-
if (execution.params.
|
|
8702
|
-
for (const
|
|
8703
|
-
|
|
8887
|
+
if (execution.params.locks) {
|
|
8888
|
+
for (const resourceToRelease of execution.params.locks) {
|
|
8889
|
+
lockedResourceSet.delete(resourceToRelease);
|
|
8704
8890
|
}
|
|
8705
8891
|
}
|
|
8706
8892
|
if (timingsMemory) {
|
|
@@ -8776,13 +8962,14 @@ To fix this warning:
|
|
|
8776
8962
|
continue;
|
|
8777
8963
|
}
|
|
8778
8964
|
}
|
|
8779
|
-
if (executionCandidate.params.
|
|
8780
|
-
const
|
|
8781
|
-
(
|
|
8782
|
-
|
|
8783
|
-
|
|
8965
|
+
if (executionCandidate.params.locks) {
|
|
8966
|
+
const resourceLockedByAnother =
|
|
8967
|
+
executionCandidate.params.locks.find((resourceToLock) =>
|
|
8968
|
+
lockedResourceSet.has(resourceToLock),
|
|
8969
|
+
);
|
|
8970
|
+
if (resourceLockedByAnother) {
|
|
8784
8971
|
logger.debug(
|
|
8785
|
-
`"${
|
|
8972
|
+
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
|
|
8786
8973
|
);
|
|
8787
8974
|
continue;
|
|
8788
8975
|
}
|
|
@@ -11148,11 +11335,12 @@ const onceWorkerThreadEvent = (worker, type, callback) => {
|
|
|
11148
11335
|
};
|
|
11149
11336
|
|
|
11150
11337
|
/*
|
|
11151
|
-
*
|
|
11152
|
-
*
|
|
11153
|
-
*
|
|
11338
|
+
* Asks the test runner for more time from a test file, when the amount is
|
|
11339
|
+
* computed rather than known in advance (it depends on the platform, on how
|
|
11340
|
+
* many fixtures were found...). A fixed amount belongs in a directive instead,
|
|
11341
|
+
* which the runner reads without executing the file:
|
|
11154
11342
|
*
|
|
11155
|
-
*
|
|
11343
|
+
* "jsenv:allocate 90s";
|
|
11156
11344
|
*
|
|
11157
11345
|
* The request is sent to the process running the test plan, which restarts the
|
|
11158
11346
|
* timeout with the requested duration and remembers it: a file asking for more
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/test",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@c88/v8-coverage": "0.1.1",
|
|
34
34
|
"@jsenv/ast": "6.9.3",
|
|
35
|
-
"@jsenv/plugin-supervisor": "1.8.
|
|
36
|
-
"@jsenv/sourcemap": "1.4.
|
|
35
|
+
"@jsenv/plugin-supervisor": "1.8.15",
|
|
36
|
+
"@jsenv/sourcemap": "1.4.3",
|
|
37
37
|
"he": "1.2.0",
|
|
38
38
|
"istanbul-lib-coverage": "3.2.2",
|
|
39
39
|
"istanbul-lib-instrument": "6.0.3",
|
|
@@ -34,6 +34,7 @@ import stripAnsi from "strip-ansi";
|
|
|
34
34
|
import { generateCoverage } from "../coverage/generate_coverage.js";
|
|
35
35
|
import { createExecutionTimings } from "./execution_timings.js";
|
|
36
36
|
import { githubAnnotationFromError } from "./github_annotation_from_error.js";
|
|
37
|
+
import { readJsenvDirectives } from "./jsenv_directives.js";
|
|
37
38
|
import { createIsInsideFragment } from "./is_inside_fragment.js";
|
|
38
39
|
import { renderOutroContent, reporterList } from "./reporters/reporter_list.js";
|
|
39
40
|
import { run } from "./run.js";
|
|
@@ -745,6 +746,9 @@ To fix this warning:
|
|
|
745
746
|
}
|
|
746
747
|
}
|
|
747
748
|
const filePlan = meta.testPlan;
|
|
749
|
+
const directives = readJsenvDirectives(
|
|
750
|
+
new URL(relativeUrl, rootDirectoryUrl),
|
|
751
|
+
);
|
|
748
752
|
for (const groupName of Object.keys(filePlan)) {
|
|
749
753
|
const stepConfig = filePlan[groupName];
|
|
750
754
|
if (stepConfig === null || stepConfig === undefined) {
|
|
@@ -766,7 +770,7 @@ To fix this warning:
|
|
|
766
770
|
runtime,
|
|
767
771
|
runtimeParams,
|
|
768
772
|
allocatedMs = defaultMsAllocatedPerExecution,
|
|
769
|
-
|
|
773
|
+
locks,
|
|
770
774
|
} = stepConfig;
|
|
771
775
|
const params = {
|
|
772
776
|
measureMemoryUsage: true,
|
|
@@ -774,7 +778,7 @@ To fix this warning:
|
|
|
774
778
|
collectPerformance: false,
|
|
775
779
|
collectConsole: true,
|
|
776
780
|
allocatedMs,
|
|
777
|
-
|
|
781
|
+
locks,
|
|
778
782
|
runtime,
|
|
779
783
|
runtimeParams: {
|
|
780
784
|
rootDirectoryUrl,
|
|
@@ -834,9 +838,22 @@ To fix this warning:
|
|
|
834
838
|
? defaultMsAllocatedPerExecution
|
|
835
839
|
: allocatedMsResult;
|
|
836
840
|
}
|
|
837
|
-
if (typeof params.
|
|
838
|
-
const
|
|
839
|
-
params.
|
|
841
|
+
if (typeof params.locks === "function") {
|
|
842
|
+
const locksResult = params.locks(execution);
|
|
843
|
+
params.locks = locksResult;
|
|
844
|
+
}
|
|
845
|
+
// what the file declares about itself wins over the plan: it is
|
|
846
|
+
// closer to the reason
|
|
847
|
+
if (
|
|
848
|
+
directives.allocatedMs !== undefined &&
|
|
849
|
+
directives.allocatedMs > params.allocatedMs
|
|
850
|
+
) {
|
|
851
|
+
params.allocatedMs = directives.allocatedMs;
|
|
852
|
+
}
|
|
853
|
+
if (directives.lockArray.length > 0) {
|
|
854
|
+
params.locks = params.locks
|
|
855
|
+
? [...new Set([...params.locks, ...directives.lockArray])]
|
|
856
|
+
: directives.lockArray;
|
|
840
857
|
}
|
|
841
858
|
|
|
842
859
|
lastExecution = execution;
|
|
@@ -1032,7 +1049,7 @@ To fix this warning:
|
|
|
1032
1049
|
|
|
1033
1050
|
const executionRemainingSet = new Set(executionStartOrderArray);
|
|
1034
1051
|
const executionExecutingSet = new Set();
|
|
1035
|
-
const
|
|
1052
|
+
const lockedResourceSet = new Set();
|
|
1036
1053
|
const start = async (execution) => {
|
|
1037
1054
|
execution.fileExecutionCount = Object.keys(
|
|
1038
1055
|
testPlanResult.results[execution.fileRelativeUrl],
|
|
@@ -1046,9 +1063,9 @@ To fix this warning:
|
|
|
1046
1063
|
execution.result.status = "skipped";
|
|
1047
1064
|
execution.result.value = execution.skipReason;
|
|
1048
1065
|
} else {
|
|
1049
|
-
if (execution.params.
|
|
1050
|
-
for (const
|
|
1051
|
-
|
|
1066
|
+
if (execution.params.locks) {
|
|
1067
|
+
for (const resourceToLock of execution.params.locks) {
|
|
1068
|
+
lockedResourceSet.add(resourceToLock);
|
|
1052
1069
|
}
|
|
1053
1070
|
}
|
|
1054
1071
|
execution.status = "executing";
|
|
@@ -1066,9 +1083,9 @@ To fix this warning:
|
|
|
1066
1083
|
});
|
|
1067
1084
|
Object.assign(execution.result, executionResult);
|
|
1068
1085
|
execution.status = "executed";
|
|
1069
|
-
if (execution.params.
|
|
1070
|
-
for (const
|
|
1071
|
-
|
|
1086
|
+
if (execution.params.locks) {
|
|
1087
|
+
for (const resourceToRelease of execution.params.locks) {
|
|
1088
|
+
lockedResourceSet.delete(resourceToRelease);
|
|
1072
1089
|
}
|
|
1073
1090
|
}
|
|
1074
1091
|
if (timingsMemory) {
|
|
@@ -1144,13 +1161,14 @@ To fix this warning:
|
|
|
1144
1161
|
continue;
|
|
1145
1162
|
}
|
|
1146
1163
|
}
|
|
1147
|
-
if (executionCandidate.params.
|
|
1148
|
-
const
|
|
1149
|
-
(
|
|
1150
|
-
|
|
1151
|
-
|
|
1164
|
+
if (executionCandidate.params.locks) {
|
|
1165
|
+
const resourceLockedByAnother =
|
|
1166
|
+
executionCandidate.params.locks.find((resourceToLock) =>
|
|
1167
|
+
lockedResourceSet.has(resourceToLock),
|
|
1168
|
+
);
|
|
1169
|
+
if (resourceLockedByAnother) {
|
|
1152
1170
|
logger.debug(
|
|
1153
|
-
`"${
|
|
1171
|
+
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
|
|
1154
1172
|
);
|
|
1155
1173
|
continue;
|
|
1156
1174
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Reads what a test file declares about its own execution, from the directive
|
|
3
|
+
* prologue — the string literals a module may open with, as in "use strict" or
|
|
4
|
+
* "use client":
|
|
5
|
+
*
|
|
6
|
+
* "jsenv:allocate 90s";
|
|
7
|
+
* "jsenv:lock service-worker";
|
|
8
|
+
*
|
|
9
|
+
* They are read without running the file, which is what allows a lock to be
|
|
10
|
+
* honored: an execution must not have started before we know what it takes.
|
|
11
|
+
* Being grammar rather than convention, a directive cannot be built at runtime
|
|
12
|
+
* and cannot move: it is the first statement or it is nothing.
|
|
13
|
+
*
|
|
14
|
+
* A "jsenv:" directive that cannot be read throws. Everything ignores an
|
|
15
|
+
* unknown directive silently, so a typo would silently give the file back the
|
|
16
|
+
* default budget; the only way it stays useful is to be loud.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createDetailedMessage } from "@jsenv/humanize";
|
|
20
|
+
import { closeSync, openSync, readFileSync, readSync } from "node:fs";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const DIRECTIVE_PREFIX = "jsenv:";
|
|
24
|
+
const DIRECTIVE_NAMES = `"jsenv:allocate <duration>", "jsenv:lock <resource>"`;
|
|
25
|
+
const DURATION_REGEX = /^(\d+)(ms|s|m)$/;
|
|
26
|
+
const MS_PER_UNIT = { ms: 1, s: 1_000, m: 60_000 };
|
|
27
|
+
// a directive prologue sits at the top of the file; comments can precede it but
|
|
28
|
+
// rarely for more than a few lines, and the whole file is read when they do
|
|
29
|
+
const HEAD_BYTE_COUNT = 4096;
|
|
30
|
+
|
|
31
|
+
export const readJsenvDirectives = (fileUrl) => {
|
|
32
|
+
const head = readHead(fileUrl);
|
|
33
|
+
let scanResult = scanDirectivePrologue(head.text);
|
|
34
|
+
if (head.partial && !scanResult.complete) {
|
|
35
|
+
scanResult = scanDirectivePrologue(readFileSync(new URL(fileUrl), "utf8"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let allocatedMs;
|
|
39
|
+
const lockArray = [];
|
|
40
|
+
for (const directiveText of scanResult.directiveTextArray) {
|
|
41
|
+
if (!directiveText.startsWith(DIRECTIVE_PREFIX)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const body = directiveText.slice(DIRECTIVE_PREFIX.length);
|
|
45
|
+
const spaceIndex = body.indexOf(" ");
|
|
46
|
+
const name = spaceIndex === -1 ? body : body.slice(0, spaceIndex);
|
|
47
|
+
const argument = spaceIndex === -1 ? "" : body.slice(spaceIndex + 1).trim();
|
|
48
|
+
const fail = (reason, details = {}) => {
|
|
49
|
+
throw new Error(
|
|
50
|
+
createDetailedMessage(reason, {
|
|
51
|
+
directive: `"${directiveText}"`,
|
|
52
|
+
file: fileURLToPath(fileUrl),
|
|
53
|
+
...details,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (name === "allocate") {
|
|
59
|
+
const match = DURATION_REGEX.exec(argument);
|
|
60
|
+
if (!match) {
|
|
61
|
+
fail(`"jsenv:allocate" expects a duration, got "${argument}"`, {
|
|
62
|
+
["durations accepted"]: `"500ms", "90s", "2m"`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
allocatedMs = Number(match[1]) * MS_PER_UNIT[match[2]];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (name === "lock") {
|
|
69
|
+
if (argument === "") {
|
|
70
|
+
fail(`"jsenv:lock" expects the name of a resource`);
|
|
71
|
+
}
|
|
72
|
+
lockArray.push(argument);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
fail(`unknown jsenv directive "${name}"`, {
|
|
76
|
+
["directives available"]: DIRECTIVE_NAMES,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { allocatedMs, lockArray };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const readHead = (fileUrl) => {
|
|
83
|
+
const fileDescriptor = openSync(fileURLToPath(fileUrl), "r");
|
|
84
|
+
try {
|
|
85
|
+
const buffer = Buffer.allocUnsafe(HEAD_BYTE_COUNT);
|
|
86
|
+
const byteCount = readSync(fileDescriptor, buffer, 0, HEAD_BYTE_COUNT, 0);
|
|
87
|
+
return {
|
|
88
|
+
text: buffer.toString("utf8", 0, byteCount),
|
|
89
|
+
partial: byteCount === HEAD_BYTE_COUNT,
|
|
90
|
+
};
|
|
91
|
+
} finally {
|
|
92
|
+
closeSync(fileDescriptor);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* Collects the string literals opening the module, stopping at the first token
|
|
98
|
+
* that is neither a comment nor one of them. "complete" tells whether that
|
|
99
|
+
* token was reached: when it was not, the source given was cut short and the
|
|
100
|
+
* caller must read further before trusting the result.
|
|
101
|
+
*/
|
|
102
|
+
const scanDirectivePrologue = (source) => {
|
|
103
|
+
const directiveTextArray = [];
|
|
104
|
+
const length = source.length;
|
|
105
|
+
let index = 0;
|
|
106
|
+
if (source.startsWith("#!")) {
|
|
107
|
+
const lineEndIndex = source.indexOf("\n");
|
|
108
|
+
if (lineEndIndex === -1) {
|
|
109
|
+
return { directiveTextArray, complete: false };
|
|
110
|
+
}
|
|
111
|
+
index = lineEndIndex + 1;
|
|
112
|
+
}
|
|
113
|
+
while (index < length) {
|
|
114
|
+
const char = source[index];
|
|
115
|
+
if (
|
|
116
|
+
char === " " ||
|
|
117
|
+
char === "\t" ||
|
|
118
|
+
char === "\n" ||
|
|
119
|
+
char === "\r" ||
|
|
120
|
+
char === ";"
|
|
121
|
+
) {
|
|
122
|
+
index++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (char === "/" && source[index + 1] === "/") {
|
|
126
|
+
const lineEndIndex = source.indexOf("\n", index);
|
|
127
|
+
if (lineEndIndex === -1) {
|
|
128
|
+
return { directiveTextArray, complete: false };
|
|
129
|
+
}
|
|
130
|
+
index = lineEndIndex + 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (char === "/" && source[index + 1] === "*") {
|
|
134
|
+
const commentEndIndex = source.indexOf("*/", index + 2);
|
|
135
|
+
if (commentEndIndex === -1) {
|
|
136
|
+
return { directiveTextArray, complete: false };
|
|
137
|
+
}
|
|
138
|
+
index = commentEndIndex + 2;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (char === '"' || char === "'") {
|
|
142
|
+
const quote = char;
|
|
143
|
+
let stringIndex = index + 1;
|
|
144
|
+
let text = "";
|
|
145
|
+
while (stringIndex < length) {
|
|
146
|
+
const stringChar = source[stringIndex];
|
|
147
|
+
if (stringChar === "\\") {
|
|
148
|
+
text += source[stringIndex + 1];
|
|
149
|
+
stringIndex += 2;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (stringChar === quote) {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (stringChar === "\n") {
|
|
156
|
+
// an unterminated string is not a directive, and not our problem
|
|
157
|
+
return { directiveTextArray, complete: true };
|
|
158
|
+
}
|
|
159
|
+
text += stringChar;
|
|
160
|
+
stringIndex++;
|
|
161
|
+
}
|
|
162
|
+
if (stringIndex >= length) {
|
|
163
|
+
return { directiveTextArray, complete: false };
|
|
164
|
+
}
|
|
165
|
+
directiveTextArray.push(text);
|
|
166
|
+
index = stringIndex + 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
return { directiveTextArray, complete: true };
|
|
170
|
+
}
|
|
171
|
+
return { directiveTextArray, complete: false };
|
|
172
|
+
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* Asks the test runner for more time from a test file, when the amount is
|
|
3
|
+
* computed rather than known in advance (it depends on the platform, on how
|
|
4
|
+
* many fixtures were found...). A fixed amount belongs in a directive instead,
|
|
5
|
+
* which the runner reads without executing the file:
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* requestAllocatedMs(90_000);
|
|
7
|
+
* "jsenv:allocate 90s";
|
|
7
8
|
*
|
|
8
9
|
* The request is sent to the process running the test plan, which restarts the
|
|
9
10
|
* timeout with the requested duration and remembers it: a file asking for more
|