dd-trace
Version:
Datadog APM tracing client for JavaScript
1,079 lines (948 loc) • 35 kB
JavaScript
'use strict'
const shimmer = require('../../datadog-shimmer')
const log = require('../../dd-trace/src/log')
const {
VITEST_WORKER_TRACE_PAYLOAD_CODE,
VITEST_WORKER_LOGS_PAYLOAD_CODE
} = require('../../dd-trace/src/plugins/util/test')
const { addHook, channel } = require('./helpers/instrument')
// test hooks
const testStartCh = channel('ci:vitest:test:start')
const testFinishTimeCh = channel('ci:vitest:test:finish-time')
const testPassCh = channel('ci:vitest:test:pass')
const testErrorCh = channel('ci:vitest:test:error')
const testSkipCh = channel('ci:vitest:test:skip')
const isNewTestCh = channel('ci:vitest:test:is-new')
const isAttemptToFixCh = channel('ci:vitest:test:is-attempt-to-fix')
const isDisabledCh = channel('ci:vitest:test:is-disabled')
const isQuarantinedCh = channel('ci:vitest:test:is-quarantined')
const isModifiedCh = channel('ci:vitest:test:is-modified')
// test suite hooks
const testSuiteStartCh = channel('ci:vitest:test-suite:start')
const testSuiteFinishCh = channel('ci:vitest:test-suite:finish')
const testSuiteErrorCh = channel('ci:vitest:test-suite:error')
// test session hooks
const testSessionStartCh = channel('ci:vitest:session:start')
const testSessionFinishCh = channel('ci:vitest:session:finish')
const libraryConfigurationCh = channel('ci:vitest:library-configuration')
const knownTestsCh = channel('ci:vitest:known-tests')
const isEarlyFlakeDetectionFaultyCh = channel('ci:vitest:is-early-flake-detection-faulty')
const testManagementTestsCh = channel('ci:vitest:test-management-tests')
const modifiedFilesCh = channel('ci:vitest:modified-files')
const workerReportTraceCh = channel('ci:vitest:worker-report:trace')
const workerReportLogsCh = channel('ci:vitest:worker-report:logs')
const taskToCtx = new WeakMap()
const taskToStatuses = new WeakMap()
const newTasks = new WeakSet()
const disabledTasks = new WeakSet()
const quarantinedTasks = new WeakSet()
const attemptToFixTasks = new WeakSet()
const modifiedTasks = new WeakSet()
let isRetryReasonEfd = false
let isRetryReasonAttemptToFix = false
const switchedStatuses = new WeakSet()
const workerProcesses = new WeakSet()
let isFlakyTestRetriesEnabled = false
let flakyTestRetriesCount = 0
let isEarlyFlakeDetectionEnabled = false
let earlyFlakeDetectionNumRetries = 0
let isEarlyFlakeDetectionFaulty = false
let isKnownTestsEnabled = false
let isTestManagementTestsEnabled = false
let isImpactedTestsEnabled = false
let testManagementAttemptToFixRetries = 0
let isDiEnabled = false
let testCodeCoverageLinesTotal
let isSessionStarted = false
let vitestPool = null
const BREAKPOINT_HIT_GRACE_PERIOD_MS = 400
function getTestCommand () {
return `vitest ${process.argv.slice(2).join(' ')}`
}
function waitForHitProbe () {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, BREAKPOINT_HIT_GRACE_PERIOD_MS)
})
}
function isValidKnownTests (receivedKnownTests) {
return !!receivedKnownTests.vitest
}
function getProvidedContext () {
try {
const {
_ddIsEarlyFlakeDetectionEnabled,
_ddIsDiEnabled,
_ddKnownTests: knownTests,
_ddEarlyFlakeDetectionNumRetries: numRepeats,
_ddIsKnownTestsEnabled: isKnownTestsEnabled,
_ddIsTestManagementTestsEnabled: isTestManagementTestsEnabled,
_ddTestManagementAttemptToFixRetries: testManagementAttemptToFixRetries,
_ddTestManagementTests: testManagementTests,
_ddIsFlakyTestRetriesEnabled: isFlakyTestRetriesEnabled,
_ddIsImpactedTestsEnabled: isImpactedTestsEnabled,
_ddModifiedFiles: modifiedFiles
} = globalThis.__vitest_worker__.providedContext
return {
isDiEnabled: _ddIsDiEnabled,
isEarlyFlakeDetectionEnabled: _ddIsEarlyFlakeDetectionEnabled,
knownTests,
numRepeats,
isKnownTestsEnabled,
isTestManagementTestsEnabled,
testManagementAttemptToFixRetries,
testManagementTests,
isFlakyTestRetriesEnabled,
isImpactedTestsEnabled,
modifiedFiles
}
} catch {
log.error('Vitest workers could not parse provided context, so some features will not work.')
return {
isDiEnabled: false,
isEarlyFlakeDetectionEnabled: false,
knownTests: {},
numRepeats: 0,
isKnownTestsEnabled: false,
isTestManagementTestsEnabled: false,
testManagementAttemptToFixRetries: 0,
testManagementTests: {},
isFlakyTestRetriesEnabled: false,
isImpactedTestsEnabled: false,
modifiedFiles: {}
}
}
}
function isReporterPackage (vitestPackage) {
return vitestPackage.B?.name === 'BaseSequencer'
}
// from 2.0.0
function isReporterPackageNew (vitestPackage) {
return vitestPackage.e?.name === 'BaseSequencer'
}
function isReporterPackageNewest (vitestPackage) {
return vitestPackage.h?.name === 'BaseSequencer'
}
function isBaseSequencer (vitestPackage) {
return vitestPackage.b?.name === 'BaseSequencer'
}
function getChannelPromise (channelToPublishTo, frameworkVersion) {
return new Promise(resolve => {
channelToPublishTo.publish({ onDone: resolve, frameworkVersion })
})
}
function isCliApiPackage (vitestPackage) {
return vitestPackage.s?.name === 'startVitest'
}
function isTestPackage (testPackage) {
return testPackage.V?.name === 'VitestTestRunner'
}
function hasForksPoolWorker (vitestPackage) {
return vitestPackage.f?.name === 'ForksPoolWorker'
}
function hasThreadsPoolWorker (vitestPackage) {
return vitestPackage.T?.name === 'ThreadsPoolWorker'
}
function getSessionStatus (state) {
if (state.getCountOfFailedTests() > 0) {
return 'fail'
}
if (state.pathsSet.size === 0) {
return 'skip'
}
return 'pass'
}
// From https://github.com/vitest-dev/vitest/blob/51c04e2f44d91322b334f8ccbcdb368facc3f8ec/packages/runner/src/run.ts#L243-L250
function getVitestTestStatus (test, retryCount) {
if (test.result.state !== 'fail') {
if (!test.repeats) {
return 'pass'
} else if (test.repeats && (test.retry ?? 0) === retryCount) {
return 'pass'
}
}
return 'fail'
}
function getTypeTasks (fileTasks, type = 'test') {
const typeTasks = []
function getTasks (tasks) {
for (const task of tasks) {
if (task.type === type) {
typeTasks.push(task)
} else if (task.tasks) {
getTasks(task.tasks)
}
}
}
getTasks(fileTasks)
return typeTasks
}
function getTestName (task) {
let testName = task.name
let currentTask = task.suite
while (currentTask) {
if (currentTask.name) {
testName = `${currentTask.name} ${testName}`
}
currentTask = currentTask.suite
}
return testName
}
function getSortWrapper (sort, frameworkVersion) {
return async function () {
if (!testSessionFinishCh.hasSubscribers) {
return sort.apply(this, arguments)
}
// There isn't any other async function that we seem to be able to hook into
// So we will use the sort from BaseSequencer. This means that a custom sequencer
// will not work. This will be a known limitation.
try {
const { err, libraryConfig } = await getChannelPromise(libraryConfigurationCh, frameworkVersion)
if (!err) {
isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
isDiEnabled = libraryConfig.isDiEnabled
isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
testManagementAttemptToFixRetries = libraryConfig.testManagementAttemptToFixRetries
isImpactedTestsEnabled = libraryConfig.isImpactedTestsEnabled
}
} catch {
isFlakyTestRetriesEnabled = false
isEarlyFlakeDetectionEnabled = false
isDiEnabled = false
isKnownTestsEnabled = false
isImpactedTestsEnabled = false
}
if (isFlakyTestRetriesEnabled && !this.ctx.config.retry && flakyTestRetriesCount > 0) {
this.ctx.config.retry = flakyTestRetriesCount
try {
const workspaceProject = this.ctx.getCoreWorkspaceProject
? this.ctx.getCoreWorkspaceProject()
: this.ctx.getRootProject()
workspaceProject._provided._ddIsFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled
} catch {
log.warn('Could not send library configuration to workers.')
}
}
if (isKnownTestsEnabled) {
const knownTestsResponse = await getChannelPromise(knownTestsCh)
if (knownTestsResponse.err) {
isEarlyFlakeDetectionEnabled = false
} else {
const knownTests = knownTestsResponse.knownTests
const getFilePaths = this.ctx.getTestFilepaths || this.ctx._globTestFilepaths
const testFilepaths = await getFilePaths.call(this.ctx)
if (isValidKnownTests(knownTests)) {
isEarlyFlakeDetectionFaultyCh.publish({
knownTests: knownTests.vitest,
testFilepaths,
onDone: (isFaulty) => {
isEarlyFlakeDetectionFaulty = isFaulty
}
})
if (isEarlyFlakeDetectionFaulty) {
isEarlyFlakeDetectionEnabled = false
log.warn('New test detection is disabled because the number of new tests is too high.')
} else {
// TODO: use this to pass session and module IDs to the worker, instead of polluting process.env
// Note: setting this.ctx.config.provide directly does not work because it's cached
try {
const workspaceProject = this.ctx.getCoreWorkspaceProject
? this.ctx.getCoreWorkspaceProject()
: this.ctx.getRootProject()
workspaceProject._provided._ddIsKnownTestsEnabled = isKnownTestsEnabled
workspaceProject._provided._ddKnownTests = knownTests
workspaceProject._provided._ddIsEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
workspaceProject._provided._ddEarlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
} catch {
log.warn('Could not send known tests to workers so Early Flake Detection will not work.')
}
}
} else {
isEarlyFlakeDetectionFaulty = true
isEarlyFlakeDetectionEnabled = false
}
}
}
if (isDiEnabled) {
try {
const workspaceProject = this.ctx.getCoreWorkspaceProject
? this.ctx.getCoreWorkspaceProject()
: this.ctx.getRootProject()
workspaceProject._provided._ddIsDiEnabled = isDiEnabled
} catch {
log.warn('Could not send Dynamic Instrumentation configuration to workers.')
}
}
if (isTestManagementTestsEnabled) {
const { err, testManagementTests: receivedTestManagementTests } = await getChannelPromise(testManagementTestsCh)
if (err) {
isTestManagementTestsEnabled = false
log.error('Could not get test management tests.')
} else {
const testManagementTests = receivedTestManagementTests
try {
const workspaceProject = this.ctx.getCoreWorkspaceProject
? this.ctx.getCoreWorkspaceProject()
: this.ctx.getRootProject()
workspaceProject._provided._ddIsTestManagementTestsEnabled = isTestManagementTestsEnabled
workspaceProject._provided._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries
workspaceProject._provided._ddTestManagementTests = testManagementTests
} catch {
log.warn('Could not send test management tests to workers so Test Management will not work.')
}
}
}
if (isImpactedTestsEnabled) {
const { err, modifiedFiles } = await getChannelPromise(modifiedFilesCh)
if (err) {
log.error('Could not get modified tests.')
} else {
try {
const workspaceProject = this.ctx.getCoreWorkspaceProject
? this.ctx.getCoreWorkspaceProject()
: this.ctx.getRootProject()
workspaceProject._provided._ddIsImpactedTestsEnabled = isImpactedTestsEnabled
workspaceProject._provided._ddModifiedFiles = modifiedFiles
} catch {
log.warn('Could not send modified tests to workers so Impacted Tests will not work.')
}
}
}
if (this.ctx.coverageProvider?.generateCoverage) {
shimmer.wrap(this.ctx.coverageProvider, 'generateCoverage', generateCoverage => async function () {
const totalCodeCoverage = await generateCoverage.apply(this, arguments)
try {
testCodeCoverageLinesTotal = totalCodeCoverage.getCoverageSummary().lines.pct
} catch {
// ignore errors
}
return totalCodeCoverage
})
}
shimmer.wrap(this.ctx, 'exit', getFinishWrapper)
shimmer.wrap(this.ctx, 'close', getFinishWrapper)
return sort.apply(this, arguments)
}
}
function getFinishWrapper (exitOrClose) {
let isClosed = false
return async function () {
if (isClosed) { // needed because exit calls close
return exitOrClose.apply(this, arguments)
}
isClosed = true
let onFinish
const flushPromise = new Promise(resolve => {
onFinish = resolve
})
const failedSuites = this.state.getFailedFilepaths()
let error
if (failedSuites.length) {
error = new Error(`Test suites failed: ${failedSuites.length}.`)
}
testSessionFinishCh.publish({
status: getSessionStatus(this.state),
testCodeCoverageLinesTotal,
error,
isEarlyFlakeDetectionEnabled,
isEarlyFlakeDetectionFaulty,
isTestManagementTestsEnabled,
vitestPool,
onFinish
})
await flushPromise
return exitOrClose.apply(this, arguments)
}
}
function getCliOrStartVitestWrapper (frameworkVersion) {
return function (oldCliOrStartVitest) {
return function () {
if (!testSessionStartCh.hasSubscribers || isSessionStarted) {
return oldCliOrStartVitest.apply(this, arguments)
}
isSessionStarted = true
testSessionStartCh.publish({ command: getTestCommand(), frameworkVersion })
return oldCliOrStartVitest.apply(this, arguments)
}
}
}
function getCreateCliWrapper (vitestPackage, frameworkVersion) {
shimmer.wrap(vitestPackage, 'c', getCliOrStartVitestWrapper(frameworkVersion))
return vitestPackage
}
function threadHandler (thread) {
const { runtime } = thread
let workerProcess
if (runtime === 'child_process') {
vitestPool = 'child_process'
workerProcess = thread.process
} else if (runtime === 'worker_threads') {
vitestPool = 'worker_threads'
workerProcess = thread.thread
} else {
vitestPool = 'unknown'
}
if (!workerProcess) {
log.error('Vitest error: could not get process or thread from TinyPool#run')
return
}
if (workerProcesses.has(workerProcess)) {
return
}
workerProcesses.add(workerProcess)
workerProcess.on('message', (message) => {
if (message.__tinypool_worker_message__ && message.data) {
if (message.interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE) {
workerReportTraceCh.publish(message.data)
} else if (message.interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE) {
workerReportLogsCh.publish(message.data)
}
}
})
}
function wrapTinyPoolRun (TinyPool) {
shimmer.wrap(TinyPool.prototype, 'run', run => async function () {
// We have to do this before and after because the threads list gets recycled, that is, the processes are re-created
this.threads.forEach(threadHandler)
const runResult = await run.apply(this, arguments)
this.threads.forEach(threadHandler)
return runResult
})
}
addHook({
name: 'tinypool',
// version from tinypool@0.8 was used in vitest@1.6.0
versions: ['>=0.8.0 <1.0.0'],
file: 'dist/esm/index.js'
}, (TinyPool) => {
wrapTinyPoolRun(TinyPool)
return TinyPool
})
addHook({
name: 'tinypool',
versions: ['>=1.0.0'],
file: 'dist/index.js'
}, (TinyPool) => {
wrapTinyPoolRun(TinyPool)
return TinyPool
})
function getWrappedOn (on) {
return function (event, callback) {
if (event !== 'message') {
return on.apply(this, arguments)
}
// `arguments[1]` is the callback function, which
// we modify to intercept our messages to not interfere
// with vitest's own messages
arguments[1] = shimmer.wrapFunction(callback, callback => function (message) {
if (message.type !== 'Buffer' && Array.isArray(message)) {
const [interprocessCode, data] = message
if (interprocessCode === VITEST_WORKER_TRACE_PAYLOAD_CODE) {
workerReportTraceCh.publish(data)
} else if (interprocessCode === VITEST_WORKER_LOGS_PAYLOAD_CODE) {
workerReportLogsCh.publish(data)
}
// If we execute the callback vitest crashes, as the message is not supported
return
}
return callback.apply(this, arguments)
})
return on.apply(this, arguments)
}
}
function getStartVitestWrapper (cliApiPackage, frameworkVersion) {
if (!isCliApiPackage(cliApiPackage)) {
return cliApiPackage
}
shimmer.wrap(cliApiPackage, 's', getCliOrStartVitestWrapper(frameworkVersion))
if (hasForksPoolWorker(cliApiPackage)) {
// function is async
shimmer.wrap(cliApiPackage.f.prototype, 'start', start => function () {
vitestPool = 'child_process'
this.env.DD_VITEST_WORKER = '1'
return start.apply(this, arguments)
})
shimmer.wrap(cliApiPackage.f.prototype, 'on', getWrappedOn)
}
if (hasThreadsPoolWorker(cliApiPackage)) {
// function is async
shimmer.wrap(cliApiPackage.T.prototype, 'start', start => function () {
vitestPool = 'worker_threads'
this.env.DD_VITEST_WORKER = '1'
return start.apply(this, arguments)
})
shimmer.wrap(cliApiPackage.T.prototype, 'on', getWrappedOn)
}
return cliApiPackage
}
function wrapVitestTestRunner (VitestTestRunner) {
// `onBeforeRunTask` is run before any repetition or attempt is run
// `onBeforeRunTask` is an async function
shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => function (task) {
const testName = getTestName(task)
const {
knownTests,
isEarlyFlakeDetectionEnabled,
isKnownTestsEnabled,
numRepeats,
isTestManagementTestsEnabled,
testManagementAttemptToFixRetries,
testManagementTests,
isImpactedTestsEnabled,
modifiedFiles
} = getProvidedContext()
if (isTestManagementTestsEnabled) {
isAttemptToFixCh.publish({
testManagementTests,
testSuiteAbsolutePath: task.file.filepath,
testName,
onDone: (isAttemptToFix) => {
if (isAttemptToFix) {
isRetryReasonAttemptToFix = task.repeats !== testManagementAttemptToFixRetries
task.repeats = testManagementAttemptToFixRetries
attemptToFixTasks.add(task)
taskToStatuses.set(task, [])
}
}
})
isDisabledCh.publish({
testManagementTests,
testSuiteAbsolutePath: task.file.filepath,
testName,
onDone: (isTestDisabled) => {
if (isTestDisabled) {
disabledTasks.add(task)
if (!attemptToFixTasks.has(task)) {
// we only actually skip if the test is not being attempted to be fixed
task.mode = 'skip'
}
}
}
})
}
if (isImpactedTestsEnabled) {
isModifiedCh.publish({
modifiedFiles,
testSuiteAbsolutePath: task.file.filepath,
onDone: (isImpacted) => {
if (isImpacted) {
if (isEarlyFlakeDetectionEnabled) {
isRetryReasonEfd = task.repeats !== numRepeats
task.repeats = numRepeats
}
modifiedTasks.add(task)
taskToStatuses.set(task, [])
}
}
})
}
if (isKnownTestsEnabled) {
isNewTestCh.publish({
knownTests,
testSuiteAbsolutePath: task.file.filepath,
testName,
onDone: (isNew) => {
if (isNew && !attemptToFixTasks.has(task)) {
if (isEarlyFlakeDetectionEnabled && !modifiedTasks.has(task)) {
isRetryReasonEfd = task.repeats !== numRepeats
task.repeats = numRepeats
}
newTasks.add(task)
taskToStatuses.set(task, [])
}
}
})
}
return onBeforeRunTask.apply(this, arguments)
})
// `onAfterRunTask` is run after all repetitions or attempts are run
// `onAfterRunTask` is an async function
shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => function (task) {
const { isEarlyFlakeDetectionEnabled, isTestManagementTestsEnabled } = getProvidedContext()
if (isTestManagementTestsEnabled) {
const isAttemptingToFix = attemptToFixTasks.has(task)
const isDisabled = disabledTasks.has(task)
const isQuarantined = quarantinedTasks.has(task)
if (isAttemptingToFix && (isDisabled || isQuarantined)) {
if (task.result.state === 'fail') {
switchedStatuses.add(task)
}
task.result.state = 'pass'
} else if (isQuarantined) {
task.result.state = 'pass'
}
}
if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task) && !attemptToFixTasks.has(task)) {
const statuses = taskToStatuses.get(task)
// If the test has passed at least once, we consider it passed
if (statuses.includes('pass')) {
if (task.result.state === 'fail') {
switchedStatuses.add(task)
}
task.result.state = 'pass'
}
}
return onAfterRunTask.apply(this, arguments)
})
// test start (only tests that are not marked as skip or todo)
// `onBeforeTryTask` is run for every repetition and attempt of the test
shimmer.wrap(VitestTestRunner.prototype, 'onBeforeTryTask', onBeforeTryTask => async function (task, retryInfo) {
if (!testPassCh.hasSubscribers && !testErrorCh.hasSubscribers && !testSkipCh.hasSubscribers) {
return onBeforeTryTask.apply(this, arguments)
}
const testName = getTestName(task)
let isNew = false
let isQuarantined = false
const {
isKnownTestsEnabled,
isEarlyFlakeDetectionEnabled,
isDiEnabled,
isTestManagementTestsEnabled,
testManagementTests,
isFlakyTestRetriesEnabled
} = getProvidedContext()
if (isKnownTestsEnabled) {
isNew = newTasks.has(task)
}
if (isTestManagementTestsEnabled) {
isQuarantinedCh.publish({
testManagementTests,
testSuiteAbsolutePath: task.file.filepath,
testName,
onDone: (isTestQuarantined) => {
isQuarantined = isTestQuarantined
if (isTestQuarantined) {
quarantinedTasks.add(task)
}
}
})
}
const { retry: numAttempt, repeats: numRepetition } = retryInfo
// We finish the previous test here because we know it has failed already
if (numAttempt > 0) {
const shouldWaitForHitProbe = isDiEnabled && numAttempt > 1
if (shouldWaitForHitProbe) {
await waitForHitProbe()
}
const promises = {}
const shouldSetProbe = isDiEnabled && numAttempt === 1
const ctx = taskToCtx.get(task)
const testError = task.result?.errors?.[0]
if (ctx) {
testErrorCh.publish({
error: testError,
shouldSetProbe,
promises,
...ctx.currentStore
})
// We wait for the probe to be set
if (promises.setProbePromise) {
await promises.setProbePromise
}
}
}
const lastExecutionStatus = task.result.state
const shouldFlipStatus = isEarlyFlakeDetectionEnabled || attemptToFixTasks.has(task)
const statuses = taskToStatuses.get(task)
// These clauses handle task.repeats, whether EFD is enabled or not
// The only thing that EFD does is to forcefully pass the test if it has passed at least once
if (numRepetition > 0 && numRepetition < task.repeats) { // it may or may have not failed
// Here we finish the earlier iteration,
// as long as it's not the _last_ iteration (which will be finished normally)
// TODO: check test duration (not to repeat if it's too slow)
const ctx = taskToCtx.get(task)
if (ctx) {
if (lastExecutionStatus === 'fail') {
const testError = task.result?.errors?.[0]
testErrorCh.publish({ error: testError, ...ctx.currentStore })
} else {
testPassCh.publish({ task, ...ctx.currentStore })
}
if (shouldFlipStatus) {
statuses.push(lastExecutionStatus)
// If we don't "reset" the result.state to "pass", once a repetition fails,
// vitest will always consider the test as failed, so we can't read the actual status
// This means that we change vitest's behavior:
// if the last attempt passes, vitest would consider the test as failed
// but after this change, it will consider the test as passed
task.result.state = 'pass'
}
}
} else if (numRepetition === task.repeats) {
if (shouldFlipStatus) {
statuses.push(lastExecutionStatus)
}
const ctx = taskToCtx.get(task)
if (lastExecutionStatus === 'fail') {
const testError = task.result?.errors?.[0]
testErrorCh.publish({ error: testError, ...ctx.currentStore })
} else {
testPassCh.publish({ task, ...ctx.currentStore })
}
}
const isRetryReasonAtr = numAttempt > 0 &&
isFlakyTestRetriesEnabled &&
!isRetryReasonAttemptToFix &&
!isRetryReasonEfd
const ctx = {
testName,
testSuiteAbsolutePath: task.file.filepath,
isRetry: numAttempt > 0 || numRepetition > 0,
isRetryReasonEfd,
isRetryReasonAttemptToFix: isRetryReasonAttemptToFix && numRepetition > 0,
isNew,
mightHitProbe: isDiEnabled && numAttempt > 0,
isAttemptToFix: attemptToFixTasks.has(task),
isDisabled: disabledTasks.has(task),
isQuarantined,
isRetryReasonAtr,
isModified: modifiedTasks.has(task)
}
taskToCtx.set(task, ctx)
testStartCh.runStores(ctx, () => {})
return onBeforeTryTask.apply(this, arguments)
})
// test finish (only passed tests)
shimmer.wrap(VitestTestRunner.prototype, 'onAfterTryTask', onAfterTryTask =>
async function (task, { retry: retryCount }) {
if (!testPassCh.hasSubscribers && !testErrorCh.hasSubscribers && !testSkipCh.hasSubscribers) {
return onAfterTryTask.apply(this, arguments)
}
const result = await onAfterTryTask.apply(this, arguments)
const { testManagementAttemptToFixRetries } = getProvidedContext()
const status = getVitestTestStatus(task, retryCount)
const ctx = taskToCtx.get(task)
const { isDiEnabled } = getProvidedContext()
if (isDiEnabled && retryCount > 1) {
await waitForHitProbe()
}
let attemptToFixPassed = false
let attemptToFixFailed = false
if (attemptToFixTasks.has(task)) {
const statuses = taskToStatuses.get(task)
if (statuses.length === testManagementAttemptToFixRetries) {
if (statuses.every(status => status === 'pass')) {
attemptToFixPassed = true
} else if (statuses.includes('fail')) {
attemptToFixFailed = true
}
}
}
if (ctx) {
// We don't finish here because the test might fail in a later hook (afterEach)
ctx.status = status
ctx.task = task
ctx.attemptToFixPassed = attemptToFixPassed
ctx.attemptToFixFailed = attemptToFixFailed
testFinishTimeCh.runStores(ctx, () => {})
}
return result
})
}
addHook({
name: 'vitest',
versions: ['>=4.0.0'],
filePattern: 'dist/chunks/test.*'
}, (testPackage) => {
if (!isTestPackage(testPackage)) {
return testPackage
}
wrapVitestTestRunner(testPackage.V)
return testPackage
})
addHook({
name: 'vitest',
versions: ['>=1.6.0 <4.0.0'],
file: 'dist/runners.js'
}, (vitestPackage) => {
const { VitestTestRunner } = vitestPackage
wrapVitestTestRunner(VitestTestRunner)
return vitestPackage
})
// There are multiple index* files across different versions of vitest,
// so we check for the existence of BaseSequencer to determine if we are in the right file
addHook({
name: 'vitest',
versions: ['>=1.6.0 <2.0.0'],
filePattern: 'dist/vendor/index.*'
}, (vitestPackage) => {
if (isReporterPackage(vitestPackage)) {
shimmer.wrap(vitestPackage.B.prototype, 'sort', getSortWrapper)
}
return vitestPackage
})
addHook({
name: 'vitest',
versions: ['>=2.0.0 <2.0.5'],
filePattern: 'dist/vendor/index.*'
}, (vitestPackage) => {
if (isReporterPackageNew(vitestPackage)) {
shimmer.wrap(vitestPackage.e.prototype, 'sort', getSortWrapper)
}
return vitestPackage
})
addHook({
name: 'vitest',
versions: ['>=2.0.5 <2.1.0'],
filePattern: 'dist/chunks/index.*'
}, (vitestPackage) => {
if (isReporterPackageNewest(vitestPackage)) {
shimmer.wrap(vitestPackage.h.prototype, 'sort', getSortWrapper)
}
return vitestPackage
})
addHook({
name: 'vitest',
versions: ['>=2.1.0 <3.0.0'],
filePattern: 'dist/chunks/RandomSequencer.*'
}, (randomSequencerPackage) => {
shimmer.wrap(randomSequencerPackage.B.prototype, 'sort', getSortWrapper)
return randomSequencerPackage
})
addHook({
name: 'vitest',
versions: ['>=3.0.9'],
filePattern: 'dist/chunks/coverage.*'
}, (coveragePackage) => {
if (isBaseSequencer(coveragePackage)) {
shimmer.wrap(coveragePackage.b.prototype, 'sort', getSortWrapper)
}
return coveragePackage
})
addHook({
name: 'vitest',
versions: ['>=3.0.0 <3.0.9'],
filePattern: 'dist/chunks/resolveConfig.*'
}, (resolveConfigPackage) => {
shimmer.wrap(resolveConfigPackage.B.prototype, 'sort', getSortWrapper)
return resolveConfigPackage
})
// Can't specify file because compiled vitest includes hashes in their files
// Following 3 wrappers are for test session start
addHook({
name: 'vitest',
versions: ['>=1.6.0 <2.0.5'],
filePattern: 'dist/vendor/cac.*'
}, getCreateCliWrapper)
addHook({
name: 'vitest',
versions: ['>=2.0.5'],
filePattern: 'dist/chunks/cac.*'
}, getCreateCliWrapper)
addHook({
name: 'vitest',
versions: ['>=1.6.0 <2.0.5'],
filePattern: 'dist/vendor/cli-api.*'
}, getStartVitestWrapper)
addHook({
name: 'vitest',
versions: ['>=2.0.5'],
filePattern: 'dist/chunks/cli-api.*'
}, getStartVitestWrapper)
// test suite start and finish
// only relevant for workers
addHook({
name: '@vitest/runner',
versions: ['>=1.6.0'],
file: 'dist/index.js'
}, (vitestPackage, frameworkVersion) => {
shimmer.wrap(vitestPackage, 'startTests', startTests => async function (testPaths) {
let testSuiteError = null
if (!testSuiteFinishCh.hasSubscribers) {
return startTests.apply(this, arguments)
}
// From >=3.0.1, the first arguments changes from a string to an object containing the filepath
const testSuiteAbsolutePath = testPaths[0]?.filepath || testPaths[0]
const testSuiteCtx = { testSuiteAbsolutePath, frameworkVersion }
testSuiteStartCh.runStores(testSuiteCtx, () => {})
const startTestsResponse = await startTests.apply(this, arguments)
let onFinish = null
const onFinishPromise = new Promise(resolve => {
onFinish = resolve
})
const testTasks = getTypeTasks(startTestsResponse[0].tasks)
// Only one test task per test, even if there are retries
testTasks.forEach(task => {
const testCtx = taskToCtx.get(task)
const { result } = task
// We have to trick vitest into thinking that the test has passed
// but we want to report it as failed if it did fail
const isSwitchedStatus = switchedStatuses.has(task)
if (result) {
const { state, duration, errors } = result
if (state === 'skip') { // programmatic skip
testSkipCh.publish({
testName: getTestName(task),
testSuiteAbsolutePath: task.file.filepath,
isNew: newTasks.has(task),
isDisabled: disabledTasks.has(task)
})
} else if (state === 'pass' && !isSwitchedStatus) {
if (testCtx) {
testPassCh.publish({ task, ...testCtx.currentStore })
}
} else if (state === 'fail' || isSwitchedStatus) {
let testError
if (errors?.length) {
testError = errors[0]
}
let hasFailedAllRetries = false
let attemptToFixFailed = false
if (attemptToFixTasks.has(task)) {
const statuses = taskToStatuses.get(task)
if (statuses.includes('fail')) {
attemptToFixFailed = true
}
if (statuses.every(status => status === 'fail')) {
hasFailedAllRetries = true
}
}
if (testCtx) {
const isRetry = task.result?.retryCount > 0
// `duration` is the duration of all the retries, so it can't be used if there are retries
testErrorCh.publish({
duration: isRetry ? undefined : duration,
error: testError,
hasFailedAllRetries,
attemptToFixFailed,
...testCtx.currentStore
})
}
if (errors?.length) {
testSuiteError = testError // we store the error to bubble it up to the suite
}
}
} else { // test.skip or test.todo
testSkipCh.publish({
testName: getTestName(task),
testSuiteAbsolutePath: task.file.filepath,
isNew: newTasks.has(task),
isDisabled: disabledTasks.has(task)
})
}
})
const testSuiteResult = startTestsResponse[0].result
if (testSuiteResult.errors?.length) { // Errors from root level hooks
testSuiteError = testSuiteResult.errors[0]
} else if (testSuiteResult.state === 'fail') { // Errors from `describe` level hooks
const suiteTasks = getTypeTasks(startTestsResponse[0].tasks, 'suite')
const failedSuites = suiteTasks.filter(task => task.result?.state === 'fail')
if (failedSuites.length && failedSuites[0].result?.errors?.length) {
testSuiteError = failedSuites[0].result.errors[0]
}
}
if (testSuiteError) {
testSuiteCtx.error = testSuiteError
testSuiteErrorCh.runStores(testSuiteCtx, () => {})
}
testSuiteFinishCh.publish({ status: testSuiteResult.state, onFinish, ...testSuiteCtx.currentStore })
await onFinishPromise
return startTestsResponse
})
return vitestPackage
})