From 5d5d63509cf2d5557b109b74c29414698e628b62 Mon Sep 17 00:00:00 2001 From: uid11 Date: Thu, 21 May 2026 17:04:40 +0300 Subject: [PATCH] PRO-20047 fix: show global error on duplicate test file paths --- src/README.md | 33 +++++------ src/constants/internal.ts | 2 + src/constants/paths.ts | 20 +++++++ src/types/internal.ts | 2 +- src/types/testRun.ts | 9 +++ .../getIsSuccessfulTestRun.ts | 12 ++++ .../getIsTestFilePathUniq.ts | 34 +++++++++++ .../completedTestRuns/getIsTestNameUniq.ts | 34 +++++++++++ .../getIsTestOptionsDifferent.ts | 18 ++++++ .../getSuccessfulTestFilePaths.ts | 23 ++++++++ src/utils/completedTestRuns/index.ts | 8 +++ src/utils/events/collectFullEventsData.ts | 10 +++- .../logEndTestRunEvent.ts | 11 +--- src/utils/events/registerEndTestRunEvent.ts | 6 +- src/utils/fs/index.ts | 8 +++ src/utils/fs/readCompletedTestRuns.ts | 18 ++++++ src/utils/fs/readGlobalErrors.ts | 2 +- src/utils/fs/readGlobalWarnings.ts | 2 +- src/utils/fs/readNotIncludedInPackTests.ts | 25 +++++++++ src/utils/fs/writeCompletedTestRun.ts | 15 +++++ src/utils/fs/writeNotIncludedInPackTest.ts | 25 +++++++++ src/utils/generalLog/index.ts | 4 +- src/utils/generalLog/successfulTestRuns.ts | 51 ----------------- src/utils/notIncludedInPackTests.ts | 56 ------------------- ...ssertThatTestNamesAndFilePathsAreUnique.ts | 6 +- ...estNamesAndFilePathsAreUniqueInOneRetry.ts | 29 ++++++---- src/utils/report/collectReportData.ts | 6 +- src/utils/test/getShouldRunTest.ts | 39 +++++++++++-- .../getUnsuccessfulTestFilePaths.ts | 2 +- 29 files changed, 345 insertions(+), 165 deletions(-) create mode 100644 src/utils/completedTestRuns/getIsSuccessfulTestRun.ts create mode 100644 src/utils/completedTestRuns/getIsTestFilePathUniq.ts create mode 100644 src/utils/completedTestRuns/getIsTestNameUniq.ts create mode 100644 src/utils/completedTestRuns/getIsTestOptionsDifferent.ts create mode 100644 src/utils/completedTestRuns/getSuccessfulTestFilePaths.ts create mode 100644 src/utils/completedTestRuns/index.ts rename src/utils/{generalLog => events}/logEndTestRunEvent.ts (68%) create mode 100644 src/utils/fs/readCompletedTestRuns.ts create mode 100644 src/utils/fs/readNotIncludedInPackTests.ts create mode 100644 src/utils/fs/writeCompletedTestRun.ts create mode 100644 src/utils/fs/writeNotIncludedInPackTest.ts delete mode 100644 src/utils/generalLog/successfulTestRuns.ts delete mode 100644 src/utils/notIncludedInPackTests.ts diff --git a/src/README.md b/src/README.md index 9a2826ff..90516110 100644 --- a/src/README.md +++ b/src/README.md @@ -40,19 +40,20 @@ Modules in the dependency graph should only import the modules above them: 33. `utils/promise` 34. `utils/resourceUsage` 35. `utils/fs` -36. `utils/getGlobalErrorHandler` -37. `utils/tests` -38. `utils/end` -39. `utils/pack` -40. `useContext` -41. `context` -42. `utils/step` -43. `utils/apiStatistics` -44. `utils/selectors` -45. `selectors` -46. `utils/log` -47. `step` -48. `utils/waitForEvents` -49. `utils/expect` -50. `expect` -51. ... +36. `utils/completedTestRuns` +37. `utils/getGlobalErrorHandler` +38. `utils/tests` +39. `utils/end` +40. `utils/pack` +41. `useContext` +42. `context` +43. `utils/step` +44. `utils/apiStatistics` +45. `utils/selectors` +46. `selectors` +47. `utils/log` +48. `step` +49. `utils/waitForEvents` +50. `utils/expect` +51. `expect` +52. ... diff --git a/src/constants/internal.ts b/src/constants/internal.ts index 3052807a..40205199 100644 --- a/src/constants/internal.ts +++ b/src/constants/internal.ts @@ -52,6 +52,7 @@ export { API_STATISTICS_PATH, AUTOTESTS_DIRECTORY_PATH, COMPILED_USERLAND_CONFIG_DIRECTORY, + COMPLETED_TEST_RUNS_PATH, CONFIG_PATH, DOT_ENV_PATH, EVENTS_DIRECTORY_PATH, @@ -61,6 +62,7 @@ export { INSTALLED_E2ED_DIRECTORY_PATH, INTERNAL_DIRECTORY_NAME, INTERNAL_REPORTS_DIRECTORY_PATH, + NOT_INCLUDED_IN_PACK_TESTS_PATH, REPORTS_DIRECTORY_PATH, SCREENSHOTS_DIRECTORY_PATH, START_INFO_PATH, diff --git a/src/constants/paths.ts b/src/constants/paths.ts index 5b20cc05..41173157 100644 --- a/src/constants/paths.ts +++ b/src/constants/paths.ts @@ -99,6 +99,15 @@ export const COMPILED_USERLAND_CONFIG_DIRECTORY = join( 'config', ) as DirectoryPathFromRoot; +/** + * Relative (from root) path to file with already completed test runs. + * @internal + */ +export const COMPLETED_TEST_RUNS_PATH = join( + TMP_DIRECTORY_PATH, + 'completedTestRuns.txt', +) as FilePathFromRoot; + /** * Relative (from root) path to `config` file, * that plays the role of the internal Playwright config. @@ -137,6 +146,17 @@ export const GLOBAL_WARNINGS_PATH = join( 'globalWarnings.txt', ) as FilePathFromRoot; +/** + * Relative (from root) path to text file with list of not included in pack tests. + * For each not included in pack test in this file, a relative path + * to the file of this test is saved in a separate line. + * @internal + */ +export const NOT_INCLUDED_IN_PACK_TESTS_PATH = join( + TMP_DIRECTORY_PATH, + 'notIncludedInPackTests.txt', +) as FilePathFromRoot; + /** * Relative (from root) path to directory with tests screenshots. * @internal diff --git a/src/types/internal.ts b/src/types/internal.ts index f8a8e1fd..505872d5 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -164,7 +164,7 @@ export type { TestStaticOptions, } from './testRun'; /** @internal */ -export type {FullTestRun, RunTest, Test, TestUnit} from './testRun'; +export type {CompletedTestRun, FullTestRun, RunTest, Test, TestUnit} from './testRun'; export type {MergeTuples, TupleRest} from './tuples'; export type { CloneWithoutUndefinedProperties, diff --git a/src/types/testRun.ts b/src/types/testRun.ts index 99c8dbae..d4c3f16b 100644 --- a/src/types/testRun.ts +++ b/src/types/testRun.ts @@ -11,6 +11,15 @@ import type {TestFilePath} from './paths'; import type {StringForLogs} from './string'; import type {TestMetaPlaceholder} from './userland'; +/** + * Completed test run object used by internal runtime mechanics. + * @internal + */ +export type CompletedTestRun = Readonly<{ + status: TestRunStatus | 'started'; +}> & + TestStaticOptions; + /** * Full test run object result of userland hooks (like mainParams and runHash). * @internal diff --git a/src/utils/completedTestRuns/getIsSuccessfulTestRun.ts b/src/utils/completedTestRuns/getIsSuccessfulTestRun.ts new file mode 100644 index 00000000..e6c88666 --- /dev/null +++ b/src/utils/completedTestRuns/getIsSuccessfulTestRun.ts @@ -0,0 +1,12 @@ +import {FAILED_TEST_RUN_STATUSES, TestRunStatus} from '../../constants/internal'; + +import type {CompletedTestRun} from '../../types/internal'; + +/** + * Returns `true`, if test run was successful, and `false` otherwise. + * @internal + */ +export const getIsSuccessfulTestRun = ({status}: CompletedTestRun): boolean => + status !== 'started' && + status !== TestRunStatus.Broken && + !FAILED_TEST_RUN_STATUSES.includes(status); diff --git a/src/utils/completedTestRuns/getIsTestFilePathUniq.ts b/src/utils/completedTestRuns/getIsTestFilePathUniq.ts new file mode 100644 index 00000000..f1b4a894 --- /dev/null +++ b/src/utils/completedTestRuns/getIsTestFilePathUniq.ts @@ -0,0 +1,34 @@ +import {writeGlobalError} from '../fs'; + +import {getIsTestOptionsDifferent} from './getIsTestOptionsDifferent'; + +import type {CompletedTestRun, TestStaticOptions} from '../../types/internal'; + +/** + * Returns `true`, if new test file path is uniq in completed test runs, + * otherwise writes global error. + * @internal + */ +export const getIsTestFilePathUniq = async ( + testStaticOptions: TestStaticOptions, + completedTestRuns: readonly CompletedTestRun[], +): Promise => { + const {filePath, name, options} = testStaticOptions; + + const testRunsWithFilePath = completedTestRuns.filter((run) => run.filePath === filePath); + + for (const completedTestRun of testRunsWithFilePath) { + if ( + name !== completedTestRun.name || + getIsTestOptionsDifferent(options, completedTestRun.options) + ) { + await writeGlobalError( + `The file "${filePath}" contains two different tests: "${completedTestRun.name}" (${JSON.stringify(completedTestRun.options)}) and "${name}" (${JSON.stringify(options)})`, + ); + + return false; + } + } + + return true; +}; diff --git a/src/utils/completedTestRuns/getIsTestNameUniq.ts b/src/utils/completedTestRuns/getIsTestNameUniq.ts new file mode 100644 index 00000000..dc3920c2 --- /dev/null +++ b/src/utils/completedTestRuns/getIsTestNameUniq.ts @@ -0,0 +1,34 @@ +import {writeGlobalError} from '../fs'; + +import {getIsTestOptionsDifferent} from './getIsTestOptionsDifferent'; + +import type {CompletedTestRun, TestStaticOptions} from '../../types/internal'; + +/** + * Returns `true`, if new test file has uniq name in completed test runs, + * otherwise writes global error. + * @internal + */ +export const getIsTestNameUniq = async ( + testStaticOptions: TestStaticOptions, + completedTestRuns: readonly CompletedTestRun[], +): Promise => { + const {filePath, name, options} = testStaticOptions; + + const testRunsWithName = completedTestRuns.filter((run) => run.name === name); + + for (const completedTestRun of testRunsWithName) { + if ( + filePath !== completedTestRun.filePath || + getIsTestOptionsDifferent(options, completedTestRun.options) + ) { + await writeGlobalError( + `There are two different tests with the same name "${name}": "${completedTestRun.filePath}" (${JSON.stringify(completedTestRun.options)}) and "${filePath}" (${JSON.stringify(options)})`, + ); + + return false; + } + } + + return true; +}; diff --git a/src/utils/completedTestRuns/getIsTestOptionsDifferent.ts b/src/utils/completedTestRuns/getIsTestOptionsDifferent.ts new file mode 100644 index 00000000..bc3e329e --- /dev/null +++ b/src/utils/completedTestRuns/getIsTestOptionsDifferent.ts @@ -0,0 +1,18 @@ +import type {TestOptions} from '../../types/internal'; + +/** + * Returns `true`, if test options different, and `false` otherwise. + * @internal + */ +export const getIsTestOptionsDifferent = ( + firstOptions: TestOptions, + secondOptions: TestOptions, +): boolean => { + const firstMeta = {...firstOptions.meta, skipReason: undefined}; + const secondMeta = {...secondOptions.meta, skipReason: undefined}; + + return ( + JSON.stringify({...firstOptions, meta: firstMeta}) !== + JSON.stringify({...secondOptions, meta: secondMeta}) + ); +}; diff --git a/src/utils/completedTestRuns/getSuccessfulTestFilePaths.ts b/src/utils/completedTestRuns/getSuccessfulTestFilePaths.ts new file mode 100644 index 00000000..02635d0d --- /dev/null +++ b/src/utils/completedTestRuns/getSuccessfulTestFilePaths.ts @@ -0,0 +1,23 @@ +import {readCompletedTestRuns} from '../fs'; + +import {getIsSuccessfulTestRun} from './getIsSuccessfulTestRun'; + +import type {TestFilePath} from '../../types/internal'; + +/** + * Get array of test file paths of successful tests. + * @internal + */ +export const getSuccessfulTestFilePaths = async (): Promise => { + const completedTestRuns = await readCompletedTestRuns(); + + const successfulTestFilePaths: TestFilePath[] = []; + + for (const completedTestRun of completedTestRuns) { + if (getIsSuccessfulTestRun(completedTestRun)) { + successfulTestFilePaths.push(completedTestRun.filePath); + } + } + + return successfulTestFilePaths; +}; diff --git a/src/utils/completedTestRuns/index.ts b/src/utils/completedTestRuns/index.ts new file mode 100644 index 00000000..1256c052 --- /dev/null +++ b/src/utils/completedTestRuns/index.ts @@ -0,0 +1,8 @@ +/** @internal */ +export {getIsSuccessfulTestRun} from './getIsSuccessfulTestRun'; +/** @internal */ +export {getIsTestFilePathUniq} from './getIsTestFilePathUniq'; +/** @internal */ +export {getIsTestNameUniq} from './getIsTestNameUniq'; +/** @internal */ +export {getSuccessfulTestFilePaths} from './getSuccessfulTestFilePaths'; diff --git a/src/utils/events/collectFullEventsData.ts b/src/utils/events/collectFullEventsData.ts index 35a0269a..5367c0bd 100644 --- a/src/utils/events/collectFullEventsData.ts +++ b/src/utils/events/collectFullEventsData.ts @@ -1,8 +1,12 @@ import {EndE2edReason} from '../../constants/internal'; import {endE2edReason as maybeEndE2edReason} from '../end'; -import {readApiStatistics, readEventsFromFiles, readStartInfo} from '../fs'; -import {getNotIncludedInPackTests} from '../notIncludedInPackTests'; +import { + readApiStatistics, + readEventsFromFiles, + readNotIncludedInPackTests, + readStartInfo, +} from '../fs'; import type {FullEventsData, UtcTimeInMs} from '../../types/internal'; @@ -15,7 +19,7 @@ export const collectFullEventsData = async (): Promise => { const endE2edReason = maybeEndE2edReason ?? EndE2edReason.Unknown; const endTimeInMs = Date.now() as UtcTimeInMs; const fullTestRuns = await readEventsFromFiles([]); - const notIncludedInPackTests = await getNotIncludedInPackTests(); + const notIncludedInPackTests = await readNotIncludedInPackTests(); const startInfo = await readStartInfo(); return { diff --git a/src/utils/generalLog/logEndTestRunEvent.ts b/src/utils/events/logEndTestRunEvent.ts similarity index 68% rename from src/utils/generalLog/logEndTestRunEvent.ts rename to src/utils/events/logEndTestRunEvent.ts index a39a390a..ce8baf47 100644 --- a/src/utils/generalLog/logEndTestRunEvent.ts +++ b/src/utils/events/logEndTestRunEvent.ts @@ -1,13 +1,10 @@ import { - FAILED_TEST_RUN_STATUSES, MESSAGE_BACKGROUND_COLOR_BY_STATUS, TEST_RUN_STATUS_SYMBOLS, - TestRunStatus, } from '../../constants/internal'; -import {generalLog} from './generalLog'; -import {getMessageWithBackgroundColor} from './getMessageWithBackgroundColor'; -import {addSuccessfulTestRun, getSuccessfulTestFilePaths} from './successfulTestRuns'; +import {getSuccessfulTestFilePaths} from '../completedTestRuns'; +import {generalLog, getMessageWithBackgroundColor} from '../generalLog'; import type {FullTestRun} from '../../types/internal'; @@ -18,10 +15,6 @@ import type {FullTestRun} from '../../types/internal'; export const logEndTestRunEvent = async (fullTestRun: FullTestRun): Promise => { const {filePath, mainParams, name, options, runError, runId, status} = fullTestRun; - if (status !== TestRunStatus.Broken && !FAILED_TEST_RUN_STATUSES.includes(status)) { - await addSuccessfulTestRun(filePath); - } - const messageBackgroundColor = MESSAGE_BACKGROUND_COLOR_BY_STATUS[status]; const messageSymbol = TEST_RUN_STATUS_SYMBOLS[status]; const messageText = `${messageSymbol} ${status} ${mainParams} ${name}`; diff --git a/src/utils/events/registerEndTestRunEvent.ts b/src/utils/events/registerEndTestRunEvent.ts index 77a74f0c..39eaecc2 100644 --- a/src/utils/events/registerEndTestRunEvent.ts +++ b/src/utils/events/registerEndTestRunEvent.ts @@ -5,14 +5,15 @@ import {getPlaywrightPage} from '../../useContext'; import {cloneWithoutLogEvents} from '../clone'; import {getRunErrorFromError} from '../error'; -import {writeApiStatistics, writeTestRunToJsonFile} from '../fs'; -import {generalLog, logEndTestRunEvent, writeLogsToFile} from '../generalLog'; +import {writeApiStatistics, writeCompletedTestRun, writeTestRunToJsonFile} from '../fs'; +import {generalLog, writeLogsToFile} from '../generalLog'; import {setReadonlyProperty} from '../object'; import {getUserlandHooks} from '../userland'; import {calculateTestRunStatus} from './calculateTestRunStatus'; import {getRegroupedSteps} from './getRegroupedSteps'; import {getTestRunEvent} from './getTestRunEvent'; +import {logEndTestRunEvent} from './logEndTestRunEvent'; import {writeFullMocksIfNeeded} from './writeFullMocksIfNeeded'; import type {EndTestRunEvent, FullTestRun, RunHash, TestRun} from '../../types/internal'; @@ -77,6 +78,7 @@ export const registerEndTestRunEvent = async (endTestRunEvent: EndTestRunEvent): const fullTestRun: FullTestRun = {mainParams, runHash, ...testRun}; + await writeCompletedTestRun({filePath, name, options, status}); await logEndTestRunEvent(fullTestRun); const apiStatistics = getApiStatistics(); diff --git a/src/utils/fs/index.ts b/src/utils/fs/index.ts index 048681f8..705a5cea 100644 --- a/src/utils/fs/index.ts +++ b/src/utils/fs/index.ts @@ -9,6 +9,8 @@ export {getLastLogEventTimeInMs, writeLogEventTime} from './logIsoString'; /** @internal */ export {readApiStatistics} from './readApiStatistics'; /** @internal */ +export {readCompletedTestRuns} from './readCompletedTestRuns'; +/** @internal */ export {readEventFromFile} from './readEventFromFile'; /** @internal */ export {readEventsFromFiles} from './readEventsFromFiles'; @@ -17,17 +19,23 @@ export {readGlobalErrors} from './readGlobalErrors'; /** @internal */ export {readGlobalWarnings} from './readGlobalWarnings'; /** @internal */ +export {readNotIncludedInPackTests} from './readNotIncludedInPackTests'; +/** @internal */ export {readStartInfo} from './readStartInfo'; /** @internal */ export {removeDirectory} from './removeDirectory'; /** @internal */ export {writeApiStatistics} from './writeApiStatistics'; +/** @internal */ +export {writeCompletedTestRun} from './writeCompletedTestRun'; export {writeFile} from './writeFile'; /** @internal */ export {writeGlobalError} from './writeGlobalError'; /** @internal */ export {writeGlobalWarning} from './writeGlobalWarning'; /** @internal */ +export {writeNotIncludedInPackTest} from './writeNotIncludedInPackTest'; +/** @internal */ export {writeStartInfo} from './writeStartInfo'; /** @internal */ export {writeTestRunToJsonFile} from './writeTestRunToJsonFile'; diff --git a/src/utils/fs/readCompletedTestRuns.ts b/src/utils/fs/readCompletedTestRuns.ts new file mode 100644 index 00000000..e9c18bfc --- /dev/null +++ b/src/utils/fs/readCompletedTestRuns.ts @@ -0,0 +1,18 @@ +import {readFile} from 'node:fs/promises'; + +import {COMPLETED_TEST_RUNS_PATH, READ_FILE_OPTIONS} from '../../constants/internal'; + +import type {CompletedTestRun} from '../../types/internal'; + +/** + * Reads array of completed test runs from temporary directory. + * @internal + */ +export const readCompletedTestRuns = async (): Promise => { + const completedTestRunsJsonString = await readFile( + COMPLETED_TEST_RUNS_PATH, + READ_FILE_OPTIONS, + ).catch(() => ''); + + return JSON.parse(`[${completedTestRunsJsonString.slice(0, -2)}]`) as CompletedTestRun[]; +}; diff --git a/src/utils/fs/readGlobalErrors.ts b/src/utils/fs/readGlobalErrors.ts index 7147c514..b9c11de0 100644 --- a/src/utils/fs/readGlobalErrors.ts +++ b/src/utils/fs/readGlobalErrors.ts @@ -3,7 +3,7 @@ import {readFile} from 'node:fs/promises'; import {GLOBAL_ERRORS_PATH, READ_FILE_OPTIONS} from '../../constants/internal'; /** - * Reads global errors of run from directory. + * Reads global errors of run from temporary directory. * @internal */ export const readGlobalErrors = async (): Promise => { diff --git a/src/utils/fs/readGlobalWarnings.ts b/src/utils/fs/readGlobalWarnings.ts index 4143b365..ca00a23a 100644 --- a/src/utils/fs/readGlobalWarnings.ts +++ b/src/utils/fs/readGlobalWarnings.ts @@ -3,7 +3,7 @@ import {readFile} from 'node:fs/promises'; import {GLOBAL_WARNINGS_PATH, READ_FILE_OPTIONS} from '../../constants/internal'; /** - * Reads global warnings of run from directory. + * Reads global warnings of run from temporary directory. * @internal */ export const readGlobalWarnings = async (): Promise => { diff --git a/src/utils/fs/readNotIncludedInPackTests.ts b/src/utils/fs/readNotIncludedInPackTests.ts new file mode 100644 index 00000000..f5e1de5f --- /dev/null +++ b/src/utils/fs/readNotIncludedInPackTests.ts @@ -0,0 +1,25 @@ +import {readFile} from 'node:fs/promises'; + +import {NOT_INCLUDED_IN_PACK_TESTS_PATH, READ_FILE_OPTIONS} from '../../constants/internal'; + +import type {TestFilePath} from '../../types/internal'; + +/** + * Reads array of not included in pack tests. + * @internal + */ +export const readNotIncludedInPackTests = async (): Promise => { + let textOfNotIncludedInPackTestsFile = ''; + + try { + textOfNotIncludedInPackTestsFile = await readFile( + NOT_INCLUDED_IN_PACK_TESTS_PATH, + READ_FILE_OPTIONS, + ); + } catch {} + + return textOfNotIncludedInPackTestsFile + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) as TestFilePath[]; +}; diff --git a/src/utils/fs/writeCompletedTestRun.ts b/src/utils/fs/writeCompletedTestRun.ts new file mode 100644 index 00000000..a27768cf --- /dev/null +++ b/src/utils/fs/writeCompletedTestRun.ts @@ -0,0 +1,15 @@ +import {appendFile} from 'node:fs/promises'; + +import {COMPLETED_TEST_RUNS_PATH} from '../../constants/internal'; + +import type {CompletedTestRun} from '../../types/internal'; + +/** + * Writes completed test run test to common file. + * @internal + */ +export const writeCompletedTestRun = async (completedTestRun: CompletedTestRun): Promise => { + const completedTestRunJsonString = JSON.stringify(completedTestRun); + + await appendFile(COMPLETED_TEST_RUNS_PATH, `${completedTestRunJsonString},\n`); +}; diff --git a/src/utils/fs/writeNotIncludedInPackTest.ts b/src/utils/fs/writeNotIncludedInPackTest.ts new file mode 100644 index 00000000..9aa97576 --- /dev/null +++ b/src/utils/fs/writeNotIncludedInPackTest.ts @@ -0,0 +1,25 @@ +import {appendFile} from 'node:fs/promises'; + +import {NOT_INCLUDED_IN_PACK_TESTS_PATH} from '../../constants/internal'; + +import {assertValueIsFalse} from '../asserts'; + +import {readNotIncludedInPackTests} from './readNotIncludedInPackTests'; + +import type {TestFilePath} from '../../types/internal'; + +/** + * Writes test to not included in pack tests. + * @internal + */ +export const writeNotIncludedInPackTest = async (testFilePath: TestFilePath): Promise => { + const notIncludedInPackTests = await readNotIncludedInPackTests(); + + assertValueIsFalse( + notIncludedInPackTests.includes(testFilePath), + 'There is no duplicate test file path in not included in pack tests', + {notIncludedInPackTests, testFilePath}, + ); + + await appendFile(NOT_INCLUDED_IN_PACK_TESTS_PATH, `${testFilePath}\n`); +}; diff --git a/src/utils/generalLog/index.ts b/src/utils/generalLog/index.ts index 05f4a2f9..5b4bb2c9 100644 --- a/src/utils/generalLog/index.ts +++ b/src/utils/generalLog/index.ts @@ -1,7 +1,7 @@ /** @internal */ export {generalLog} from './generalLog'; /** @internal */ -export {logEndTestRunEvent} from './logEndTestRunEvent'; +export {getMessageWithBackgroundColor} from './getMessageWithBackgroundColor'; /** @internal */ export {failMessage, okMessage} from './messages'; /** @internal */ @@ -9,6 +9,4 @@ export {writeLogsToFile} from './logFile'; /** @internal */ export {logStartE2edError} from './logStartE2edError'; /** @internal */ -export {getSuccessfulTestFilePaths} from './successfulTestRuns'; -/** @internal */ export {truncateArrayForLogs} from './truncateArrayForLogs'; diff --git a/src/utils/generalLog/successfulTestRuns.ts b/src/utils/generalLog/successfulTestRuns.ts deleted file mode 100644 index e714df4c..00000000 --- a/src/utils/generalLog/successfulTestRuns.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {appendFile, readFile} from 'node:fs/promises'; -import {join} from 'node:path'; - -import {READ_FILE_OPTIONS, TMP_DIRECTORY_PATH} from '../../constants/internal'; - -import {assertValueIsFalse} from '../asserts'; -import {isUiMode} from '../uiMode'; - -import type {FilePathFromRoot, TestFilePath} from '../../types/internal'; - -/** - * Relative (from root) path to text file with list of successful tests. - */ -const SUCCESSFUL_TESTS_PATH = join(TMP_DIRECTORY_PATH, 'successfulTests.txt') as FilePathFromRoot; - -/** - * Get array of test file paths of successful tests. - * @internal - */ -export const getSuccessfulTestFilePaths = async (): Promise => { - let successfulTestsFile = ''; - - try { - successfulTestsFile = await readFile(SUCCESSFUL_TESTS_PATH, READ_FILE_OPTIONS); - } catch {} - - return successfulTestsFile - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) as TestFilePath[]; -}; - -/** - * Adds one successful test run. - * @internal - */ -export const addSuccessfulTestRun = async (testFilePath: TestFilePath): Promise => { - const successfulTestFilePaths = await getSuccessfulTestFilePaths(); - - if (isUiMode && successfulTestFilePaths.includes(testFilePath)) { - return; - } - - assertValueIsFalse( - successfulTestFilePaths.includes(testFilePath), - 'There is no duplicate test file path in successful test runs', - {successfulTestFilePaths, testFilePath}, - ); - - await appendFile(SUCCESSFUL_TESTS_PATH, `${testFilePath}\n`); -}; diff --git a/src/utils/notIncludedInPackTests.ts b/src/utils/notIncludedInPackTests.ts deleted file mode 100644 index 0c60c655..00000000 --- a/src/utils/notIncludedInPackTests.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {appendFile, readFile} from 'node:fs/promises'; -import {join} from 'node:path'; - -import {READ_FILE_OPTIONS, TMP_DIRECTORY_PATH} from '../constants/internal'; - -import {assertValueIsFalse} from './asserts'; - -import type {FilePathFromRoot, TestFilePath} from '../types/internal'; - -/** - * Relative (from root) path to text file with list of not included in pack tests. - * For each not included in pack test in this file, a relative path - * to the file of this test is saved in a separate line. - */ -const NOT_INCLUDED_IN_PACK_TESTS_PATH = join( - TMP_DIRECTORY_PATH, - 'notIncludedInPackTests.txt', -) as FilePathFromRoot; - -/** - * Get array of not included in pack tests. - * @internal - */ -export const getNotIncludedInPackTests = async (): Promise => { - let textOfNotIncludedInPackTestsFile = ''; - - try { - textOfNotIncludedInPackTestsFile = await readFile( - NOT_INCLUDED_IN_PACK_TESTS_PATH, - READ_FILE_OPTIONS, - ); - } catch {} - - return textOfNotIncludedInPackTestsFile - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) as TestFilePath[]; -}; - -/** - * Adds test to not included in pack tests. - * @internal - */ -export const addTestToNotIncludedInPackTests = async ( - testFilePath: TestFilePath, -): Promise => { - const notIncludedInPackTests = await getNotIncludedInPackTests(); - - assertValueIsFalse( - notIncludedInPackTests.includes(testFilePath), - 'There is no duplicate test file path in not included in pack tests', - {notIncludedInPackTests, testFilePath}, - ); - - await appendFile(NOT_INCLUDED_IN_PACK_TESTS_PATH, `${testFilePath}\n`); -}; diff --git a/src/utils/report/assertThatTestNamesAndFilePathsAreUnique.ts b/src/utils/report/assertThatTestNamesAndFilePathsAreUnique.ts index f3b6d6d7..74365c8e 100644 --- a/src/utils/report/assertThatTestNamesAndFilePathsAreUnique.ts +++ b/src/utils/report/assertThatTestNamesAndFilePathsAreUnique.ts @@ -6,9 +6,9 @@ import type {FullTestRun} from '../../types/internal'; * Asserts that test names and file paths are unique (except of names internally retried runs). * @internal */ -export const assertThatTestNamesAndFilePathsAreUnique = ( +export const assertThatTestNamesAndFilePathsAreUnique = async ( fullTestRuns: readonly FullTestRun[], -): void => { +): Promise => { const testRunsByRetryIndex: Record = {}; for (const fullTestRun of fullTestRuns) { @@ -22,6 +22,6 @@ export const assertThatTestNamesAndFilePathsAreUnique = ( } for (const testRunsInOneRetry of Object.values(testRunsByRetryIndex)) { - assertThatTestNamesAndFilePathsAreUniqueInOneRetry(testRunsInOneRetry); + await assertThatTestNamesAndFilePathsAreUniqueInOneRetry(testRunsInOneRetry); } }; diff --git a/src/utils/report/assertThatTestNamesAndFilePathsAreUniqueInOneRetry.ts b/src/utils/report/assertThatTestNamesAndFilePathsAreUniqueInOneRetry.ts index 29128457..a1b83d54 100644 --- a/src/utils/report/assertThatTestNamesAndFilePathsAreUniqueInOneRetry.ts +++ b/src/utils/report/assertThatTestNamesAndFilePathsAreUniqueInOneRetry.ts @@ -2,6 +2,7 @@ import {TestRunStatus} from '../../constants/internal'; import {assertValueIsFalse} from '../asserts'; import {cloneWithoutLogEvents} from '../clone'; +import {writeGlobalError} from '../fs'; import type {FullTestRun, TestFilePath} from '../../types/internal'; @@ -9,9 +10,9 @@ import type {FullTestRun, TestFilePath} from '../../types/internal'; * Asserts that test names and file paths inside one retry are unique. * @internal */ -export const assertThatTestNamesAndFilePathsAreUniqueInOneRetry = ( +export const assertThatTestNamesAndFilePathsAreUniqueInOneRetry = async ( fullTestRuns: readonly FullTestRun[], -): void => { +): Promise => { const filePathsHash: Record = Object.create(null) as {}; const namesHash: Record = Object.create(null) as {}; @@ -20,14 +21,22 @@ export const assertThatTestNamesAndFilePathsAreUniqueInOneRetry = ( for (const fullTestRun of unbrokenTestRuns) { const {filePath, name} = fullTestRun; - assertValueIsFalse( - filePath in filePathsHash, - 'filePath is unique: each test should be in a separate file', - { - firstFullTestRun: cloneWithoutLogEvents(filePathsHash[filePath] as FullTestRun), - secondFullTestRun: cloneWithoutLogEvents(fullTestRun), - }, - ); + if (filePath in filePathsHash) { + const firstTestString = JSON.stringify({ + name: (filePathsHash[filePath] as FullTestRun).name, + options: filePathsHash[filePath]?.options, + }); + + const secondTestString = JSON.stringify({ + name: fullTestRun.name, + options: fullTestRun.options, + }); + + await writeGlobalError( + `The file "${filePath}" unexpectedly contains two different tests: ${firstTestString}, ${secondTestString}`, + ); + } + assertValueIsFalse(name in namesHash, 'name is unique: each test must have a unique name', { firstFullTestRun: cloneWithoutLogEvents(namesHash[name] as FullTestRun), secondFullTestRun: cloneWithoutLogEvents(fullTestRun), diff --git a/src/utils/report/collectReportData.ts b/src/utils/report/collectReportData.ts index cf3e6de8..cce0f2b1 100644 --- a/src/utils/report/collectReportData.ts +++ b/src/utils/report/collectReportData.ts @@ -26,12 +26,12 @@ export const collectReportData = async ({ }: FullEventsData): Promise => { const {liteReportFileName, logFileName, reportFileName} = getFullPackConfig(); - const {errors, warnings} = await getReportErrors(fullTestRuns, notIncludedInPackTests); - if (!isUiMode) { - assertThatTestNamesAndFilePathsAreUnique(fullTestRuns); + await assertThatTestNamesAndFilePathsAreUnique(fullTestRuns); } + const {errors, warnings} = await getReportErrors(fullTestRuns, notIncludedInPackTests); + unificateRunHashes(fullTestRuns); const apiStatistics = getTotalApiStatistics(apiStatisticsOfTests); diff --git a/src/utils/test/getShouldRunTest.ts b/src/utils/test/getShouldRunTest.ts index 6ae77e31..124a4f0b 100644 --- a/src/utils/test/getShouldRunTest.ts +++ b/src/utils/test/getShouldRunTest.ts @@ -1,5 +1,9 @@ -import {getSuccessfulTestFilePaths} from '../generalLog'; -import {addTestToNotIncludedInPackTests} from '../notIncludedInPackTests'; +import { + getIsSuccessfulTestRun, + getIsTestFilePathUniq, + getIsTestNameUniq, +} from '../completedTestRuns'; +import {readCompletedTestRuns, writeCompletedTestRun, writeNotIncludedInPackTest} from '../fs'; import {isUiMode} from '../uiMode'; import {getIsTestIncludedInPack} from './getIsTestIncludedInPack'; @@ -8,13 +12,15 @@ import type {TestStaticOptions} from '../../types/internal'; /** * Returns `true`, if test should be run, and `false` otherwise. + * Writes all running tests into the array of `CompletedTestRun`, + * and checks the uniqueness of test file path and the uniqueness of test name. * @internal */ export const getShouldRunTest = async (testStaticOptions: TestStaticOptions): Promise => { const isTestIncludedInPack = getIsTestIncludedInPack(testStaticOptions); if (!isTestIncludedInPack) { - await addTestToNotIncludedInPackTests(testStaticOptions.filePath); + await writeNotIncludedInPackTest(testStaticOptions.filePath); return false; } @@ -23,7 +29,30 @@ export const getShouldRunTest = async (testStaticOptions: TestStaticOptions): Pr return true; } - const successfulTestFilePaths = await getSuccessfulTestFilePaths(); + const completedTestRuns = await readCompletedTestRuns(); - return !successfulTestFilePaths.includes(testStaticOptions.filePath); + await writeCompletedTestRun({...testStaticOptions, status: 'started'}); + + const isTestFilePathUniq = await getIsTestFilePathUniq(testStaticOptions, completedTestRuns); + + if (isTestFilePathUniq === false) { + return false; + } + + const isTestNameUniq = await getIsTestNameUniq(testStaticOptions, completedTestRuns); + + if (isTestNameUniq === false) { + return false; + } + + for (const completedTestRun of completedTestRuns) { + if ( + completedTestRun.filePath === testStaticOptions.filePath && + getIsSuccessfulTestRun(completedTestRun) + ) { + return false; + } + } + + return true; }; diff --git a/src/utils/testFilePaths/getUnsuccessfulTestFilePaths.ts b/src/utils/testFilePaths/getUnsuccessfulTestFilePaths.ts index c550f97e..115ce5ac 100644 --- a/src/utils/testFilePaths/getUnsuccessfulTestFilePaths.ts +++ b/src/utils/testFilePaths/getUnsuccessfulTestFilePaths.ts @@ -1,5 +1,5 @@ import {assertValueIsFalse, assertValueIsTrue} from '../asserts'; -import {getSuccessfulTestFilePaths} from '../generalLog'; +import {getSuccessfulTestFilePaths} from '../completedTestRuns'; import type {TestFilePath} from '../../types/internal';