| 79 | let testMatch = JEST_TESTS; |
| 80 | |
| 81 | function getTestsForGroup( |
| 82 | nodeIndex: number, |
| 83 | nodeTotal: number, |
| 84 | allTests: ReadonlyArray<string>, |
| 85 | testStats: Record<string, number> |
| 86 | ): string[] { |
| 87 | if (allTests.length === 0) { |
| 88 | return []; |
| 89 | } |
| 90 | |
| 91 | const speculatedSuiteDuration = Object.values(testStats).reduce((a, b) => a + b, 0); |
| 92 | const targetDuration = speculatedSuiteDuration / nodeTotal; |
| 93 | |
| 94 | if (speculatedSuiteDuration <= 0) { |
| 95 | throw new Error('Speculated suite duration is <= 0'); |
| 96 | } |
| 97 | |
| 98 | // We are going to take all of our tests and split them into groups. |
| 99 | // If we have a test without a known duration, we will default it to 2 second |
| 100 | // This is to ensure that we still assign some weight to the tests and still attempt to somewhat balance them. |
| 101 | // The 1.5s default is selected as a p50 value of all of our JS tests in CI (as of 2022-10-26) taken from our sentry performance monitoring. |
| 102 | const tests = new Map<string, number>(); |
| 103 | const SUITE_P50_DURATION_MS = 1500; |
| 104 | |
| 105 | const allTestsSet = new Set(allTests); |
| 106 | |
| 107 | // First, iterate over all of the tests we have stats for. |
| 108 | Object.entries(testStats).forEach(([test, duration]) => { |
| 109 | if (!allTestsSet.has(test)) { |
| 110 | return; |
| 111 | } |
| 112 | if (duration <= 0) { |
| 113 | throw new Error(`Test duration is <= 0 for ${test}`); |
| 114 | } |
| 115 | tests.set(test, duration); |
| 116 | }); |
| 117 | // Then, iterate over all of the remaining tests and assign them a default duration. |
| 118 | for (const test of allTests) { |
| 119 | if (tests.has(test)) { |
| 120 | continue; |
| 121 | } |
| 122 | tests.set(test, SUITE_P50_DURATION_MS); |
| 123 | } |
| 124 | |
| 125 | // Sanity check to ensure that we have all of our tests accounted for, we need to fail |
| 126 | // if this is not the case as we risk not executing some tests and passing the build. |
| 127 | if (tests.size < allTests.length) { |
| 128 | throw new Error( |
| 129 | `All tests should be accounted for, missing ${allTests.length - tests.size}` |
| 130 | ); |
| 131 | } |
| 132 | |
| 133 | const groups: string[][] = []; |
| 134 | |
| 135 | // We sort files by path so that we try and improve the transformer cache hit rate. |
| 136 | // Colocated domain specific files are likely to require other domain specific files. |
| 137 | const testsSortedByPath = Array.from(tests.entries()).sort((a, b) => { |
| 138 | return a[0].localeCompare(b[0]); |