@jsenv/test 1.8.1 → 2.1.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.
@@ -39,6 +39,7 @@ const isWindowsPathnameSpecifier = (specifier) => {
39
39
  const hasScheme = (specifier) => /^[a-zA-Z]+:/.test(specifier);
40
40
 
41
41
  const resolveAssociations = (associations, baseUrl) => {
42
+ if (baseUrl && typeof baseUrl.href === "string") baseUrl = baseUrl.href;
42
43
  assertUrlLike(baseUrl, "baseUrl");
43
44
  const associationsResolved = {};
44
45
  Object.keys(associations).forEach((key) => {
@@ -122,6 +123,7 @@ const asFlatAssociations = (associations) => {
122
123
  */
123
124
  const applyPatternMatching = ({ url, pattern }) => {
124
125
  assertUrlLike(pattern, "pattern");
126
+ if (url && typeof url.href === "string") url = url.href;
125
127
  assertUrlLike(url, "url");
126
128
  const { matched, patternIndex, index, groups } = applyMatching(pattern, url);
127
129
  const matchGroups = [];
@@ -369,6 +371,7 @@ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
369
371
  };
370
372
 
371
373
  const applyAssociations = ({ url, associations }) => {
374
+ if (url && typeof url.href === "string") url = url.href;
372
375
  assertUrlLike(url);
373
376
  const flatAssociations = asFlatAssociations(associations);
374
377
  return Object.keys(flatAssociations).reduce((previousValue, pattern) => {
@@ -418,6 +421,7 @@ const applyAliases = ({ url, aliases }) => {
418
421
  };
419
422
 
420
423
  const urlChildMayMatch = ({ url, associations, predicate }) => {
424
+ if (url && typeof url.href === "string") url = url.href;
421
425
  assertUrlLike(url, "url");
422
426
  // the function was meants to be used on url ending with '/'
423
427
  if (!url.endsWith("/")) {
@@ -1,12 +1,12 @@
1
- import { chmod, stat, lstat, readdir, promises, unlink, openSync, closeSync, rmdir, readFile as readFile$1, writeFile as writeFile$1, writeFileSync as writeFileSync$1, mkdirSync, readFileSync, readdirSync, existsSync } from "node:fs";
1
+ import os from "node:os";
2
+ import { readdir, chmod, stat, lstat, promises, readFile as readFile$1, writeFile as writeFile$1, writeFileSync as writeFileSync$1, mkdirSync, unlink, openSync, closeSync, rmdir, readFileSync, readdirSync, existsSync } from "node:fs";
2
3
  import process$1, { memoryUsage } from "node:process";
3
4
  import v8, { takeCoverage } from "node:v8";
4
5
  import stripAnsi from "strip-ansi";
5
6
  import { URL_META, filterV8Coverage } from "./js/v8_coverage.js";
6
7
  import { pathToFileURL, fileURLToPath } from "node:url";
7
- import crypto from "node:crypto";
8
8
  import { dirname } from "node:path";
9
- import os from "node:os";
9
+ import crypto from "node:crypto";
10
10
  import tty from "node:tty";
11
11
  import stringWidth from "string-width";
12
12
  import { readGitHubWorkflowEnv, startGithubCheckRun } from "@jsenv/github-check-run";
@@ -17,7 +17,7 @@ import { createServer } from "node:net";
17
17
  import wrapAnsi from "wrap-ansi";
18
18
  import { runInNewContext } from "node:vm";
19
19
  import { injectSupervisorIntoHTML, supervisorFileUrl } from "@jsenv/plugin-supervisor";
20
- import { SOURCEMAP, generateSourcemapDataUrl } from "@jsenv/sourcemap";
20
+ import { generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
21
21
  import { findFreePort } from "@jsenv/server";
22
22
  import { Worker } from "node:worker_threads";
23
23
 
@@ -865,15 +865,158 @@ const assertAndNormalizeFileUrl = (
865
865
  return value;
866
866
  };
867
867
 
868
- const statsToType = (stats) => {
869
- if (stats.isFile()) return "file";
870
- if (stats.isDirectory()) return "directory";
871
- if (stats.isSymbolicLink()) return "symbolic-link";
872
- if (stats.isFIFO()) return "fifo";
873
- if (stats.isSocket()) return "socket";
874
- if (stats.isCharacterDevice()) return "character-device";
875
- if (stats.isBlockDevice()) return "block-device";
876
- return undefined;
868
+ const comparePathnames = (leftPathame, rightPathname) => {
869
+ const leftPartArray = leftPathame.split("/");
870
+ const rightPartArray = rightPathname.split("/");
871
+
872
+ const leftLength = leftPartArray.length;
873
+ const rightLength = rightPartArray.length;
874
+
875
+ const maxLength = Math.max(leftLength, rightLength);
876
+ let i = 0;
877
+ while (i < maxLength) {
878
+ const leftPartExists = i in leftPartArray;
879
+ const rightPartExists = i in rightPartArray;
880
+
881
+ // longer comes first
882
+ if (!leftPartExists) {
883
+ return +1;
884
+ }
885
+ if (!rightPartExists) {
886
+ return -1;
887
+ }
888
+
889
+ const leftPartIsLast = i === leftPartArray.length - 1;
890
+ const rightPartIsLast = i === rightPartArray.length - 1;
891
+ // folder comes first
892
+ if (leftPartIsLast && !rightPartIsLast) {
893
+ return +1;
894
+ }
895
+ if (!leftPartIsLast && rightPartIsLast) {
896
+ return -1;
897
+ }
898
+
899
+ const leftPart = leftPartArray[i];
900
+ const rightPart = rightPartArray[i];
901
+ i++;
902
+ // local comparison comes first
903
+ const comparison = leftPart.localeCompare(rightPart);
904
+ if (comparison !== 0) {
905
+ return comparison;
906
+ }
907
+ }
908
+
909
+ if (leftLength < rightLength) {
910
+ return +1;
911
+ }
912
+ if (leftLength > rightLength) {
913
+ return -1;
914
+ }
915
+ return 0;
916
+ };
917
+
918
+ const isWindows$2 = process.platform === "win32";
919
+ const baseUrlFallback = fileSystemPathToUrl(process.cwd());
920
+
921
+ /**
922
+ * Some url might be resolved or remapped to url without the windows drive letter.
923
+ * For instance
924
+ * new URL('/foo.js', 'file:///C:/dir/file.js')
925
+ * resolves to
926
+ * 'file:///foo.js'
927
+ *
928
+ * But on windows it becomes a problem because we need the drive letter otherwise
929
+ * url cannot be converted to a filesystem path.
930
+ *
931
+ * ensureWindowsDriveLetter ensure a resolved url still contains the drive letter.
932
+ */
933
+
934
+ const ensureWindowsDriveLetter = (url, baseUrl) => {
935
+ try {
936
+ url = String(new URL(url));
937
+ } catch (e) {
938
+ throw new Error(`absolute url expected but got ${url}`);
939
+ }
940
+
941
+ if (!isWindows$2) {
942
+ return url;
943
+ }
944
+
945
+ try {
946
+ baseUrl = String(new URL(baseUrl));
947
+ } catch (e) {
948
+ throw new Error(
949
+ `absolute baseUrl expected but got ${baseUrl} to ensure windows drive letter on ${url}`,
950
+ );
951
+ }
952
+
953
+ if (!url.startsWith("file://")) {
954
+ return url;
955
+ }
956
+ const afterProtocol = url.slice("file://".length);
957
+ // we still have the windows drive letter
958
+ if (extractDriveLetter(afterProtocol)) {
959
+ return url;
960
+ }
961
+
962
+ // drive letter was lost, restore it
963
+ const baseUrlOrFallback = baseUrl.startsWith("file://")
964
+ ? baseUrl
965
+ : baseUrlFallback;
966
+ const driveLetter = extractDriveLetter(
967
+ baseUrlOrFallback.slice("file://".length),
968
+ );
969
+ if (!driveLetter) {
970
+ throw new Error(
971
+ `drive letter expected on baseUrl but got ${baseUrl} to ensure windows drive letter on ${url}`,
972
+ );
973
+ }
974
+ return `file:///${driveLetter}:${afterProtocol}`;
975
+ };
976
+
977
+ const extractDriveLetter = (resource) => {
978
+ // we still have the windows drive letter
979
+ if (/[a-zA-Z]/.test(resource[1]) && resource[2] === ":") {
980
+ return resource[1];
981
+ }
982
+ return null;
983
+ };
984
+
985
+ const readDirectory = async (url, { emfileMaxWait = 1000 } = {}) => {
986
+ const directoryUrl = assertAndNormalizeDirectoryUrl(url);
987
+ const directoryUrlObject = new URL(directoryUrl);
988
+ const startMs = Date.now();
989
+ let attemptCount = 0;
990
+
991
+ const attempt = async () => {
992
+ try {
993
+ const names = await new Promise((resolve, reject) => {
994
+ readdir(directoryUrlObject, (error, names) => {
995
+ if (error) {
996
+ reject(error);
997
+ } else {
998
+ resolve(names);
999
+ }
1000
+ });
1001
+ });
1002
+ return names.map(encodeURIComponent);
1003
+ } catch (e) {
1004
+ // https://nodejs.org/dist/latest-v13.x/docs/api/errors.html#errors_common_system_errors
1005
+ if (e.code === "EMFILE" || e.code === "ENFILE") {
1006
+ attemptCount++;
1007
+ const nowMs = Date.now();
1008
+ const timeSpentWaiting = nowMs - startMs;
1009
+ if (timeSpentWaiting > emfileMaxWait) {
1010
+ throw e;
1011
+ }
1012
+ await new Promise((resolve) => setTimeout(resolve), attemptCount);
1013
+ return await attempt();
1014
+ }
1015
+ throw e;
1016
+ }
1017
+ };
1018
+
1019
+ return attempt();
877
1020
  };
878
1021
 
879
1022
  // https://github.com/coderaiser/cloudcmd/issues/63#issuecomment-195478143
@@ -996,7 +1139,7 @@ const getPermissionOrComputeDefault = (action, subject, permissions) => {
996
1139
  */
997
1140
 
998
1141
 
999
- const isWindows$2 = process.platform === "win32";
1142
+ const isWindows$1 = process.platform === "win32";
1000
1143
 
1001
1144
  const readEntryStat = async (
1002
1145
  source,
@@ -1016,7 +1159,7 @@ const readEntryStat = async (
1016
1159
  return readStat(sourcePath, {
1017
1160
  followLink,
1018
1161
  ...handleNotFoundOption,
1019
- ...(isWindows$2
1162
+ ...(isWindows$1
1020
1163
  ? {
1021
1164
  // Windows can EPERM on stat
1022
1165
  handlePermissionDeniedError: async (error) => {
@@ -1081,93 +1224,6 @@ const readStat = (
1081
1224
  });
1082
1225
  };
1083
1226
 
1084
- const readDirectory = async (url, { emfileMaxWait = 1000 } = {}) => {
1085
- const directoryUrl = assertAndNormalizeDirectoryUrl(url);
1086
- const directoryUrlObject = new URL(directoryUrl);
1087
- const startMs = Date.now();
1088
- let attemptCount = 0;
1089
-
1090
- const attempt = async () => {
1091
- try {
1092
- const names = await new Promise((resolve, reject) => {
1093
- readdir(directoryUrlObject, (error, names) => {
1094
- if (error) {
1095
- reject(error);
1096
- } else {
1097
- resolve(names);
1098
- }
1099
- });
1100
- });
1101
- return names.map(encodeURIComponent);
1102
- } catch (e) {
1103
- // https://nodejs.org/dist/latest-v13.x/docs/api/errors.html#errors_common_system_errors
1104
- if (e.code === "EMFILE" || e.code === "ENFILE") {
1105
- attemptCount++;
1106
- const nowMs = Date.now();
1107
- const timeSpentWaiting = nowMs - startMs;
1108
- if (timeSpentWaiting > emfileMaxWait) {
1109
- throw e;
1110
- }
1111
- await new Promise((resolve) => setTimeout(resolve), attemptCount);
1112
- return await attempt();
1113
- }
1114
- throw e;
1115
- }
1116
- };
1117
-
1118
- return attempt();
1119
- };
1120
-
1121
- const comparePathnames = (leftPathame, rightPathname) => {
1122
- const leftPartArray = leftPathame.split("/");
1123
- const rightPartArray = rightPathname.split("/");
1124
-
1125
- const leftLength = leftPartArray.length;
1126
- const rightLength = rightPartArray.length;
1127
-
1128
- const maxLength = Math.max(leftLength, rightLength);
1129
- let i = 0;
1130
- while (i < maxLength) {
1131
- const leftPartExists = i in leftPartArray;
1132
- const rightPartExists = i in rightPartArray;
1133
-
1134
- // longer comes first
1135
- if (!leftPartExists) {
1136
- return +1;
1137
- }
1138
- if (!rightPartExists) {
1139
- return -1;
1140
- }
1141
-
1142
- const leftPartIsLast = i === leftPartArray.length - 1;
1143
- const rightPartIsLast = i === rightPartArray.length - 1;
1144
- // folder comes first
1145
- if (leftPartIsLast && !rightPartIsLast) {
1146
- return +1;
1147
- }
1148
- if (!leftPartIsLast && rightPartIsLast) {
1149
- return -1;
1150
- }
1151
-
1152
- const leftPart = leftPartArray[i];
1153
- const rightPart = rightPartArray[i];
1154
- i++;
1155
- // local comparison comes first
1156
- const comparison = leftPart.localeCompare(rightPart);
1157
- if (comparison !== 0) {
1158
- return comparison;
1159
- }
1160
- }
1161
-
1162
- if (leftLength < rightLength) {
1163
- return +1;
1164
- }
1165
- if (leftLength > rightLength) {
1166
- return -1;
1167
- }
1168
- return 0;
1169
- };
1170
-
1171
1227
  const collectFiles = async ({
1172
1228
  signal = new AbortController().signal,
1173
1229
  directoryUrl,
@@ -1259,39 +1315,130 @@ const collectFiles = async ({
1259
1315
  }
1260
1316
  };
1261
1317
 
1262
- // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_fspromises_mkdir_path_options
1263
- const { mkdir } = promises;
1264
-
1265
- const writeDirectory = async (
1266
- destination,
1267
- { recursive = true, allowUseless = false } = {},
1268
- ) => {
1269
- const destinationUrl = assertAndNormalizeDirectoryUrl(destination);
1270
- const destinationPath = urlToFileSystemPath(destinationUrl);
1271
-
1272
- const destinationStats = await readEntryStat(destinationUrl, {
1273
- nullIfNotFound: true,
1274
- followLink: false,
1275
- });
1276
-
1277
- if (destinationStats) {
1278
- if (destinationStats.isDirectory()) {
1279
- if (allowUseless) {
1280
- return;
1281
- }
1282
- throw new Error(`directory already exists at ${destinationPath}`);
1318
+ const statsToType = (stats) => {
1319
+ if (stats.isFile()) return "file";
1320
+ if (stats.isDirectory()) return "directory";
1321
+ if (stats.isSymbolicLink()) return "symbolic-link";
1322
+ if (stats.isFIFO()) return "fifo";
1323
+ if (stats.isSocket()) return "socket";
1324
+ if (stats.isCharacterDevice()) return "character-device";
1325
+ if (stats.isBlockDevice()) return "block-device";
1326
+ return undefined;
1327
+ };
1328
+
1329
+ // https://nodejs.org/dist/latest-v13.x/docs/api/fs.html#fs_fspromises_mkdir_path_options
1330
+ const { mkdir } = promises;
1331
+
1332
+ const writeDirectory = async (
1333
+ destination,
1334
+ { recursive = true, allowUseless = false } = {},
1335
+ ) => {
1336
+ const destinationUrl = assertAndNormalizeDirectoryUrl(destination);
1337
+ const destinationPath = urlToFileSystemPath(destinationUrl);
1338
+
1339
+ const destinationStats = await readEntryStat(destinationUrl, {
1340
+ nullIfNotFound: true,
1341
+ followLink: false,
1342
+ });
1343
+
1344
+ if (destinationStats) {
1345
+ if (destinationStats.isDirectory()) {
1346
+ if (allowUseless) {
1347
+ return;
1348
+ }
1349
+ throw new Error(`directory already exists at ${destinationPath}`);
1350
+ }
1351
+
1352
+ const destinationType = statsToType(destinationStats);
1353
+ throw new Error(
1354
+ `cannot write directory at ${destinationPath} because there is a ${destinationType}`,
1355
+ );
1356
+ }
1357
+
1358
+ try {
1359
+ await mkdir(destinationPath, { recursive });
1360
+ } catch (error) {
1361
+ if (allowUseless && error.code === "EEXIST") {
1362
+ return;
1363
+ }
1364
+ throw error;
1365
+ }
1366
+ };
1367
+
1368
+ const ensureParentDirectories = async (destination) => {
1369
+ const destinationUrl = assertAndNormalizeFileUrl(destination);
1370
+ const destinationPath = urlToFileSystemPath(destinationUrl);
1371
+ const destinationParentPath = dirname(destinationPath);
1372
+
1373
+ await writeDirectory(destinationParentPath, {
1374
+ recursive: true,
1375
+ allowUseless: true,
1376
+ });
1377
+ };
1378
+
1379
+ const readFile = async (value, { as = "buffer" } = {}) => {
1380
+ const fileUrl = assertAndNormalizeFileUrl(value);
1381
+ const buffer = await new Promise((resolve, reject) => {
1382
+ readFile$1(new URL(fileUrl), (error, buffer) => {
1383
+ if (error) {
1384
+ reject(error);
1385
+ } else {
1386
+ resolve(buffer);
1387
+ }
1388
+ });
1389
+ });
1390
+ if (as === "buffer") {
1391
+ return buffer;
1392
+ }
1393
+ if (as === "string") {
1394
+ return buffer.toString();
1395
+ }
1396
+ if (as === "json") {
1397
+ return JSON.parse(buffer.toString());
1398
+ }
1399
+ throw new Error(
1400
+ `"as" must be one of "buffer","string","json" received "${as}"`,
1401
+ );
1402
+ };
1403
+
1404
+ const writeFile = async (destination, content = "") => {
1405
+ const destinationUrl = assertAndNormalizeFileUrl(destination);
1406
+ const destinationUrlObject = new URL(destinationUrl);
1407
+ try {
1408
+ await writeFileNaive(destinationUrlObject, content);
1409
+ } catch (error) {
1410
+ if (error.code === "ENOENT") {
1411
+ await ensureParentDirectories(destinationUrl);
1412
+ await writeFileNaive(destinationUrlObject, content);
1413
+ return;
1283
1414
  }
1284
-
1285
- const destinationType = statsToType(destinationStats);
1286
- throw new Error(
1287
- `cannot write directory at ${destinationPath} because there is a ${destinationType}`,
1288
- );
1415
+ throw error;
1289
1416
  }
1417
+ };
1418
+
1419
+ const writeFileNaive = (urlObject, content) => {
1420
+ return new Promise((resolve, reject) => {
1421
+ writeFile$1(urlObject, content, (error) => {
1422
+ if (error) {
1423
+ reject(error);
1424
+ } else {
1425
+ resolve();
1426
+ }
1427
+ });
1428
+ });
1429
+ };
1290
1430
 
1431
+ const writeFileSync = (destination, content = "") => {
1432
+ const destinationUrl = assertAndNormalizeFileUrl(destination);
1433
+ const destinationUrlObject = new URL(destinationUrl);
1291
1434
  try {
1292
- await mkdir(destinationPath, { recursive });
1435
+ writeFileSync$1(destinationUrlObject, content);
1293
1436
  } catch (error) {
1294
- if (allowUseless && error.code === "EEXIST") {
1437
+ if (error.code === "ENOENT") {
1438
+ mkdirSync(new URL("./", destinationUrlObject), {
1439
+ recursive: true,
1440
+ });
1441
+ writeFileSync$1(destinationUrlObject, content);
1295
1442
  return;
1296
1443
  }
1297
1444
  throw error;
@@ -1543,6 +1690,18 @@ const removeDirectoryNaive = (
1543
1690
  });
1544
1691
  };
1545
1692
 
1693
+ process.platform === "win32";
1694
+
1695
+ /*
1696
+ * - stats object documentation on Node.js
1697
+ * https://nodejs.org/docs/latest-v13.x/api/fs.html#fs_class_fs_stats
1698
+ */
1699
+
1700
+
1701
+ process.platform === "win32";
1702
+
1703
+ process.platform === "win32";
1704
+
1546
1705
  const ensureEmptyDirectory = async (source) => {
1547
1706
  const stats = await readEntryStat(source, {
1548
1707
  nullIfNotFound: true,
@@ -1550,15 +1709,17 @@ const ensureEmptyDirectory = async (source) => {
1550
1709
  });
1551
1710
  if (stats === null) {
1552
1711
  // if there is nothing, create a directory
1553
- return writeDirectory(source, { allowUseless: true });
1712
+ await writeDirectory(source, { allowUseless: true });
1713
+ return;
1554
1714
  }
1555
1715
  if (stats.isDirectory()) {
1556
1716
  // if there is a directory remove its content and done
1557
- return removeEntry(source, {
1717
+ await removeEntry(source, {
1558
1718
  allowUseless: true,
1559
1719
  recursive: true,
1560
1720
  onlyContent: true,
1561
1721
  });
1722
+ return;
1562
1723
  }
1563
1724
 
1564
1725
  const sourceType = statsToType(stats);
@@ -1568,159 +1729,10 @@ const ensureEmptyDirectory = async (source) => {
1568
1729
  );
1569
1730
  };
1570
1731
 
1571
- const ensureParentDirectories = async (destination) => {
1572
- const destinationUrl = assertAndNormalizeFileUrl(destination);
1573
- const destinationPath = urlToFileSystemPath(destinationUrl);
1574
- const destinationParentPath = dirname(destinationPath);
1575
-
1576
- return writeDirectory(destinationParentPath, {
1577
- recursive: true,
1578
- allowUseless: true,
1579
- });
1580
- };
1581
-
1582
- const isWindows$1 = process.platform === "win32";
1583
- const baseUrlFallback = fileSystemPathToUrl(process.cwd());
1584
-
1585
- /**
1586
- * Some url might be resolved or remapped to url without the windows drive letter.
1587
- * For instance
1588
- * new URL('/foo.js', 'file:///C:/dir/file.js')
1589
- * resolves to
1590
- * 'file:///foo.js'
1591
- *
1592
- * But on windows it becomes a problem because we need the drive letter otherwise
1593
- * url cannot be converted to a filesystem path.
1594
- *
1595
- * ensureWindowsDriveLetter ensure a resolved url still contains the drive letter.
1596
- */
1597
-
1598
- const ensureWindowsDriveLetter = (url, baseUrl) => {
1599
- try {
1600
- url = String(new URL(url));
1601
- } catch (e) {
1602
- throw new Error(`absolute url expected but got ${url}`);
1603
- }
1604
-
1605
- if (!isWindows$1) {
1606
- return url;
1607
- }
1608
-
1609
- try {
1610
- baseUrl = String(new URL(baseUrl));
1611
- } catch (e) {
1612
- throw new Error(
1613
- `absolute baseUrl expected but got ${baseUrl} to ensure windows drive letter on ${url}`,
1614
- );
1615
- }
1616
-
1617
- if (!url.startsWith("file://")) {
1618
- return url;
1619
- }
1620
- const afterProtocol = url.slice("file://".length);
1621
- // we still have the windows drive letter
1622
- if (extractDriveLetter(afterProtocol)) {
1623
- return url;
1624
- }
1625
-
1626
- // drive letter was lost, restore it
1627
- const baseUrlOrFallback = baseUrl.startsWith("file://")
1628
- ? baseUrl
1629
- : baseUrlFallback;
1630
- const driveLetter = extractDriveLetter(
1631
- baseUrlOrFallback.slice("file://".length),
1632
- );
1633
- if (!driveLetter) {
1634
- throw new Error(
1635
- `drive letter expected on baseUrl but got ${baseUrl} to ensure windows drive letter on ${url}`,
1636
- );
1637
- }
1638
- return `file:///${driveLetter}:${afterProtocol}`;
1639
- };
1640
-
1641
- const extractDriveLetter = (resource) => {
1642
- // we still have the windows drive letter
1643
- if (/[a-zA-Z]/.test(resource[1]) && resource[2] === ":") {
1644
- return resource[1];
1645
- }
1646
- return null;
1647
- };
1648
-
1649
- process.platform === "win32";
1650
-
1651
- const readFile = async (value, { as = "buffer" } = {}) => {
1652
- const fileUrl = assertAndNormalizeFileUrl(value);
1653
- const buffer = await new Promise((resolve, reject) => {
1654
- readFile$1(new URL(fileUrl), (error, buffer) => {
1655
- if (error) {
1656
- reject(error);
1657
- } else {
1658
- resolve(buffer);
1659
- }
1660
- });
1661
- });
1662
- if (as === "buffer") {
1663
- return buffer;
1664
- }
1665
- if (as === "string") {
1666
- return buffer.toString();
1667
- }
1668
- if (as === "json") {
1669
- return JSON.parse(buffer.toString());
1670
- }
1671
- throw new Error(
1672
- `"as" must be one of "buffer","string","json" received "${as}"`,
1673
- );
1674
- };
1675
-
1676
1732
  process.platform === "win32";
1677
1733
 
1678
1734
  process.platform === "linux";
1679
1735
 
1680
- const writeFile = async (destination, content = "") => {
1681
- const destinationUrl = assertAndNormalizeFileUrl(destination);
1682
- const destinationUrlObject = new URL(destinationUrl);
1683
- try {
1684
- await writeFileNaive(destinationUrlObject, content);
1685
- } catch (error) {
1686
- if (error.code === "ENOENT") {
1687
- await ensureParentDirectories(destinationUrl);
1688
- await writeFileNaive(destinationUrlObject, content);
1689
- return;
1690
- }
1691
- throw error;
1692
- }
1693
- };
1694
-
1695
- const writeFileNaive = (urlObject, content) => {
1696
- return new Promise((resolve, reject) => {
1697
- writeFile$1(urlObject, content, (error) => {
1698
- if (error) {
1699
- reject(error);
1700
- } else {
1701
- resolve();
1702
- }
1703
- });
1704
- });
1705
- };
1706
-
1707
- const writeFileSync = (destination, content = "") => {
1708
- const destinationUrl = assertAndNormalizeFileUrl(destination);
1709
- const destinationUrlObject = new URL(destinationUrl);
1710
- try {
1711
- writeFileSync$1(destinationUrlObject, content);
1712
- } catch (error) {
1713
- if (error.code === "ENOENT") {
1714
- mkdirSync(new URL("./", destinationUrlObject), {
1715
- recursive: true,
1716
- });
1717
- writeFileSync$1(destinationUrlObject, content);
1718
- return;
1719
- }
1720
- throw error;
1721
- }
1722
- };
1723
-
1724
1736
  const LOG_LEVEL_OFF = "off";
1725
1737
 
1726
1738
  const LOG_LEVEL_DEBUG = "debug";
@@ -4636,7 +4648,7 @@ const ensureGlobalGc = () => {
4636
4648
  * @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
4637
4649
  * @param {boolean} [testPlanParameters.logShortForCompletedExecutions=false] Abbreviate completed execution information to shorten terminal output
4638
4650
  * @param {boolean} [testPlanParameters.logMergeForCompletedExecutions=false] Merge completed execution logs to shorten terminal output
4639
- * @param {number} [testPlanParameters.maxExecutionsInParallel=1] Maximum amount of execution in parallel
4651
+ * @param {boolean|number} [testPlanParameters.concurrency=false] Maximum amount of execution running at the same time
4640
4652
  * @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
4641
4653
  * @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
4642
4654
  * @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
@@ -4663,7 +4675,7 @@ const executeTestPlan = async ({
4663
4675
  webServer,
4664
4676
  testPlan,
4665
4677
  updateProcessExitCode = true,
4666
- maxExecutionsInParallel = 1,
4678
+ concurrency = false,
4667
4679
  defaultMsAllocatedPerExecution = 30_000,
4668
4680
  failFast = false,
4669
4681
  // keepRunning: false to ensure runtime is stopped once executed
@@ -4930,6 +4942,10 @@ To fix this warning:
4930
4942
  }
4931
4943
  }
4932
4944
  }
4945
+
4946
+ if (typeof concurrency !== "boolean" && typeof concurrency !== "number") {
4947
+ throw new TypeError(`concurrency must be a boolean or a number`);
4948
+ }
4933
4949
  }
4934
4950
 
4935
4951
  logger.debug(
@@ -4968,6 +4984,25 @@ To fix this warning:
4968
4984
  }
4969
4985
  }
4970
4986
  }
4987
+ if (cooldownBetweenExecutions) {
4988
+ if (cooldownBetweenExecutions < 0) {
4989
+ cooldownBetweenExecutions = 0;
4990
+ }
4991
+ if (cooldownBetweenExecutions === Infinity) {
4992
+ cooldownBetweenExecutions = 0;
4993
+ }
4994
+ }
4995
+ if (concurrency === true) {
4996
+ const availableCpus = os.cpus().length;
4997
+ concurrency =
4998
+ availableCpus > 2
4999
+ ? // One CPU is used to run this code so we do availableCpus - 1
5000
+ availableCpus - 1
5001
+ : 1;
5002
+ }
5003
+ if (concurrency === false) {
5004
+ concurrency = 1;
5005
+ }
4971
5006
  }
4972
5007
 
4973
5008
  testPlan = {
@@ -5107,7 +5142,7 @@ To fix this warning:
5107
5142
  const executionLogsEnabled = logger.levels.info;
5108
5143
  const executionSpinner =
5109
5144
  logRefresh &&
5110
- maxExecutionsInParallel === 1 &&
5145
+ concurrency === 1 &&
5111
5146
  !debugLogsEnabled &&
5112
5147
  executionLogsEnabled &&
5113
5148
  process.stdout.isTTY &&
@@ -5130,9 +5165,9 @@ To fix this warning:
5130
5165
 
5131
5166
  const callWhenPreviousExecutionAreDone = createCallOrderer();
5132
5167
 
5133
- await executeInParallel({
5168
+ await executeConcurrently({
5134
5169
  multipleExecutionsOperation,
5135
- maxExecutionsInParallel,
5170
+ concurrency,
5136
5171
  cooldownBetweenExecutions,
5137
5172
  executionSteps,
5138
5173
  start: async (paramsFromStep) => {
@@ -5405,57 +5440,50 @@ const canOverwriteLogGetter = ({
5405
5440
  return true;
5406
5441
  };
5407
5442
 
5408
- const executeInParallel = async ({
5443
+ const executeConcurrently = async ({
5409
5444
  multipleExecutionsOperation,
5410
- maxExecutionsInParallel,
5445
+ concurrency,
5411
5446
  cooldownBetweenExecutions,
5412
5447
  executionSteps,
5413
5448
  start,
5414
5449
  }) => {
5415
- const executionResults = [];
5416
5450
  let progressionIndex = 0;
5417
- let remainingExecutionCount = executionSteps.length;
5451
+ let remainingExecutionToStart = executionSteps.length;
5418
5452
 
5419
- const nextChunk = async () => {
5420
- if (multipleExecutionsOperation.signal.aborted) {
5453
+ const startNext = async () => {
5454
+ if (remainingExecutionToStart === 0) {
5421
5455
  return;
5422
5456
  }
5423
- const outputPromiseArray = [];
5424
- let previousExecPromise = Promise.resolve();
5425
- while (
5426
- remainingExecutionCount > 0 &&
5427
- outputPromiseArray.length < maxExecutionsInParallel
5428
- ) {
5429
- remainingExecutionCount--;
5430
- const outputPromise = executeOne(progressionIndex, previousExecPromise);
5431
- previousExecPromise = outputPromise;
5432
- progressionIndex++;
5433
- outputPromiseArray.push(outputPromise);
5434
- }
5435
- if (outputPromiseArray.length) {
5436
- await Promise.all(outputPromiseArray);
5437
- if (remainingExecutionCount > 0) {
5438
- await nextChunk();
5439
- }
5440
- }
5441
- };
5442
-
5443
- const executeOne = async (index, previousExecPromise) => {
5444
- const input = executionSteps[index];
5445
- const output = await start(input, previousExecPromise);
5446
- if (!multipleExecutionsOperation.signal.aborted) {
5447
- executionResults[index] = output;
5457
+ remainingExecutionToStart--;
5458
+ const input = executionSteps[progressionIndex];
5459
+ progressionIndex++;
5460
+ await start(input);
5461
+ if (multipleExecutionsOperation.signal.aborted) {
5462
+ return;
5448
5463
  }
5449
5464
  if (cooldownBetweenExecutions) {
5450
- await new Promise((resolve) =>
5451
- setTimeout(resolve, cooldownBetweenExecutions),
5452
- );
5465
+ await new Promise((resolve) => {
5466
+ const timeoutId = setTimeout(() => {
5467
+ removeClearTimeoutOnAbort();
5468
+ resolve();
5469
+ }, cooldownBetweenExecutions);
5470
+ const removeClearTimeoutOnAbort =
5471
+ multipleExecutionsOperation.signal.addAbortCallback(() => {
5472
+ clearTimeout(timeoutId);
5473
+ });
5474
+ });
5475
+ if (multipleExecutionsOperation.signal.aborted) {
5476
+ return;
5477
+ }
5453
5478
  }
5479
+ await startNext();
5454
5480
  };
5455
5481
 
5456
- await nextChunk();
5457
-
5458
- return executionResults;
5482
+ const promises = [];
5483
+ while (concurrency--) {
5484
+ promises.push(startNext());
5485
+ }
5486
+ await Promise.all(promises);
5459
5487
  };
5460
5488
 
5461
5489
  const memoize = (compute) => {
@@ -6864,9 +6892,12 @@ const nodeChildProcess = ({
6864
6892
  );
6865
6893
  return;
6866
6894
  }
6895
+ if (code === null || code === 0) {
6896
+ result.status = "completed";
6897
+ result.namespace = {};
6898
+ return;
6899
+ }
6867
6900
  if (
6868
- code === null ||
6869
- code === 0 ||
6870
6901
  code === EXIT_CODES.SIGINT ||
6871
6902
  code === EXIT_CODES.SIGTERM ||
6872
6903
  code === EXIT_CODES.SIGABORT
@@ -7170,9 +7201,12 @@ const nodeWorkerThread = ({
7170
7201
  );
7171
7202
  return;
7172
7203
  }
7204
+ if (code === null || code === 0) {
7205
+ result.status = "completed";
7206
+ result.namespace = {};
7207
+ return;
7208
+ }
7173
7209
  if (
7174
- code === null ||
7175
- code === 0 ||
7176
7210
  code === EXIT_CODES.SIGINT ||
7177
7211
  code === EXIT_CODES.SIGTERM ||
7178
7212
  code === EXIT_CODES.SIGABORT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/test",
3
- "version": "1.8.1",
3
+ "version": "2.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,13 +35,13 @@
35
35
  "dependencies": {
36
36
  "@jsenv/abort": "4.2.4",
37
37
  "@jsenv/ast": "6.0.1",
38
- "@jsenv/filesystem": "4.3.2",
38
+ "@jsenv/filesystem": "4.4.0",
39
39
  "@jsenv/log": "3.4.3",
40
40
  "@jsenv/plugin-supervisor": "1.3.9",
41
41
  "@jsenv/urls": "2.2.1",
42
- "@jsenv/url-meta": "8.1.0",
42
+ "@jsenv/url-meta": "8.2.0",
43
43
  "@jsenv/utils": "2.0.1",
44
- "@jsenv/github-check-run": "0.1.4",
44
+ "@jsenv/github-check-run": "0.1.5",
45
45
  "v8-to-istanbul": "9.1.3",
46
46
  "istanbul-lib-coverage": "3.2.2",
47
47
  "istanbul-lib-instrument": "6.0.1",
@@ -1,3 +1,4 @@
1
+ import os from "node:os";
1
2
  import { existsSync } from "node:fs";
2
3
  import { memoryUsage } from "node:process";
3
4
  import { takeCoverage } from "node:v8";
@@ -48,7 +49,7 @@ import { ensureGlobalGc } from "./gc.js";
48
49
  * @param {Object} testPlanParameters.testPlan Object associating files with runtimes where they will be executed
49
50
  * @param {boolean} [testPlanParameters.logShortForCompletedExecutions=false] Abbreviate completed execution information to shorten terminal output
50
51
  * @param {boolean} [testPlanParameters.logMergeForCompletedExecutions=false] Merge completed execution logs to shorten terminal output
51
- * @param {number} [testPlanParameters.maxExecutionsInParallel=1] Maximum amount of execution in parallel
52
+ * @param {boolean|number} [testPlanParameters.concurrency=false] Maximum amount of execution running at the same time
52
53
  * @param {number} [testPlanParameters.defaultMsAllocatedPerExecution=30000] Milliseconds after which execution is aborted and considered as failed by timeout
53
54
  * @param {boolean} [testPlanParameters.failFast=false] Fails immediatly when a test execution fails
54
55
  * @param {number} [testPlanParameters.cooldownBetweenExecutions=0] Millisecond to wait between each execution
@@ -75,7 +76,7 @@ export const executeTestPlan = async ({
75
76
  webServer,
76
77
  testPlan,
77
78
  updateProcessExitCode = true,
78
- maxExecutionsInParallel = 1,
79
+ concurrency = false,
79
80
  defaultMsAllocatedPerExecution = 30_000,
80
81
  failFast = false,
81
82
  // keepRunning: false to ensure runtime is stopped once executed
@@ -342,6 +343,10 @@ To fix this warning:
342
343
  }
343
344
  }
344
345
  }
346
+
347
+ if (typeof concurrency !== "boolean" && typeof concurrency !== "number") {
348
+ throw new TypeError(`concurrency must be a boolean or a number`);
349
+ }
345
350
  }
346
351
 
347
352
  logger.debug(
@@ -380,6 +385,25 @@ To fix this warning:
380
385
  }
381
386
  }
382
387
  }
388
+ if (cooldownBetweenExecutions) {
389
+ if (cooldownBetweenExecutions < 0) {
390
+ cooldownBetweenExecutions = 0;
391
+ }
392
+ if (cooldownBetweenExecutions === Infinity) {
393
+ cooldownBetweenExecutions = 0;
394
+ }
395
+ }
396
+ if (concurrency === true) {
397
+ const availableCpus = os.cpus().length;
398
+ concurrency =
399
+ availableCpus > 2
400
+ ? // One CPU is used to run this code so we do availableCpus - 1
401
+ availableCpus - 1
402
+ : 1;
403
+ }
404
+ if (concurrency === false) {
405
+ concurrency = 1;
406
+ }
383
407
  }
384
408
 
385
409
  testPlan = {
@@ -519,7 +543,7 @@ To fix this warning:
519
543
  const executionLogsEnabled = logger.levels.info;
520
544
  const executionSpinner =
521
545
  logRefresh &&
522
- maxExecutionsInParallel === 1 &&
546
+ concurrency === 1 &&
523
547
  !debugLogsEnabled &&
524
548
  executionLogsEnabled &&
525
549
  process.stdout.isTTY &&
@@ -542,9 +566,9 @@ To fix this warning:
542
566
 
543
567
  const callWhenPreviousExecutionAreDone = createCallOrderer();
544
568
 
545
- await executeInParallel({
569
+ await executeConcurrently({
546
570
  multipleExecutionsOperation,
547
- maxExecutionsInParallel,
571
+ concurrency,
548
572
  cooldownBetweenExecutions,
549
573
  executionSteps,
550
574
  start: async (paramsFromStep) => {
@@ -817,55 +841,48 @@ const canOverwriteLogGetter = ({
817
841
  return true;
818
842
  };
819
843
 
820
- const executeInParallel = async ({
844
+ const executeConcurrently = async ({
821
845
  multipleExecutionsOperation,
822
- maxExecutionsInParallel,
846
+ concurrency,
823
847
  cooldownBetweenExecutions,
824
848
  executionSteps,
825
849
  start,
826
850
  }) => {
827
- const executionResults = [];
828
851
  let progressionIndex = 0;
829
- let remainingExecutionCount = executionSteps.length;
852
+ let remainingExecutionToStart = executionSteps.length;
830
853
 
831
- const nextChunk = async () => {
832
- if (multipleExecutionsOperation.signal.aborted) {
854
+ const startNext = async () => {
855
+ if (remainingExecutionToStart === 0) {
833
856
  return;
834
857
  }
835
- const outputPromiseArray = [];
836
- let previousExecPromise = Promise.resolve();
837
- while (
838
- remainingExecutionCount > 0 &&
839
- outputPromiseArray.length < maxExecutionsInParallel
840
- ) {
841
- remainingExecutionCount--;
842
- const outputPromise = executeOne(progressionIndex, previousExecPromise);
843
- previousExecPromise = outputPromise;
844
- progressionIndex++;
845
- outputPromiseArray.push(outputPromise);
846
- }
847
- if (outputPromiseArray.length) {
848
- await Promise.all(outputPromiseArray);
849
- if (remainingExecutionCount > 0) {
850
- await nextChunk();
851
- }
852
- }
853
- };
854
-
855
- const executeOne = async (index, previousExecPromise) => {
856
- const input = executionSteps[index];
857
- const output = await start(input, previousExecPromise);
858
- if (!multipleExecutionsOperation.signal.aborted) {
859
- executionResults[index] = output;
858
+ remainingExecutionToStart--;
859
+ const input = executionSteps[progressionIndex];
860
+ progressionIndex++;
861
+ await start(input);
862
+ if (multipleExecutionsOperation.signal.aborted) {
863
+ return;
860
864
  }
861
865
  if (cooldownBetweenExecutions) {
862
- await new Promise((resolve) =>
863
- setTimeout(resolve, cooldownBetweenExecutions),
864
- );
866
+ await new Promise((resolve) => {
867
+ const timeoutId = setTimeout(() => {
868
+ removeClearTimeoutOnAbort();
869
+ resolve();
870
+ }, cooldownBetweenExecutions);
871
+ const removeClearTimeoutOnAbort =
872
+ multipleExecutionsOperation.signal.addAbortCallback(() => {
873
+ clearTimeout(timeoutId);
874
+ });
875
+ });
876
+ if (multipleExecutionsOperation.signal.aborted) {
877
+ return;
878
+ }
865
879
  }
880
+ await startNext();
866
881
  };
867
882
 
868
- await nextChunk();
869
-
870
- return executionResults;
883
+ const promises = [];
884
+ while (concurrency--) {
885
+ promises.push(startNext());
886
+ }
887
+ await Promise.all(promises);
871
888
  };
@@ -256,9 +256,12 @@ export const nodeChildProcess = ({
256
256
  );
257
257
  return;
258
258
  }
259
+ if (code === null || code === 0) {
260
+ result.status = "completed";
261
+ result.namespace = {};
262
+ return;
263
+ }
259
264
  if (
260
- code === null ||
261
- code === 0 ||
262
265
  code === EXIT_CODES.SIGINT ||
263
266
  code === EXIT_CODES.SIGTERM ||
264
267
  code === EXIT_CODES.SIGABORT
@@ -202,9 +202,12 @@ export const nodeWorkerThread = ({
202
202
  );
203
203
  return;
204
204
  }
205
+ if (code === null || code === 0) {
206
+ result.status = "completed";
207
+ result.namespace = {};
208
+ return;
209
+ }
205
210
  if (
206
- code === null ||
207
- code === 0 ||
208
211
  code === EXIT_CODES.SIGINT ||
209
212
  code === EXIT_CODES.SIGTERM ||
210
213
  code === EXIT_CODES.SIGABORT