Skip to content

Commit fa1b5e3

Browse files
committed
work
1 parent a332eb9 commit fa1b5e3

7 files changed

Lines changed: 217 additions & 29 deletions

File tree

packages/related/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@jsenv/cli",
3-
"version": "0.3.157",
3+
"version": "0.3.158",
44
"type": "module",
55
"description": "Command Line Interface for jsenv",
66
"repository": {

packages/related/cli/template-node-package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@jsenv/assert": "4.5.7",
1616
"@jsenv/core": "41.4.30",
1717
"@jsenv/eslint-config-relax": "1.9.0",
18-
"@jsenv/test": "3.7.36",
18+
"@jsenv/test": "3.7.37",
1919
"eslint": "9.39.2",
2020
"prettier": "3.9.6"
2121
}

packages/related/cli/template-web-components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"@jsenv/plugin-bundling": "2.10.19",
2323
"@jsenv/plugin-minification": "1.7.11",
2424
"@jsenv/eslint-config-relax": "1.9.0",
25-
"@jsenv/test": "3.7.36",
25+
"@jsenv/test": "3.7.37",
2626
"eslint": "9.39.2",
2727
"open": "11.0.1",
2828
"@playwright/browser-chromium": "1.62.1",

packages/related/cli/template-web-preact/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@jsenv/plugin-bundling": "2.10.19",
2727
"@jsenv/plugin-minification": "1.7.11",
2828
"@jsenv/eslint-config-relax": "1.9.0",
29-
"@jsenv/test": "3.7.36",
29+
"@jsenv/test": "3.7.37",
3030
"eslint": "9.39.2",
3131
"open": "11.0.1",
3232
"@playwright/browser-chromium": "1.62.1",

packages/related/cli/template-web-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"@jsenv/plugin-bundling": "2.10.19",
2828
"@jsenv/plugin-minification": "1.7.11",
2929
"@jsenv/eslint-config-relax": "1.9.0",
30-
"@jsenv/test": "3.7.36",
30+
"@jsenv/test": "3.7.37",
3131
"eslint": "9.39.2",
3232
"open": "11.0.1",
3333
"@playwright/browser-chromium": "1.62.1",

packages/related/cli/template-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"@jsenv/eslint-config-relax": "1.9.0",
2121
"@jsenv/plugin-bundling": "2.10.19",
2222
"@jsenv/plugin-minification": "1.7.11",
23-
"@jsenv/test": "3.7.36",
23+
"@jsenv/test": "3.7.37",
2424
"eslint": "9.39.2",
2525
"open": "11.0.1",
2626
"@playwright/browser-chromium": "1.62.1",

packages/related/test/dist/jsenv_test.js

