fix(test runner): do not special case test.fail (#8447)
This makes `test.fail` tests considered as passing when they actually fail: - Stop restarting the worker. - Retry when it passes instead of a fail. - Behaves similar to regular tests in a `describe.serial` suite.
This commit is contained in:
		
							parent
							
								
									e726c18788
								
							
						
					
					
						commit
						de85d8bb83
					
				|  | @ -192,10 +192,9 @@ export class Dispatcher { | |||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Only retry expected failures, not passes and only if the test failed.
 | ||||
|       for (const testId of retryCandidates) { | ||||
|         const pair = this._testById.get(testId)!; | ||||
|         if (!this._isStopped && pair.test.expectedStatus === 'passed' && pair.test.results.length < pair.test.retries + 1) { | ||||
|         if (!this._isStopped && pair.test.results.length < pair.test.retries + 1) { | ||||
|           pair.result = pair.test._appendTestResult(); | ||||
|           pair.steps = new Map(); | ||||
|           pair.stepStack = new Set(); | ||||
|  |  | |||
|  | @ -147,8 +147,8 @@ export class BaseReporter implements Reporter  { | |||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   willRetry(test: TestCase, result: TestResult): boolean { | ||||
|     return test.outcome() === 'unexpected' && result.status !== 'passed' && test.results.length <= test.retries; | ||||
|   willRetry(test: TestCase): boolean { | ||||
|     return test.outcome() === 'unexpected' && test.results.length <= test.retries; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -159,10 +159,9 @@ export function formatFailure(config: FullConfig, test: TestCase, index?: number | |||
|     const resultTokens = formatResultFailure(test, result, '    '); | ||||
|     if (!resultTokens.length) | ||||
|       continue; | ||||
|     const statusSuffix = (result.status === 'passed' && test.expectedStatus === 'failed') ? ' -- passed unexpectedly' : ''; | ||||
|     if (result.retry) { | ||||
|       tokens.push(''); | ||||
|       tokens.push(colors.gray(pad(`    Retry #${result.retry}${statusSuffix}`, '-'))); | ||||
|       tokens.push(colors.gray(pad(`    Retry #${result.retry}`, '-'))); | ||||
|     } | ||||
|     tokens.push(...resultTokens); | ||||
|     const output = ((result as any)[kOutputSymbol] || []) as TestResultOutput[]; | ||||
|  | @ -187,6 +186,10 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI | |||
|     resultTokens.push(''); | ||||
|     resultTokens.push(indent(colors.red(`Timeout of ${test.timeout}ms exceeded.`), initialIndent)); | ||||
|   } | ||||
|   if (result.status === 'passed' && test.expectedStatus === 'failed') { | ||||
|     resultTokens.push(''); | ||||
|     resultTokens.push(indent(colors.red(`Expected to fail, but passed.`), initialIndent)); | ||||
|   } | ||||
|   if (result.error !== undefined) | ||||
|     resultTokens.push(indent(formatError(result.error, test.location.file), initialIndent)); | ||||
|   return resultTokens; | ||||
|  | @ -211,8 +214,7 @@ export function formatTestTitle(config: FullConfig, test: TestCase, step?: TestS | |||
| 
 | ||||
| function formatTestHeader(config: FullConfig, test: TestCase, indent: string, index?: number): string { | ||||
|   const title = formatTestTitle(config, test); | ||||
|   const passedUnexpectedlySuffix = (test.results[0].status === 'passed' && test.expectedStatus === 'failed') ? ' -- passed unexpectedly' : ''; | ||||
|   const header = `${indent}${index ? index + ') ' : ''}${title}${passedUnexpectedlySuffix}`; | ||||
|   const header = `${indent}${index ? index + ') ' : ''}${title}`; | ||||
|   return colors.red(pad(header, '=')); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -44,7 +44,7 @@ class DotReporter extends BaseReporter { | |||
|       process.stdout.write(colors.yellow('°')); | ||||
|       return; | ||||
|     } | ||||
|     if (this.willRetry(test, result)) { | ||||
|     if (this.willRetry(test)) { | ||||
|       process.stdout.write(colors.gray('×')); | ||||
|       return; | ||||
|     } | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ class LineReporter extends BaseReporter { | |||
|     const width = process.stdout.columns! - 1; | ||||
|     const title = `[${++this._current}/${this._total}] ${formatTestTitle(this.config, test)}`.substring(0, width); | ||||
|     process.stdout.write(`\u001B[1A\u001B[2K${title}\n`); | ||||
|     if (!this.willRetry(test, result) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected')) { | ||||
|     if (!this.willRetry(test) && (test.outcome() === 'flaky' || test.outcome() === 'unexpected')) { | ||||
|       process.stdout.write(`\u001B[1A\u001B[2K`); | ||||
|       console.log(formatFailure(this.config, test, ++this._failures)); | ||||
|       console.log(); | ||||
|  |  | |||
|  | @ -365,7 +365,7 @@ export class WorkerRunner extends EventEmitter { | |||
|     if (reportEvents) | ||||
|       this.emit('testEnd', buildTestEndPayload(testId, testInfo)); | ||||
| 
 | ||||
|     const isFailure = testInfo.status === 'timedOut' || (testInfo.status === 'failed' && testInfo.expectedStatus !== 'failed'); | ||||
|     const isFailure = testInfo.status !== 'skipped' && testInfo.status !== testInfo.expectedStatus; | ||||
|     const preserveOutput = this._loader.fullConfig().preserveOutput === 'always' || | ||||
|       (this._loader.fullConfig().preserveOutput === 'failures-only' && isFailure); | ||||
|     if (!preserveOutput) | ||||
|  | @ -374,7 +374,7 @@ export class WorkerRunner extends EventEmitter { | |||
|     this._currentTest = null; | ||||
|     setCurrentTestInfo(null); | ||||
| 
 | ||||
|     if (testInfo.status !== 'passed' && testInfo.status !== 'skipped') { | ||||
|     if (isFailure) { | ||||
|       if (test._type === 'test') | ||||
|         this._failedTestId = testId; | ||||
|       else if (!this._fatalError) | ||||
|  |  | |||
|  | @ -116,7 +116,7 @@ test('should fail on unexpected pass', async ({ runInlineTest }) => { | |||
|   }); | ||||
|   expect(exitCode).toBe(1); | ||||
|   expect(failed).toBe(1); | ||||
|   expect(output).toContain('passed unexpectedly'); | ||||
|   expect(output).toContain('Expected to fail, but passed'); | ||||
| }); | ||||
| 
 | ||||
| test('should respect global timeout', async ({ runInlineTest }) => { | ||||
|  |  | |||
|  | @ -87,10 +87,10 @@ test('should fail on unexpected pass with retries', async ({ runInlineTest }) => | |||
|   }, { retries: 1 }); | ||||
|   expect(exitCode).toBe(1); | ||||
|   expect(failed).toBe(1); | ||||
|   expect(output).toContain('passed unexpectedly'); | ||||
|   expect(output).toContain('Expected to fail, but passed.'); | ||||
| }); | ||||
| 
 | ||||
| test('should not retry unexpected pass', async ({ runInlineTest }) => { | ||||
| test('should retry unexpected pass', async ({ runInlineTest }) => { | ||||
|   const { exitCode, passed, failed, output } = await runInlineTest({ | ||||
|     'unexpected-pass.spec.js': ` | ||||
|       const { test } = pwt; | ||||
|  | @ -103,7 +103,7 @@ test('should not retry unexpected pass', async ({ runInlineTest }) => { | |||
|   expect(exitCode).toBe(1); | ||||
|   expect(passed).toBe(0); | ||||
|   expect(failed).toBe(1); | ||||
|   expect(stripAscii(output).split('\n')[0]).toBe('F'); | ||||
|   expect(stripAscii(output).split('\n')[0]).toBe('××F'); | ||||
| }); | ||||
| 
 | ||||
| test('should not retry expected failure', async ({ runInlineTest }) => { | ||||
|  |  | |||
|  | @ -129,3 +129,84 @@ test('test.describe.serial.only should work', async ({ runInlineTest }) => { | |||
|     '%%test3', | ||||
|   ]); | ||||
| }); | ||||
| 
 | ||||
| test('test.describe.serial should work with test.fail', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.test.ts': ` | ||||
|       const { test } = pwt; | ||||
|       test.describe.serial('suite', () => { | ||||
|         test('zero', () => { | ||||
|           console.log('\\n%%zero'); | ||||
|         }); | ||||
| 
 | ||||
|         test('one', ({}) => { | ||||
|           console.log('\\n%%one'); | ||||
|           test.fail(); | ||||
|           expect(1).toBe(2); | ||||
|         }); | ||||
| 
 | ||||
|         test('two', ({}, testInfo) => { | ||||
|           console.log('\\n%%two'); | ||||
|           test.fail(); | ||||
|           expect(testInfo.retry).toBe(0); | ||||
|         }); | ||||
| 
 | ||||
|         test('three', () => { | ||||
|           console.log('\\n%%three'); | ||||
|         }); | ||||
|       }); | ||||
|     `,
 | ||||
|   }, { retries: 0 }); | ||||
|   expect(result.exitCode).toBe(1); | ||||
|   expect(result.passed).toBe(2); | ||||
|   expect(result.failed).toBe(1); | ||||
|   expect(result.skipped).toBe(1); | ||||
|   expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ | ||||
|     '%%zero', | ||||
|     '%%one', | ||||
|     '%%two', | ||||
|   ]); | ||||
| }); | ||||
| 
 | ||||
| test('test.describe.serial should work with test.fail and retries', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.test.ts': ` | ||||
|       const { test } = pwt; | ||||
|       test.describe.serial('suite', () => { | ||||
|         test('zero', () => { | ||||
|           console.log('\\n%%zero'); | ||||
|         }); | ||||
| 
 | ||||
|         test('one', ({}) => { | ||||
|           console.log('\\n%%one'); | ||||
|           test.fail(); | ||||
|           expect(1).toBe(2); | ||||
|         }); | ||||
| 
 | ||||
|         test('two', ({}, testInfo) => { | ||||
|           console.log('\\n%%two'); | ||||
|           test.fail(); | ||||
|           expect(testInfo.retry).toBe(0); | ||||
|         }); | ||||
| 
 | ||||
|         test('three', () => { | ||||
|           console.log('\\n%%three'); | ||||
|         }); | ||||
|       }); | ||||
|     `,
 | ||||
|   }, { retries: 1 }); | ||||
|   expect(result.exitCode).toBe(0); | ||||
|   expect(result.passed).toBe(3); | ||||
|   expect(result.flaky).toBe(1); | ||||
|   expect(result.failed).toBe(0); | ||||
|   expect(result.skipped).toBe(0); | ||||
|   expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ | ||||
|     '%%zero', | ||||
|     '%%one', | ||||
|     '%%two', | ||||
|     '%%zero', | ||||
|     '%%one', | ||||
|     '%%two', | ||||
|     '%%three', | ||||
|   ]); | ||||
| }); | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ test('should reuse worker after test.skip()', async ({ runInlineTest }) => { | |||
|   expect(result.exitCode).toBe(0); | ||||
| }); | ||||
| 
 | ||||
| test('should use new worker after test.fail()', async ({ runInlineTest }) => { | ||||
| test('should not use new worker after test.fail()', async ({ runInlineTest }) => { | ||||
|   const result = await runInlineTest({ | ||||
|     'a.test.js': ` | ||||
|       const { test } = pwt; | ||||
|  | @ -129,7 +129,7 @@ test('should use new worker after test.fail()', async ({ runInlineTest }) => { | |||
|       }); | ||||
| 
 | ||||
|       test('succeeds 2', async ({}, testInfo) => { | ||||
|         expect(testInfo.workerIndex).toBe(1); | ||||
|         expect(testInfo.workerIndex).toBe(0); | ||||
|       }); | ||||
|     `,
 | ||||
|   }); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue