@jsenv/core 23.4.2 → 23.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "23.4.2",
3
+ "version": "23.5.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -63,12 +63,12 @@
63
63
  "@babel/plugin-syntax-numeric-separator": "7.10.4",
64
64
  "@babel/plugin-transform-modules-systemjs": "7.15.4",
65
65
  "@c88/v8-coverage": "0.1.1",
66
- "@jsenv/abort": "4.0.0",
67
- "@jsenv/filesystem": "2.5.0",
66
+ "@jsenv/abort": "4.1.1",
67
+ "@jsenv/filesystem": "2.5.1",
68
68
  "@jsenv/importmap": "1.1.0",
69
- "@jsenv/log": "1.1.0",
69
+ "@jsenv/log": "1.4.0",
70
70
  "@jsenv/logger": "4.0.1",
71
- "@jsenv/server": "10.0.0",
71
+ "@jsenv/server": "10.0.1",
72
72
  "@jsenv/uneval": "1.6.0",
73
73
  "@jsenv/workers": "1.2.0",
74
74
  "@rollup/plugin-commonjs": "21.0.0",
package/src/execute.js CHANGED
@@ -27,11 +27,8 @@ export const execute = async ({
27
27
  runtimeParams,
28
28
 
29
29
  allocatedMs,
30
- measureDuration,
31
30
  mirrorConsole = true,
32
31
  captureConsole,
33
- collectRuntimeName,
34
- collectRuntimeVersion,
35
32
  inheritCoverage,
36
33
  collectCoverage,
37
34
  measurePerformance,
@@ -139,11 +136,8 @@ export const execute = async ({
139
136
  },
140
137
 
141
138
  allocatedMs,
142
- measureDuration,
143
139
  mirrorConsole,
144
140
  captureConsole,
145
- collectRuntimeName,
146
- collectRuntimeVersion,
147
141
  inheritCoverage,
148
142
  collectCoverage,
149
143
  measurePerformance,
@@ -33,13 +33,13 @@ export const executeTestPlan = async ({
33
33
 
34
34
  testPlan,
35
35
  defaultMsAllocatedPerExecution,
36
+ cooldownBetweenExecutions,
36
37
 
37
38
  maxExecutionsInParallel,
38
39
 
39
40
  completedExecutionLogAbbreviation = false,
40
41
  completedExecutionLogMerging = false,
41
42
  logSummary = true,
42
- measureGlobalDuration = true,
43
43
  updateProcessExitCode = true,
44
44
 
45
45
  coverage = process.argv.includes("--cover") ||
@@ -144,10 +144,10 @@ export const executeTestPlan = async ({
144
144
 
145
145
  defaultMsAllocatedPerExecution,
146
146
  maxExecutionsInParallel,
147
+ cooldownBetweenExecutions,
147
148
  completedExecutionLogMerging,
148
149
  completedExecutionLogAbbreviation,
149
150
  logSummary,
150
- measureGlobalDuration,
151
151
 
152
152
  coverage,
153
153
  coverageConfig,
@@ -225,6 +225,7 @@ export const executeTestPlan = async ({
225
225
  }
226
226
 
227
227
  return {
228
+ testPlanAborted: result.aborted,
228
229
  testPlanSummary: result.planSummary,
229
230
  testPlanReport: result.planReport,
230
231
  testPlanCoverage: planCoverage,
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "@jsenv/filesystem"
2
+ import { Abort } from "@jsenv/abort"
2
3
 
3
4
  import {
4
5
  visitNodeV8Directory,
@@ -9,13 +10,12 @@ import { composeTwoFileByFileIstanbulCoverages } from "../coverage_utils/istanbu
9
10
  import { v8CoverageToIstanbul } from "../coverage_utils/v8_coverage_to_istanbul.js"
10
11
  import { composeV8AndIstanbul } from "../coverage_utils/v8_and_istanbul.js"
11
12
  import { normalizeFileByFileCoveragePaths } from "../coverage_utils/file_by_file_coverage.js"
12
- import { listRelativeFileUrlToCover } from "../coverage_empty/list_files_not_covered.js"
13
- import { relativeUrlToEmptyCoverage } from "../coverage_empty/relativeUrlToEmptyCoverage.js"
13
+ import { getMissingFileByFileCoverage } from "../coverage_missing/missing_coverage.js"
14
14
 
15
15
  export const reportToCoverage = async (
16
16
  report,
17
17
  {
18
- multipleExecutionsOperation,
18
+ signal,
19
19
  logger,
20
20
  projectDirectoryUrl,
21
21
  babelPluginMap,
@@ -26,70 +26,39 @@ export const reportToCoverage = async (
26
26
  coverageV8ConflictWarning,
27
27
  },
28
28
  ) => {
29
- let v8Coverage
30
- let fileByFileIstanbulCoverage
31
-
32
29
  // collect v8 and istanbul coverage from executions
33
- await Object.keys(report).reduce(async (previous, file) => {
34
- await previous
35
-
36
- const executionResultForFile = report[file]
37
- await Object.keys(executionResultForFile).reduce(
38
- async (previous, executionName) => {
39
- await previous
40
-
41
- const executionResultForFileOnRuntime =
42
- executionResultForFile[executionName]
43
- const { status, coverageFileUrl } = executionResultForFileOnRuntime
44
- if (!coverageFileUrl) {
45
- // several reasons not to have coverage here:
46
- // 1. the file we executed did not import an instrumented file.
47
- // - a test file without import
48
- // - a test file importing only file excluded from coverage
49
- // - a coverDescription badly configured so that we don't realize
50
- // a file should be covered
51
-
52
- // 2. the file we wanted to executed timedout
53
- // - infinite loop
54
- // - too extensive operation
55
- // - a badly configured or too low allocatedMs for that execution.
56
-
57
- // 3. the file we wanted to execute contains syntax-error
58
-
59
- // in any scenario we are fine because
60
- // coverDescription will generate empty coverage for files
61
- // that were suppose to be coverage but were not.
62
- if (status === "completed") {
63
- logger.debug(
64
- `No execution.coverageFileUrl from execution named "${executionName}" of ${file}`,
65
- )
66
- }
67
- return
68
- }
69
-
70
- const executionCoverage = await readFile(coverageFileUrl, {
71
- as: "json",
72
- })
73
- if (isV8Coverage(executionCoverage)) {
74
- v8Coverage = v8Coverage
75
- ? composeTwoV8Coverages(v8Coverage, executionCoverage)
76
- : executionCoverage
77
- } else {
78
- fileByFileIstanbulCoverage = fileByFileIstanbulCoverage
79
- ? composeTwoFileByFileIstanbulCoverages(
80
- fileByFileIstanbulCoverage,
81
- executionCoverage,
82
- )
83
- : executionCoverage
84
- }
85
- },
86
- Promise.resolve(),
87
- )
88
- }, Promise.resolve())
30
+ let { v8Coverage, fileByFileIstanbulCoverage } = await getCoverageFromReport({
31
+ signal,
32
+ report,
33
+ onMissing: ({ file, executionResult, executionName }) => {
34
+ // several reasons not to have coverage here:
35
+ // 1. the file we executed did not import an instrumented file.
36
+ // - a test file without import
37
+ // - a test file importing only file excluded from coverage
38
+ // - a coverDescription badly configured so that we don't realize
39
+ // a file should be covered
40
+
41
+ // 2. the file we wanted to executed timedout
42
+ // - infinite loop
43
+ // - too extensive operation
44
+ // - a badly configured or too low allocatedMs for that execution.
45
+
46
+ // 3. the file we wanted to execute contains syntax-error
47
+
48
+ // in any scenario we are fine because
49
+ // coverDescription will generate empty coverage for files
50
+ // that were suppose to be coverage but were not.
51
+ if (executionResult.status === "completed") {
52
+ logger.debug(
53
+ `No execution.coverageFileUrl from execution named "${executionName}" of ${file}`,
54
+ )
55
+ }
56
+ },
57
+ })
89
58
 
90
59
  if (!coverageForceIstanbul && process.env.NODE_V8_COVERAGE) {
91
60
  await visitNodeV8Directory({
92
- signal: multipleExecutionsOperation.signal,
61
+ signal,
93
62
  NODE_V8_COVERAGE: process.env.NODE_V8_COVERAGE,
94
63
  onV8Coverage: (nodeV8Coverage) => {
95
64
  const nodeV8CoverageLight = filterV8Coverage(nodeV8Coverage, {
@@ -105,7 +74,9 @@ export const reportToCoverage = async (
105
74
  // try to merge v8 with istanbul, if any
106
75
  let fileByFileCoverage
107
76
  if (v8Coverage) {
108
- let v8FileByFileCoverage = await v8CoverageToIstanbul(v8Coverage)
77
+ let v8FileByFileCoverage = await v8CoverageToIstanbul(v8Coverage, {
78
+ signal,
79
+ })
109
80
 
110
81
  v8FileByFileCoverage = normalizeFileByFileCoveragePaths(
111
82
  v8FileByFileCoverage,
@@ -140,34 +111,77 @@ export const reportToCoverage = async (
140
111
 
141
112
  // now add coverage for file not covered
142
113
  if (coverageIncludeMissing) {
143
- const relativeUrlsToCover = await listRelativeFileUrlToCover({
144
- multipleExecutionsOperation,
114
+ const missingFileByFileCoverage = await getMissingFileByFileCoverage({
115
+ signal,
145
116
  projectDirectoryUrl,
146
117
  coverageConfig,
118
+ fileByFileCoverage,
119
+ babelPluginMap,
147
120
  })
121
+ Object.assign(fileByFileCoverage, missingFileByFileCoverage)
122
+ }
148
123
 
149
- const relativeUrlsMissing = relativeUrlsToCover.filter(
150
- (relativeUrlToCover) =>
151
- Object.keys(fileByFileCoverage).every((key) => {
152
- return key !== `./${relativeUrlToCover}`
153
- }),
154
- )
124
+ return fileByFileCoverage
125
+ }
126
+
127
+ const getCoverageFromReport = async ({ signal, report, onMissing }) => {
128
+ const operation = Abort.startOperation()
129
+ operation.addAbortSignal(signal)
130
+
131
+ try {
132
+ let v8Coverage
133
+ let fileByFileIstanbulCoverage
134
+
135
+ // collect v8 and istanbul coverage from executions
136
+ await Object.keys(report).reduce(async (previous, file) => {
137
+ operation.throwIfAborted()
138
+ await previous
139
+
140
+ const executionResultForFile = report[file]
141
+ await Object.keys(executionResultForFile).reduce(
142
+ async (previous, executionName) => {
143
+ operation.throwIfAborted()
144
+ await previous
145
+
146
+ const executionResultForFileOnRuntime =
147
+ executionResultForFile[executionName]
148
+ const { coverageFileUrl } = executionResultForFileOnRuntime
149
+ if (!coverageFileUrl) {
150
+ onMissing({
151
+ executionName,
152
+ file,
153
+ executionResult: executionResultForFileOnRuntime,
154
+ })
155
+ return
156
+ }
155
157
 
156
- await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
157
- const emptyCoverage = await relativeUrlToEmptyCoverage(
158
- relativeUrlMissing,
159
- {
160
- multipleExecutionsOperation,
161
- projectDirectoryUrl,
162
- babelPluginMap,
158
+ const executionCoverage = await readFile(coverageFileUrl, {
159
+ as: "json",
160
+ })
161
+ if (isV8Coverage(executionCoverage)) {
162
+ v8Coverage = v8Coverage
163
+ ? composeTwoV8Coverages(v8Coverage, executionCoverage)
164
+ : executionCoverage
165
+ } else {
166
+ fileByFileIstanbulCoverage = fileByFileIstanbulCoverage
167
+ ? composeTwoFileByFileIstanbulCoverages(
168
+ fileByFileIstanbulCoverage,
169
+ executionCoverage,
170
+ )
171
+ : executionCoverage
172
+ }
163
173
  },
174
+ Promise.resolve(),
164
175
  )
165
- fileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage
166
- return emptyCoverage
167
176
  }, Promise.resolve())
168
- }
169
177
 
170
- return fileByFileCoverage
178
+ return {
179
+ v8Coverage,
180
+ fileByFileIstanbulCoverage,
181
+ }
182
+ } finally {
183
+ await operation.end()
184
+ }
171
185
  }
172
186
 
173
187
  const isV8Coverage = (coverage) => Boolean(coverage.result)
@@ -1,7 +1,7 @@
1
1
  import { collectFiles } from "@jsenv/filesystem"
2
2
 
3
3
  export const listRelativeFileUrlToCover = async ({
4
- multipleExecutionsOperation,
4
+ signal,
5
5
  projectDirectoryUrl,
6
6
  coverageConfig,
7
7
  }) => {
@@ -10,7 +10,7 @@ export const listRelativeFileUrlToCover = async ({
10
10
  }
11
11
 
12
12
  const matchingFileResultArray = await collectFiles({
13
- signal: multipleExecutionsOperation.signal,
13
+ signal,
14
14
  directoryUrl: projectDirectoryUrl,
15
15
  structuredMetaMap: structuredMetaMapForCoverage,
16
16
  predicate: ({ cover }) => cover,
@@ -0,0 +1,46 @@
1
+ import { Abort } from "@jsenv/abort"
2
+
3
+ import { listRelativeFileUrlToCover } from "./list_files_not_covered.js"
4
+ import { relativeUrlToEmptyCoverage } from "./relativeUrlToEmptyCoverage.js"
5
+
6
+ export const getMissingFileByFileCoverage = async ({
7
+ signal,
8
+ projectDirectoryUrl,
9
+ coverageConfig,
10
+ fileByFileCoverage,
11
+ babelPluginMap,
12
+ }) => {
13
+ const relativeUrlsToCover = await listRelativeFileUrlToCover({
14
+ signal,
15
+ projectDirectoryUrl,
16
+ coverageConfig,
17
+ })
18
+
19
+ const relativeUrlsMissing = relativeUrlsToCover.filter((relativeUrlToCover) =>
20
+ Object.keys(fileByFileCoverage).every((key) => {
21
+ return key !== `./${relativeUrlToCover}`
22
+ }),
23
+ )
24
+
25
+ const operation = Abort.startOperation()
26
+ operation.addAbortSignal(signal)
27
+
28
+ const missingFileByFileCoverage = {}
29
+ await relativeUrlsMissing.reduce(async (previous, relativeUrlMissing) => {
30
+ operation.throwIfAborted()
31
+ await previous
32
+ await operation.withSignal(async (signal) => {
33
+ const emptyCoverage = await relativeUrlToEmptyCoverage(
34
+ relativeUrlMissing,
35
+ {
36
+ signal,
37
+ projectDirectoryUrl,
38
+ babelPluginMap,
39
+ },
40
+ )
41
+ missingFileByFileCoverage[`./${relativeUrlMissing}`] = emptyCoverage
42
+ })
43
+ }, Promise.resolve())
44
+
45
+ return missingFileByFileCoverage
46
+ }
@@ -1,4 +1,5 @@
1
1
  import { resolveUrl, urlToFileSystemPath, readFile } from "@jsenv/filesystem"
2
+ import { Abort } from "@jsenv/abort"
2
3
 
3
4
  import {
4
5
  babelPluginsFromBabelPluginMap,
@@ -9,22 +10,23 @@ import { createEmptyCoverage } from "./createEmptyCoverage.js"
9
10
 
10
11
  export const relativeUrlToEmptyCoverage = async (
11
12
  relativeUrl,
12
- { multipleExecutionsOperation, projectDirectoryUrl, babelPluginMap },
13
+ { signal, projectDirectoryUrl, babelPluginMap },
13
14
  ) => {
14
- const { transformAsync } = await import("@babel/core")
15
-
16
- const fileUrl = resolveUrl(relativeUrl, projectDirectoryUrl)
17
- multipleExecutionsOperation.throwIfAborted()
18
- const source = await readFile(fileUrl)
15
+ const operation = Abort.startOperation()
16
+ operation.addAbortSignal(signal)
19
17
 
20
18
  try {
19
+ const { transformAsync } = await import("@babel/core")
20
+ const fileUrl = resolveUrl(relativeUrl, projectDirectoryUrl)
21
+ const source = await readFile(fileUrl)
22
+
21
23
  babelPluginMap = {
22
24
  ...getMinimalBabelPluginMap(),
23
25
  ...babelPluginMap,
24
26
  "transform-instrument": [babelPluginInstrument, { projectDirectoryUrl }],
25
27
  }
26
28
 
27
- multipleExecutionsOperation.throwIfAborted()
29
+ operation.throwIfAborted()
28
30
  const { metadata } = await transformAsync(source, {
29
31
  filename: urlToFileSystemPath(fileUrl),
30
32
  filenameRelative: relativeUrl,
@@ -55,5 +57,7 @@ export const relativeUrlToEmptyCoverage = async (
55
57
  return createEmptyCoverage(relativeUrl)
56
58
  }
57
59
  throw e
60
+ } finally {
61
+ await operation.end()
58
62
  }
59
63
  }
@@ -5,12 +5,16 @@ import {
5
5
  resolveUrl,
6
6
  } from "@jsenv/filesystem"
7
7
  import { createDetailedMessage } from "@jsenv/logger"
8
+ import { Abort } from "@jsenv/abort"
8
9
 
9
10
  export const visitNodeV8Directory = async ({
10
- // signal
11
+ signal,
11
12
  NODE_V8_COVERAGE,
12
13
  onV8Coverage,
13
14
  }) => {
15
+ const operation = Abort.startOperation()
16
+ operation.addAbortSignal(signal)
17
+
14
18
  const tryReadDirectory = async () => {
15
19
  const dirContent = await readDirectory(NODE_V8_COVERAGE)
16
20
  if (dirContent.length > 0) {
@@ -19,39 +23,47 @@ export const visitNodeV8Directory = async ({
19
23
  console.warn(`v8 coverage directory is empty at ${NODE_V8_COVERAGE}`)
20
24
  return dirContent
21
25
  }
22
- const dirContent = await tryReadDirectory()
23
26
 
24
- const coverageDirectoryUrl = assertAndNormalizeDirectoryUrl(NODE_V8_COVERAGE)
25
- await dirContent.reduce(async (previous, dirEntry) => {
26
- await previous
27
+ try {
28
+ operation.throwIfAborted()
29
+ const dirContent = await tryReadDirectory()
27
30
 
28
- const dirEntryUrl = resolveUrl(dirEntry, coverageDirectoryUrl)
29
- const tryReadJsonFile = async () => {
30
- const fileContent = await readFile(dirEntryUrl, { as: "string" })
31
- if (fileContent === "") {
32
- console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`)
33
- return null
34
- }
31
+ const coverageDirectoryUrl =
32
+ assertAndNormalizeDirectoryUrl(NODE_V8_COVERAGE)
33
+ await dirContent.reduce(async (previous, dirEntry) => {
34
+ operation.throwIfAborted()
35
+ await previous
35
36
 
36
- try {
37
- const fileAsJson = JSON.parse(fileContent)
38
- return fileAsJson
39
- } catch (e) {
40
- console.warn(
41
- createDetailedMessage(`Error while reading coverage file`, {
42
- "error stack": e.stack,
43
- "file": dirEntryUrl,
44
- }),
45
- )
46
- return null
37
+ const dirEntryUrl = resolveUrl(dirEntry, coverageDirectoryUrl)
38
+ const tryReadJsonFile = async () => {
39
+ const fileContent = await readFile(dirEntryUrl, { as: "string" })
40
+ if (fileContent === "") {
41
+ console.warn(`Coverage JSON file is empty at ${dirEntryUrl}`)
42
+ return null
43
+ }
44
+
45
+ try {
46
+ const fileAsJson = JSON.parse(fileContent)
47
+ return fileAsJson
48
+ } catch (e) {
49
+ console.warn(
50
+ createDetailedMessage(`Error while reading coverage file`, {
51
+ "error stack": e.stack,
52
+ "file": dirEntryUrl,
53
+ }),
54
+ )
55
+ return null
56
+ }
47
57
  }
48
- }
49
58
 
50
- const fileContent = await tryReadJsonFile()
51
- if (fileContent) {
52
- onV8Coverage(fileContent)
53
- }
54
- }, Promise.resolve())
59
+ const fileContent = await tryReadJsonFile()
60
+ if (fileContent) {
61
+ onV8Coverage(fileContent)
62
+ }
63
+ }, Promise.resolve())
64
+ } finally {
65
+ await operation.end()
66
+ }
55
67
  }
56
68
 
57
69
  export const filterV8Coverage = (v8Coverage, { coverageIgnorePredicate }) => {
@@ -1,53 +1,63 @@
1
1
  import { urlToFileSystemPath } from "@jsenv/filesystem"
2
+ import { Abort } from "@jsenv/abort"
2
3
 
3
4
  import { require } from "@jsenv/core/src/internal/require.js"
4
5
 
5
6
  import { composeTwoFileByFileIstanbulCoverages } from "./istanbul_coverage_composition.js"
6
7
 
7
- export const v8CoverageToIstanbul = async (v8Coverage) => {
8
- const v8ToIstanbul = require("v8-to-istanbul")
9
- const sourcemapCache = v8Coverage["source-map-cache"]
10
- let istanbulCoverageComposed = null
11
- await v8Coverage.result.reduce(async (previous, fileV8Coverage) => {
12
- await previous
8
+ export const v8CoverageToIstanbul = async (v8Coverage, { signal }) => {
9
+ const operation = Abort.startOperation()
10
+ operation.addAbortSignal(signal)
13
11
 
14
- const { source } = fileV8Coverage
15
- let sources
16
- // when v8 coverage comes from playwright (chromium) v8Coverage.source is set
17
- if (typeof source === "string") {
18
- sources = { source }
19
- }
20
- // when v8 coverage comes from Node.js, the source can be read from sourcemapCache
21
- else if (sourcemapCache) {
22
- sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourcemapCache)
23
- }
24
- const path = urlToFileSystemPath(fileV8Coverage.url)
12
+ try {
13
+ const v8ToIstanbul = require("v8-to-istanbul")
14
+ const sourcemapCache = v8Coverage["source-map-cache"]
15
+ let istanbulCoverageComposed = null
25
16
 
26
- const converter = v8ToIstanbul(
27
- path,
28
- // wrapperLength is undefined we don't need it
29
- // https://github.com/istanbuljs/v8-to-istanbul/blob/2b54bc97c5edf8a37b39a171ec29134ba9bfd532/lib/v8-to-istanbul.js#L27
30
- undefined,
31
- sources,
32
- )
33
- await converter.load()
17
+ await v8Coverage.result.reduce(async (previous, fileV8Coverage) => {
18
+ operation.throwIfAborted()
19
+ await previous
34
20
 
35
- converter.applyCoverage(fileV8Coverage.functions)
36
- const istanbulCoverage = converter.toIstanbul()
21
+ const { source } = fileV8Coverage
22
+ let sources
23
+ // when v8 coverage comes from playwright (chromium) v8Coverage.source is set
24
+ if (typeof source === "string") {
25
+ sources = { source }
26
+ }
27
+ // when v8 coverage comes from Node.js, the source can be read from sourcemapCache
28
+ else if (sourcemapCache) {
29
+ sources = sourcesFromSourceMapCache(fileV8Coverage.url, sourcemapCache)
30
+ }
31
+ const path = urlToFileSystemPath(fileV8Coverage.url)
37
32
 
38
- istanbulCoverageComposed = istanbulCoverageComposed
39
- ? composeTwoFileByFileIstanbulCoverages(
40
- istanbulCoverageComposed,
41
- istanbulCoverage,
42
- )
43
- : istanbulCoverage
44
- }, Promise.resolve())
33
+ const converter = v8ToIstanbul(
34
+ path,
35
+ // wrapperLength is undefined we don't need it
36
+ // https://github.com/istanbuljs/v8-to-istanbul/blob/2b54bc97c5edf8a37b39a171ec29134ba9bfd532/lib/v8-to-istanbul.js#L27
37
+ undefined,
38
+ sources,
39
+ )
40
+ await converter.load()
45
41
 
46
- if (!istanbulCoverageComposed) {
47
- return {}
42
+ converter.applyCoverage(fileV8Coverage.functions)
43
+ const istanbulCoverage = converter.toIstanbul()
44
+
45
+ istanbulCoverageComposed = istanbulCoverageComposed
46
+ ? composeTwoFileByFileIstanbulCoverages(
47
+ istanbulCoverageComposed,
48
+ istanbulCoverage,
49
+ )
50
+ : istanbulCoverage
51
+ }, Promise.resolve())
52
+
53
+ if (!istanbulCoverageComposed) {
54
+ return {}
55
+ }
56
+ istanbulCoverageComposed = markAsConvertedFromV8(istanbulCoverageComposed)
57
+ return istanbulCoverageComposed
58
+ } finally {
59
+ await operation.end()
48
60
  }
49
- istanbulCoverageComposed = markAsConvertedFromV8(istanbulCoverageComposed)
50
- return istanbulCoverageComposed
51
61
  }
52
62
 
53
63
  const markAsConvertedFromV8 = (fileByFileCoverage) => {
@@ -5,7 +5,8 @@ import { EXECUTION_COLORS } from "./execution_colors.js"
5
5
 
6
6
  export const createSummaryLog = (summary) => `
7
7
  -------------- summary -----------------
8
- ${createSummaryMessage(summary)}${createTotalDurationMessage(summary)}
8
+ ${createSummaryMessage(summary)}
9
+ total duration: ${msAsDuration(summary.duration)}
9
10
  ----------------------------------------
10
11
  `
11
12
 
@@ -114,10 +115,3 @@ const createMixedDetails = ({
114
115
 
115
116
  return `${parts.join(", ")}`
116
117
  }
117
-
118
- const createTotalDurationMessage = ({ startMs, endMs }) => {
119
- if (!endMs) return ""
120
-
121
- return `
122
- total duration: ${msAsDuration(endMs - startMs)}`
123
- }
@@ -1,7 +1,8 @@
1
- import { stat } from "node:fs"
1
+ import { existsSync } from "node:fs"
2
2
  import wrapAnsi from "wrap-ansi"
3
3
  import cuid from "cuid"
4
- import { loggerToLevels, createDetailedMessage } from "@jsenv/logger"
4
+ import { loggerToLevels } from "@jsenv/logger"
5
+ import { createLog, startSpinner } from "@jsenv/log"
5
6
  import {
6
7
  urlToFileSystemPath,
7
8
  resolveUrl,
@@ -10,12 +11,11 @@ import {
10
11
  normalizeStructuredMetaMap,
11
12
  urlToMeta,
12
13
  } from "@jsenv/filesystem"
13
- import { createLog } from "@jsenv/log"
14
14
  import { Abort } from "@jsenv/abort"
15
15
 
16
16
  import { launchAndExecute } from "../executing/launchAndExecute.js"
17
17
  import { reportToCoverage } from "./coverage/reportToCoverage.js"
18
- import { createExecutionResultLog } from "./executionLogs.js"
18
+ import { formatExecuting, formatExecutionResult } from "./executionLogs.js"
19
19
  import { createSummaryLog } from "./createSummaryLog.js"
20
20
 
21
21
  export const executeConcurrently = async (
@@ -33,10 +33,10 @@ export const executeConcurrently = async (
33
33
  babelPluginMap,
34
34
 
35
35
  defaultMsAllocatedPerExecution = 30000,
36
+ cooldownBetweenExecutions = 0,
36
37
  maxExecutionsInParallel = 1,
37
38
  completedExecutionLogMerging,
38
39
  completedExecutionLogAbbreviation,
39
- measureGlobalDuration = true,
40
40
 
41
41
  coverage,
42
42
  coverageConfig,
@@ -46,21 +46,21 @@ export const executeConcurrently = async (
46
46
  coverageTempDirectoryRelativeUrl,
47
47
  runtimeSupport,
48
48
 
49
- mainFileNotFoundCallback = ({ fileRelativeUrl }) => {
50
- logger.error(
51
- new Error(
52
- createDetailedMessage(`an execution main file does not exists.`, {
53
- ["file relative path"]: fileRelativeUrl,
54
- }),
55
- ),
56
- )
57
- },
58
49
  beforeExecutionCallback = () => {},
59
50
  afterExecutionCallback = () => {},
60
51
 
61
52
  logSummary,
62
53
  },
63
54
  ) => {
55
+ if (completedExecutionLogMerging && !process.stdout.isTTY) {
56
+ completedExecutionLogMerging = false
57
+ logger.debug(
58
+ `Force completedExecutionLogMerging to false because process.stdout.isTTY is false`,
59
+ )
60
+ }
61
+ const executionLogsEnabled = loggerToLevels(logger).info
62
+ const executionSpinner = executionLogsEnabled && process.stdout.isTTY
63
+
64
64
  const startMs = Date.now()
65
65
 
66
66
  const report = {}
@@ -113,7 +113,7 @@ export const executeConcurrently = async (
113
113
 
114
114
  try {
115
115
  value.coverage = await reportToCoverage(value.report, {
116
- multipleExecutionsOperation,
116
+ signal: multipleExecutionsOperation.signal,
117
117
  logger,
118
118
  projectDirectoryUrl,
119
119
  babelPluginMap,
@@ -133,8 +133,7 @@ export const executeConcurrently = async (
133
133
  }
134
134
  }
135
135
 
136
- let previousExecutionResult
137
- let previousExecutionLog
136
+ let executionLog = createLog({ newLine: "around" })
138
137
  let abortedCount = 0
139
138
  let timedoutCount = 0
140
139
  let erroredCount = 0
@@ -142,18 +141,19 @@ export const executeConcurrently = async (
142
141
  const executionsDone = await executeInParallel({
143
142
  multipleExecutionsOperation,
144
143
  maxExecutionsInParallel,
144
+ cooldownBetweenExecutions,
145
145
  executionSteps,
146
146
  start: async (paramsFromStep) => {
147
147
  const executionIndex = executionSteps.indexOf(paramsFromStep)
148
- const { executionName, fileRelativeUrl } = paramsFromStep
148
+ const { executionName, fileRelativeUrl, runtime } = paramsFromStep
149
+ const runtimeName = runtime.name
150
+ const runtimeVersion = runtime.version
151
+
149
152
  const executionParams = {
150
153
  // the params below can be overriden by executionDefaultParams
151
154
  measurePerformance: false,
152
155
  collectPerformance: false,
153
- measureDuration: true,
154
156
  captureConsole: true,
155
- collectRuntimeName: true,
156
- collectRuntimeVersion: true,
157
157
  // stopAfterExecute: true to ensure runtime is stopped once executed
158
158
  // because we have what we wants: execution is completed and
159
159
  // we have associated coverage and capturedConsole
@@ -163,6 +163,7 @@ export const executeConcurrently = async (
163
163
  stopAfterExecuteReason: "execution-done",
164
164
  allocatedMs: defaultMsAllocatedPerExecution,
165
165
  ...paramsFromStep,
166
+ runtime,
166
167
  // mirrorConsole: false because file will be executed in parallel
167
168
  // so log would be a mess to read
168
169
  mirrorConsole: false,
@@ -170,48 +171,69 @@ export const executeConcurrently = async (
170
171
 
171
172
  const beforeExecutionInfo = {
172
173
  fileRelativeUrl,
174
+ runtimeName,
175
+ runtimeVersion,
173
176
  executionIndex,
174
177
  executionParams,
175
178
  }
176
179
 
180
+ let spinner
181
+ if (executionSpinner) {
182
+ spinner = startSpinner({
183
+ log: executionLog,
184
+ text: formatExecuting(beforeExecutionInfo, {
185
+ executionCount,
186
+ abortedCount,
187
+ timedoutCount,
188
+ erroredCount,
189
+ completedCount,
190
+ }),
191
+ })
192
+ }
193
+ beforeExecutionCallback(beforeExecutionInfo)
194
+
177
195
  const filePath = urlToFileSystemPath(
178
196
  `${projectDirectoryUrl}${fileRelativeUrl}`,
179
197
  )
180
- const fileExists = await pathLeadsToFile(filePath)
181
- if (!fileExists) {
182
- mainFileNotFoundCallback(beforeExecutionInfo)
183
- return
184
- }
185
-
186
- beforeExecutionCallback(beforeExecutionInfo)
198
+ let executionResult
199
+ if (existsSync(filePath)) {
200
+ executionResult = await launchAndExecute({
201
+ signal: multipleExecutionsOperation.signal,
202
+ launchAndExecuteLogLevel,
187
203
 
188
- // launchAndExecute peut retourner un aborted
189
- // et c'est bien, on veut le gérer, si tous les suivants sont aborted
190
- // on le gere en dehors de cette boucle
191
- const executionResult = await launchAndExecute({
192
- signal: multipleExecutionsOperation.signal,
193
- launchAndExecuteLogLevel,
194
-
195
- ...executionParams,
196
- collectCoverage: coverage,
197
- coverageTempDirectoryUrl,
198
- runtimeParams: {
199
- projectDirectoryUrl,
200
- compileServerOrigin,
201
- outDirectoryRelativeUrl,
204
+ ...executionParams,
202
205
  collectCoverage: coverage,
203
- coverageIgnorePredicate,
204
- coverageForceIstanbul,
205
- ...executionParams.runtimeParams,
206
- },
207
- executeParams: {
208
- fileRelativeUrl,
209
- ...executionParams.executeParams,
210
- },
211
- coverageV8ConflictWarning,
212
- })
206
+ coverageTempDirectoryUrl,
207
+ runtimeParams: {
208
+ projectDirectoryUrl,
209
+ compileServerOrigin,
210
+ outDirectoryRelativeUrl,
211
+ collectCoverage: coverage,
212
+ coverageIgnorePredicate,
213
+ coverageForceIstanbul,
214
+ ...executionParams.runtimeParams,
215
+ },
216
+ executeParams: {
217
+ fileRelativeUrl,
218
+ ...executionParams.executeParams,
219
+ },
220
+ coverageV8ConflictWarning,
221
+ })
222
+ } else {
223
+ executionResult = {
224
+ status: "errored",
225
+ error: new Error(
226
+ `No file at ${fileRelativeUrl} for execution "${executionName}"`,
227
+ ),
228
+ }
229
+ }
230
+ if (fileRelativeUrl in report === false) {
231
+ report[fileRelativeUrl] = {}
232
+ }
233
+ report[fileRelativeUrl][executionName] = executionResult
213
234
  const afterExecutionInfo = {
214
235
  ...beforeExecutionInfo,
236
+ endMs: Date.now(),
215
237
  executionResult,
216
238
  }
217
239
  afterExecutionCallback(afterExecutionInfo)
@@ -226,8 +248,8 @@ export const executeConcurrently = async (
226
248
  completedCount++
227
249
  }
228
250
 
229
- if (loggerToLevels(logger).info) {
230
- let log = createExecutionResultLog(afterExecutionInfo, {
251
+ if (executionLogsEnabled) {
252
+ let log = formatExecutionResult(afterExecutionInfo, {
231
253
  completedExecutionLogAbbreviation,
232
254
  executionCount,
233
255
  abortedCount,
@@ -242,31 +264,21 @@ export const executeConcurrently = async (
242
264
  wordWrap: false,
243
265
  })
244
266
 
245
- if (
246
- previousExecutionLog &&
247
- completedExecutionLogMerging &&
248
- previousExecutionResult &&
249
- previousExecutionResult.status === "completed" &&
250
- (previousExecutionResult.consoleCalls
251
- ? previousExecutionResult.consoleCalls.length === 0
252
- : true) &&
253
- executionResult.status === "completed"
254
- ) {
255
- previousExecutionLog.write(log)
267
+ // replace spinner with this execution result
268
+ if (spinner) spinner.stop()
269
+ executionLog.write(log)
270
+
271
+ const canOverwriteLog = canOverwriteLogGetter({
272
+ completedExecutionLogMerging,
273
+ executionResult,
274
+ })
275
+ if (canOverwriteLog) {
276
+ // nothing to do, we reuse the current executionLog object
256
277
  } else {
257
- if (previousExecutionLog) {
258
- previousExecutionLog.destroy()
259
- }
260
- previousExecutionLog = createLog()
261
- previousExecutionLog.write(log)
278
+ executionLog.destroy()
279
+ executionLog = createLog({ newLine: "around" })
262
280
  }
263
281
  }
264
-
265
- if (fileRelativeUrl in report === false) {
266
- report[fileRelativeUrl] = {}
267
- }
268
- report[fileRelativeUrl][executionName] = executionResult
269
- previousExecutionResult = executionResult
270
282
  },
271
283
  })
272
284
 
@@ -281,7 +293,7 @@ export const executeConcurrently = async (
281
293
  executionsDone.length -
282
294
  // we substract abortedCount because they are not pushed into executionsDone
283
295
  summaryCounts.abortedCount,
284
- ...(measureGlobalDuration ? { startMs, endMs: Date.now() } : {}),
296
+ duration: Date.now() - startMs,
285
297
  }
286
298
  if (logSummary) {
287
299
  logger.info(createSummaryLog(summary))
@@ -293,11 +305,36 @@ export const executeConcurrently = async (
293
305
  })
294
306
  }
295
307
 
308
+ const canOverwriteLogGetter = ({
309
+ completedExecutionLogMerging,
310
+ executionResult,
311
+ }) => {
312
+ if (!completedExecutionLogMerging) {
313
+ return false
314
+ }
315
+
316
+ if (executionResult.status === "aborted") {
317
+ return true
318
+ }
319
+
320
+ if (executionResult.status !== "completed") {
321
+ return false
322
+ }
323
+
324
+ const { consoleCalls = [] } = executionResult
325
+ if (consoleCalls.length > 0) {
326
+ return false
327
+ }
328
+
329
+ return true
330
+ }
331
+
296
332
  const executeInParallel = async ({
297
333
  multipleExecutionsOperation,
298
334
  executionSteps,
299
335
  start,
300
336
  maxExecutionsInParallel = 1,
337
+ cooldownBetweenExecutions,
301
338
  }) => {
302
339
  const executionResults = []
303
340
  let progressionIndex = 0
@@ -333,6 +370,11 @@ const executeInParallel = async ({
333
370
  if (!multipleExecutionsOperation.signal.aborted) {
334
371
  executionResults[index] = output
335
372
  }
373
+ if (cooldownBetweenExecutions) {
374
+ await new Promise((resolve) =>
375
+ setTimeout(resolve, cooldownBetweenExecutions),
376
+ )
377
+ }
336
378
  }
337
379
 
338
380
  await nextChunk()
@@ -340,22 +382,6 @@ const executeInParallel = async ({
340
382
  return executionResults
341
383
  }
342
384
 
343
- const pathLeadsToFile = (path) => {
344
- return new Promise((resolve, reject) => {
345
- stat(path, (error, stats) => {
346
- if (error) {
347
- if (error.code === "ENOENT") {
348
- resolve(false)
349
- } else {
350
- reject(error)
351
- }
352
- } else {
353
- resolve(stats.isFile())
354
- }
355
- })
356
- })
357
- }
358
-
359
385
  const reportToSummary = (report) => {
360
386
  const fileNames = Object.keys(report)
361
387
 
@@ -25,10 +25,10 @@ export const executePlan = async (
25
25
 
26
26
  defaultMsAllocatedPerExecution,
27
27
  maxExecutionsInParallel,
28
+ cooldownBetweenExecutions,
28
29
  completedExecutionLogMerging,
29
30
  completedExecutionLogAbbreviation,
30
31
  logSummary,
31
- measureGlobalDuration,
32
32
 
33
33
  coverage,
34
34
  coverageConfig,
@@ -82,7 +82,6 @@ export const executePlan = async (
82
82
  SIGINT: true,
83
83
  },
84
84
  () => {
85
- logger.info("Aborting execution (SIGINT)")
86
85
  abort()
87
86
  },
88
87
  )
@@ -148,10 +147,10 @@ export const executePlan = async (
148
147
 
149
148
  defaultMsAllocatedPerExecution,
150
149
  maxExecutionsInParallel,
150
+ cooldownBetweenExecutions,
151
151
  completedExecutionLogMerging,
152
152
  completedExecutionLogAbbreviation,
153
153
  logSummary,
154
- measureGlobalDuration,
155
154
 
156
155
  coverage,
157
156
  coverageConfig,
@@ -163,6 +162,7 @@ export const executePlan = async (
163
162
  })
164
163
 
165
164
  return {
165
+ aborted: multipleExecutionsOperation.signal.aborted,
166
166
  planSummary: result.summary,
167
167
  planReport: result.report,
168
168
  planCoverage: result.coverage,
@@ -4,8 +4,40 @@ import { msAsDuration } from "../logs/msAsDuration.js"
4
4
  import { EXECUTION_COLORS } from "./execution_colors.js"
5
5
  import { createSummaryDetails } from "./createSummaryLog.js"
6
6
 
7
- export const createExecutionResultLog = (
8
- { executionIndex, fileRelativeUrl, executionParams, executionResult },
7
+ export const formatExecuting = (
8
+ { executionIndex },
9
+ { executionCount, abortedCount, timedoutCount, erroredCount, completedCount },
10
+ ) => {
11
+ const executionNumber = executionIndex + 1
12
+ const description = ANSI.color(
13
+ `executing ${executionNumber} of ${executionCount}`,
14
+ EXECUTION_COLORS.executing,
15
+ )
16
+ const summary =
17
+ executionIndex === 0
18
+ ? ""
19
+ : `(${createSummaryDetails({
20
+ executionCount: executionIndex,
21
+ abortedCount,
22
+ timedoutCount,
23
+ erroredCount,
24
+ completedCount,
25
+ })})`
26
+
27
+ return formatExecution({
28
+ label: `${description} ${summary}`,
29
+ })
30
+ }
31
+
32
+ export const formatExecutionResult = (
33
+ {
34
+ executionIndex,
35
+ fileRelativeUrl,
36
+ runtimeName,
37
+ runtimeVersion,
38
+ executionParams,
39
+ executionResult,
40
+ },
9
41
  {
10
42
  completedExecutionLogAbbreviation,
11
43
  executionCount,
@@ -24,6 +56,7 @@ export const createExecutionResultLog = (
24
56
  executionCount,
25
57
  allocatedMs,
26
58
  })
59
+
27
60
  const summary = `(${createSummaryDetails({
28
61
  executionCount: executionNumber,
29
62
  abortedCount,
@@ -33,21 +66,22 @@ export const createExecutionResultLog = (
33
66
  })})`
34
67
 
35
68
  if (completedExecutionLogAbbreviation && status === "completed") {
36
- return `
37
- ${description} ${summary}`
69
+ return `${description} ${summary}`
38
70
  }
39
71
 
40
- const { runtimeName, runtimeVersion, consoleCalls, startMs, endMs, error } =
41
- executionResult
42
-
43
- const runtime = `${runtimeName}/${runtimeVersion}`
44
- return `
45
- ${description} ${summary}
46
- file: ${fileRelativeUrl}
47
- runtime: ${runtime}${appendDuration({
48
- startMs,
49
- endMs,
50
- })}${appendConsole(consoleCalls)}${appendError(error)}`
72
+ const { consoleCalls = [], error, duration } = executionResult
73
+ const console = formatConsoleCalls(consoleCalls)
74
+
75
+ return formatExecution({
76
+ label: `${description} ${summary}`,
77
+ details: {
78
+ file: fileRelativeUrl,
79
+ runtime: `${runtimeName}/${runtimeVersion}`,
80
+ duration: msAsDuration(duration),
81
+ ...(error ? { error: error.stack } : {}),
82
+ },
83
+ console,
84
+ })
51
85
  }
52
86
 
53
87
  const descriptionFormatters = {
@@ -83,38 +117,33 @@ const descriptionFormatters = {
83
117
  },
84
118
  }
85
119
 
86
- const appendDuration = ({ endMs, startMs }) => {
87
- if (!endMs) return ""
88
-
89
- return `
90
- duration: ${msAsDuration(endMs - startMs)}`
91
- }
92
-
93
- const appendConsole = (consoleCalls) => {
94
- if (!consoleCalls || consoleCalls.length === 0) return ""
95
-
120
+ const formatConsoleCalls = (consoleCalls) => {
96
121
  const consoleOutput = consoleCalls.reduce((previous, { text }) => {
97
122
  return `${previous}${text}`
98
123
  }, "")
99
124
 
100
125
  const consoleOutputTrimmed = consoleOutput.trim()
101
- if (consoleOutputTrimmed === "") return ""
126
+ if (consoleOutputTrimmed === "") {
127
+ return ""
128
+ }
102
129
 
103
- return `
104
- ${ANSI.color(`-------- console --------`, ANSI.GREY)}
130
+ return `${ANSI.color(`-------- console --------`, ANSI.GREY)}
105
131
  ${consoleOutputTrimmed}
106
132
  ${ANSI.color(`-------------------------`, ANSI.GREY)}`
107
133
  }
108
134
 
109
- const appendError = (error) => {
110
- if (!error) {
111
- return ``
135
+ const formatExecution = ({ label, details = {}, console }) => {
136
+ let message = ``
137
+
138
+ message += label
139
+ Object.keys(details).forEach((key) => {
140
+ message += `
141
+ ${key}: ${details[key]}`
142
+ })
143
+ if (console) {
144
+ message += `
145
+ ${console}`
112
146
  }
113
147
 
114
- return `
115
- error: ${error.stack}`
148
+ return message
116
149
  }
117
-
118
- // export const createShortExecutionResultLog = () => {
119
- // return `Execution completed (2/9) - (all completed)`
120
- // }
@@ -1,8 +1,9 @@
1
1
  import { ANSI } from "@jsenv/log"
2
2
 
3
3
  export const EXECUTION_COLORS = {
4
+ executing: ANSI.BLUE,
4
5
  aborted: ANSI.MAGENTA,
5
- timedout: ANSI.YELLOW,
6
+ timedout: ANSI.MAGENTA,
6
7
  errored: ANSI.RED,
7
8
  completed: ANSI.GREEN,
8
9
  cancelled: ANSI.GREY,
@@ -14,11 +14,8 @@ export const launchAndExecute = async ({
14
14
  executeParams,
15
15
 
16
16
  allocatedMs,
17
- measureDuration = false,
18
17
  mirrorConsole = false,
19
18
  captureConsole = false, // rename collectConsole ?
20
- collectRuntimeName = false,
21
- collectRuntimeVersion = false,
22
19
  inheritCoverage = false,
23
20
  collectCoverage = false,
24
21
  coverageTempDirectoryUrl,
@@ -102,25 +99,14 @@ export const launchAndExecute = async ({
102
99
  )
103
100
  }
104
101
 
105
- if (collectRuntimeName) {
106
- executionResultTransformer = composeTransformer(
107
- executionResultTransformer,
108
- (executionResult) => {
109
- executionResult.runtimeName = runtime.name
110
- return executionResult
111
- },
112
- )
113
- }
114
-
115
- if (collectRuntimeVersion) {
116
- executionResultTransformer = composeTransformer(
117
- executionResultTransformer,
118
- (executionResult) => {
119
- executionResult.runtimeVersion = runtime.version
120
- return executionResult
121
- },
122
- )
123
- }
102
+ executionResultTransformer = composeTransformer(
103
+ executionResultTransformer,
104
+ (executionResult) => {
105
+ executionResult.runtimeName = runtime.name
106
+ executionResult.runtimeVersion = runtime.version
107
+ return executionResult
108
+ },
109
+ )
124
110
 
125
111
  if (
126
112
  inheritCoverage &&
@@ -204,18 +190,14 @@ export const launchAndExecute = async ({
204
190
  )
205
191
  }
206
192
 
207
- if (measureDuration) {
208
- const startMs = Date.now()
209
- executionResultTransformer = composeTransformer(
210
- executionResultTransformer,
211
- (executionResult) => {
212
- const endMs = Date.now()
213
- executionResult.startMs = startMs
214
- executionResult.endMs = endMs
215
- return executionResult
216
- },
217
- )
218
- }
193
+ const startMs = Date.now()
194
+ executionResultTransformer = composeTransformer(
195
+ executionResultTransformer,
196
+ (executionResult) => {
197
+ executionResult.duration = Date.now() - startMs
198
+ return executionResult
199
+ },
200
+ )
219
201
 
220
202
  try {
221
203
  const runtimeLabel = `${runtime.name}/${runtime.version}`
@@ -2,10 +2,11 @@ import { require } from "../require.js"
2
2
 
3
3
  const humanizeDuration = require("humanize-duration")
4
4
 
5
- export const msAsDuration = (metricValue) => {
6
- return humanizeDuration(metricValue, {
5
+ export const msAsDuration = (ms) => {
6
+ return humanizeDuration(ms, {
7
7
  largest: 2,
8
- maxDecimalPoints: metricValue < 0.1 ? 3 : metricValue < 1000 ? 2 : 1,
8
+ maxDecimalPoints: ms < 0.1 ? 3 : ms < 1000 ? 2 : ms < 60000 ? 1 : 0,
9
+ delimiter: " and ",
9
10
  // units: ["s"]
10
11
  })
11
12
  }