Lines changed: 211 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createSupportsColor, isUnicodeSupported, stripAnsi, eastAsianWidth, clearTerminal, eraseLines } from "./jsenv_test_node_modules.js";
22
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";
44
import { takeCoverage } from "node:v8";
55
import { pathToFileURL, fileURLToPath } from "node:url";
66
import { createRequire } from "node:module";
@@ -5959,6 +5959,176 @@ const replaceUrls$1 = (source, replace) => {
59595959
});
59605960
};
59615961

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+
59626132
const createIsInsideFragment = (fragment, total) => {
59636133
let [dividend, divisor] = fragment.split("/");
59646134
dividend = parseInt(dividend);
@@ -8377,6 +8547,9 @@ To fix this warning:
83778547
}
83788548
}
83798549
const filePlan = meta.testPlan;
8550+
const directives = readJsenvDirectives(
8551+
new URL(relativeUrl, rootDirectoryUrl),
8552+
);
83808553
for (const groupName of Object.keys(filePlan)) {
83818554
const stepConfig = filePlan[groupName];
83828555
if (stepConfig === null || stepConfig === undefined) {
@@ -8398,15 +8571,15 @@ To fix this warning:
83988571
runtime,
83998572
runtimeParams,
84008573
allocatedMs = defaultMsAllocatedPerExecution,
8401-
uses,
8574+
locks,
84028575
} = stepConfig;
84038576
const params = {
84048577
measureMemoryUsage: true,
84058578
measurePerformance: false,
84068579
collectPerformance: false,
84078580
collectConsole: true,
84088581
allocatedMs,
8409-
uses,
8582+
locks,
84108583
runtime,
84118584
runtimeParams: {
84128585
rootDirectoryUrl,
@@ -8466,9 +8639,22 @@ To fix this warning:
84668639
? defaultMsAllocatedPerExecution
84678640
: allocatedMsResult;
84688641
}
8469-
if (typeof params.uses === "function") {
8470-
const usesResult = params.uses(execution);
8471-
params.uses = usesResult;
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;
84728658
}
84738659

84748660
lastExecution = execution;
@@ -8664,7 +8850,7 @@ To fix this warning:
86648850

86658851
const executionRemainingSet = new Set(executionStartOrderArray);
86668852
const executionExecutingSet = new Set();
8667-
const usedTagSet = new Set();
8853+
const lockedResourceSet = new Set();
86688854
const start = async (execution) => {
86698855
execution.fileExecutionCount = Object.keys(
86708856
testPlanResult.results[execution.fileRelativeUrl],
@@ -8678,9 +8864,9 @@ To fix this warning:
86788864
execution.result.status = "skipped";
86798865
execution.result.value = execution.skipReason;
86808866
} else {
8681-
if (execution.params.uses) {
8682-
for (const tagThatWillBeUsed of execution.params.uses) {
8683-
usedTagSet.add(tagThatWillBeUsed);
8867+
if (execution.params.locks) {
8868+
for (const resourceToLock of execution.params.locks) {
8869+
lockedResourceSet.add(resourceToLock);
86848870
}
86858871
}
86868872
execution.status = "executing";
@@ -8698,9 +8884,9 @@ To fix this warning:
86988884
});
86998885
Object.assign(execution.result, executionResult);
87008886
execution.status = "executed";
8701-
if (execution.params.uses) {
8702-
for (const tagNoLongerInUse of execution.params.uses) {
8703-
usedTagSet.delete(tagNoLongerInUse);
8887+
if (execution.params.locks) {
8888+
for (const resourceToRelease of execution.params.locks) {
8889+
lockedResourceSet.delete(resourceToRelease);
87048890
}
87058891
}
87068892
if (timingsMemory) {
@@ -8776,13 +8962,14 @@ To fix this warning:
87768962
continue;
87778963
}
87788964
}
8779-
if (executionCandidate.params.uses) {
8780-
const nonAvailableTag = executionCandidate.params.uses.find(
8781-
(tagToUse) => usedTagSet.has(tagToUse),
8782-
);
8783-
if (nonAvailableTag) {
8965+
if (executionCandidate.params.locks) {
8966+
const resourceLockedByAnother =
8967+
executionCandidate.params.locks.find((resourceToLock) =>
8968+
lockedResourceSet.has(resourceToLock),
8969+
);
8970+
if (resourceLockedByAnother) {
87848971
logger.debug(
8785-
`"${nonAvailableTag}" is not available, ${executionCandidate.name} will wait until it is released by a previous execution`,
8972+
`"${resourceLockedByAnother}" is locked, ${executionCandidate.name} will wait until it is released by a previous execution`,
87868973
);
87878974
continue;
87888975
}
@@ -11148,11 +11335,12 @@ const onceWorkerThreadEvent = (worker, type, callback) => {
1114811335
};
1114911336

1115011337
/*
11151-
* Called from a test file to tell the test runner how much time this file needs:
11152-
*
11153-
* import { requestAllocatedMs } from "@jsenv/test";
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:
1115411342
*
11155-
* requestAllocatedMs(90_000);
11343+
* "jsenv:allocate 90s";
1115611344
*
1115711345
* The request is sent to the process running the test plan, which restarts the
1115811346
* timeout with the requested duration and remembers it: a file asking for more

0 commit comments

Comments
 (0)