Skip to content

Commit a65e715

Browse files
authored
chore: improve int summary counts when content-api is unavailable (#16127)
## Summary - make `test:int:summary` resilient when `PAYLOAD_DATABASE=content-api` is configured but the content API service is not reachable - add fallback collection counting via `vitest list --json` so suites report `0/X` instead of `0/0` - keep totals stable by excluding explicit skipped tests (`it.skip`, `test.skip`, and `mongoIt` when not on mongodb) while preserving todo exclusion - add a content-api timeout guard per suite to avoid hanging runs ## Validation - `PAYLOAD_DATABASE=content-api pnpm test:int:summary` now reports totals per suite (no more `0/0`) - compared totals with and without explicit skip exclusion in this checkout: - without explicit skip exclusion: `4/1885` - with explicit skip exclusion: `4/1879` - delta: `-6` - `PAYLOAD_DATABASE=content-api pnpm test:int` was also checked to validate why parsing pending/skipped directly from failing runs is misleading (many tests become skipped/queued due early setup failure) Co-authored-by: German Jablonski <GermanJablo@users.noreply.github.com>
1 parent 9284441 commit a65e715

1 file changed

Lines changed: 211 additions & 5 deletions

File tree

test/runTestsWithSummary.ts

Lines changed: 211 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { execSync } from 'child_process'
22
import fs from 'fs'
33
import path from 'path'
4+
import ts from 'typescript'
45
import { fileURLToPath } from 'url'
56

67
const filename = fileURLToPath(import.meta.url)
@@ -74,6 +75,25 @@ interface SuiteResult {
7475
total: number
7576
}
7677

78+
const isContentAPIMode = process.env.PAYLOAD_DATABASE === 'content-api'
79+
const contentAPISuiteTimeout = 15000
80+
const vitestBinary = './node_modules/.bin/vitest'
81+
82+
function getVitestEnv(options?: { unsetPayloadDatabase?: boolean }): NodeJS.ProcessEnv {
83+
const env = {
84+
...process.env,
85+
DISABLE_LOGGING: 'true',
86+
NODE_NO_WARNINGS: '1',
87+
NODE_OPTIONS: '--no-deprecation --no-experimental-strip-types',
88+
}
89+
90+
if (options?.unsetPayloadDatabase) {
91+
delete env.PAYLOAD_DATABASE
92+
}
93+
94+
return env
95+
}
96+
7797
function getTestDirectories(): string[] {
7898
const testDir = dirname
7999

@@ -143,9 +163,11 @@ function parseTestResults(output: string): { passed: number; total: number } {
143163
try {
144164
const data = JSON.parse(candidate)
145165
if (typeof data.numPassedTests === 'number' && typeof data.numTotalTests === 'number') {
166+
const excludedTests = data.numTodoTests || 0
167+
146168
return {
147169
passed: data.numPassedTests,
148-
total: data.numTotalTests - (data.numTodoTests || 0),
170+
total: Math.max(0, data.numTotalTests - excludedTests),
149171
}
150172
}
151173
} catch (e) {
@@ -160,6 +182,176 @@ function parseTestResults(output: string): { passed: number; total: number } {
160182
}
161183
}
162184

185+
function parseCollectedTests(output: string): number {
186+
const jsonStart = output.lastIndexOf('\n[') + 1 || output.indexOf('[')
187+
if (jsonStart === -1) {
188+
return 0
189+
}
190+
191+
let candidate = output.substring(jsonStart)
192+
193+
while (candidate.length > 10) {
194+
try {
195+
const data = JSON.parse(candidate)
196+
197+
if (Array.isArray(data)) {
198+
return data.length
199+
}
200+
} catch (e) {
201+
candidate = candidate.substring(0, candidate.length - 1)
202+
}
203+
}
204+
205+
return 0
206+
}
207+
208+
function getCollectedTestCount(suiteName: string): number {
209+
const testPath = path.join(dirname, suiteName, 'int.spec.ts')
210+
211+
for (const unsetPayloadDatabase of [false, true]) {
212+
try {
213+
const command = `${vitestBinary} list --project int ${testPath} --json`
214+
215+
const output = execSync(command, {
216+
cwd: path.join(dirname, '..'),
217+
encoding: 'utf8',
218+
env: getVitestEnv({ unsetPayloadDatabase }),
219+
stdio: ['pipe', 'pipe', 'pipe'],
220+
...(isContentAPIMode ? { timeout: contentAPISuiteTimeout } : {}),
221+
})
222+
223+
const count = parseCollectedTests(output)
224+
if (count > 0) {
225+
return count
226+
}
227+
} catch (error: unknown) {
228+
let errorOutput = ''
229+
if (error && typeof error === 'object') {
230+
if ('stdout' in error) {
231+
const stdout = (error as { stdout?: unknown }).stdout
232+
if (typeof stdout === 'string') {
233+
errorOutput += stdout
234+
} else if (stdout && Buffer.isBuffer(stdout)) {
235+
errorOutput += stdout.toString('utf8')
236+
}
237+
}
238+
if ('stderr' in error) {
239+
const stderr = (error as { stderr?: unknown }).stderr
240+
if (typeof stderr === 'string') {
241+
errorOutput += '\n' + stderr
242+
} else if (stderr && Buffer.isBuffer(stderr)) {
243+
errorOutput += '\n' + stderr.toString('utf8')
244+
}
245+
}
246+
}
247+
248+
const count = parseCollectedTests(errorOutput)
249+
if (count > 0) {
250+
return count
251+
}
252+
}
253+
}
254+
255+
return 0
256+
}
257+
258+
function isBaseTestIdentifier(node: ts.Node): node is ts.Identifier {
259+
return ts.isIdentifier(node) && (node.text === 'it' || node.text === 'test')
260+
}
261+
262+
function isRunnableTestCall(node: ts.CallExpression): boolean {
263+
const expression = node.expression
264+
265+
if (isBaseTestIdentifier(expression)) {
266+
return true
267+
}
268+
269+
if (ts.isPropertyAccessExpression(expression)) {
270+
if (!isBaseTestIdentifier(expression.expression)) {
271+
return false
272+
}
273+
274+
return (
275+
expression.name.text !== 'skip' &&
276+
expression.name.text !== 'todo' &&
277+
expression.name.text !== 'each'
278+
)
279+
}
280+
281+
if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
282+
const innerExpression = expression.expression
283+
284+
return isBaseTestIdentifier(innerExpression.expression) && innerExpression.name.text === 'each'
285+
}
286+
287+
return false
288+
}
289+
290+
function getStaticTestCount(suiteName: string): number {
291+
const testPath = path.join(dirname, suiteName, 'int.spec.ts')
292+
const sourceText = fs.readFileSync(testPath, 'utf8')
293+
const sourceFile = ts.createSourceFile(
294+
testPath,
295+
sourceText,
296+
ts.ScriptTarget.Latest,
297+
true,
298+
ts.ScriptKind.TS,
299+
)
300+
let count = 0
301+
302+
function visit(node: ts.Node) {
303+
if (ts.isCallExpression(node) && isRunnableTestCall(node)) {
304+
count++
305+
}
306+
307+
ts.forEachChild(node, visit)
308+
}
309+
310+
visit(sourceFile)
311+
312+
return count
313+
}
314+
315+
function getExplicitSkippedTestCount(suiteName: string): number {
316+
const testPath = path.join(dirname, suiteName, 'int.spec.ts')
317+
const sourceText = fs.readFileSync(testPath, 'utf8')
318+
const sourceFile = ts.createSourceFile(
319+
testPath,
320+
sourceText,
321+
ts.ScriptTarget.Latest,
322+
true,
323+
ts.ScriptKind.TS,
324+
)
325+
let count = 0
326+
327+
function visit(node: ts.Node) {
328+
if (
329+
ts.isCallExpression(node) &&
330+
ts.isIdentifier(node.expression) &&
331+
node.expression.text === 'mongoIt' &&
332+
process.env.PAYLOAD_DATABASE !== 'mongodb'
333+
) {
334+
count++
335+
}
336+
337+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
338+
const { expression, name } = node.expression
339+
if (
340+
ts.isIdentifier(expression) &&
341+
(expression.text === 'it' || expression.text === 'test') &&
342+
name.text === 'skip'
343+
) {
344+
count++
345+
}
346+
}
347+
348+
ts.forEachChild(node, visit)
349+
}
350+
351+
visit(sourceFile)
352+
return count
353+
}
354+
163355
function runTestSuite(suiteName: string): SuiteResult {
164356
const startTime = Date.now()
165357
const result: SuiteResult = {
@@ -172,18 +364,27 @@ function runTestSuite(suiteName: string): SuiteResult {
172364

173365
try {
174366
const testPath = path.join(dirname, suiteName, 'int.spec.ts')
175-
const command = `cross-env NODE_OPTIONS="--no-deprecation --no-experimental-strip-types" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true vitest run --project int ${testPath} --reporter=json`
367+
const command = `${vitestBinary} run --project int ${testPath} --reporter=json`
176368

177369
const output = execSync(command, {
178370
cwd: path.join(dirname, '..'),
179371
encoding: 'utf8',
372+
env: getVitestEnv(),
180373
stdio: ['pipe', 'pipe', 'pipe'],
374+
...(isContentAPIMode ? { timeout: contentAPISuiteTimeout } : {}),
181375
})
182376

183377
// Parse Jest output to extract test counts
184378
const parsed = parseTestResults(output)
185379
result.passed = parsed.passed
186380
result.total = parsed.total
381+
382+
if (result.total === 0) {
383+
result.total = getCollectedTestCount(suiteName)
384+
}
385+
if (result.total === 0) {
386+
result.total = getStaticTestCount(suiteName)
387+
}
187388
} catch (error: unknown) {
188389
// Try to parse failure output from both stdout and stderr
189390
let errorOutput = ''
@@ -210,11 +411,16 @@ function runTestSuite(suiteName: string): SuiteResult {
210411
result.passed = parsed.passed
211412
result.total = parsed.total
212413

213-
// Only mark as failed if tests actually failed (not all passed)
214-
// Some tests may exit with error code even if all tests pass
215-
result.failed = result.passed < result.total
414+
if (result.total === 0) {
415+
result.total = getCollectedTestCount(suiteName)
416+
}
417+
if (result.total === 0) {
418+
result.total = getStaticTestCount(suiteName)
419+
}
216420
}
217421

422+
result.total = Math.max(0, result.total - getExplicitSkippedTestCount(suiteName))
423+
result.failed = result.passed < result.total
218424
result.duration = Date.now() - startTime
219425
return result
220426
}

0 commit comments

Comments
 (0)