11import { createSupportsColor , isUnicodeSupported , stripAnsi , eastAsianWidth , clearTerminal , eraseLines } from "./jsenv_test_node_modules.js" ;
22import { 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" ;
44import { takeCoverage } from "node:v8" ;
55import { pathToFileURL , fileURLToPath } from "node:url" ;
66import { 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 + ) ( m s | 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+
59626132const 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