@jsenv/test 3.7.36 → 3.7.37

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.
@@ -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
- uses,
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
- uses,
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.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;
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 usedTagSet = new Set();
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.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);
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.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);
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.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) {
8784
8971
  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`,
8786
8973
  );
8787
8974
  continue;
8788
8975
  }
@@ -11148,11 +11335,12 @@ const onceWorkerThreadEvent = (worker, type, callback) => {
11148
11335
  };
11149
11336
 
11150
11337
  /*
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:
11154
11342
  *
11155
- * requestAllocatedMs(90_000);
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.36",
3
+ "version": "3.7.37",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- uses,
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
- uses,
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.uses === "function") {
838
- const usesResult = params.uses(execution);
839
- params.uses = usesResult;
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 usedTagSet = new Set();
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.uses) {
1050
- for (const tagThatWillBeUsed of execution.params.uses) {
1051
- usedTagSet.add(tagThatWillBeUsed);
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.uses) {
1070
- for (const tagNoLongerInUse of execution.params.uses) {
1071
- usedTagSet.delete(tagNoLongerInUse);
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.uses) {
1148
- const nonAvailableTag = executionCandidate.params.uses.find(
1149
- (tagToUse) => usedTagSet.has(tagToUse),
1150
- );
1151
- if (nonAvailableTag) {
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
- `"${nonAvailableTag}" is not available, ${executionCandidate.name} will wait until it is released by a previous execution`,
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
- * Called from a test file to tell the test runner how much time this file needs:
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
- * import { requestAllocatedMs } from "@jsenv/test";
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