-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8150 from spalger/test/shardTestsToEaseMemoryPres…
…sure add test sharding
- Loading branch information
Showing
11 changed files
with
313 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/ui/public/test_harness/test_sharding/find_test_bundle_url.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/** | ||
* We don't have a lot of options for passing arguments to the page that karma | ||
* creates, so we tack some query string params onto the test bundle script url. | ||
* | ||
* This function finds that url by looking for a script tag that has | ||
* the "/tests.bundle.js" segment | ||
* | ||
* @return {string} url | ||
*/ | ||
export function findTestBundleUrl() { | ||
const scriptTags = document.querySelectorAll('script[src]'); | ||
const scriptUrls = [].map.call(scriptTags, el => el.getAttribute('src')); | ||
const testBundleUrl = scriptUrls.find(url => url.includes('/tests.bundle.js')); | ||
|
||
if (!testBundleUrl) { | ||
throw new Error('test bundle url couldn\'t be found'); | ||
} | ||
|
||
return testBundleUrl; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import murmurHash3 from 'murmurhash3js'; | ||
|
||
// murmur hashes are 32bit unsigned integers | ||
const MAX_HASH = Math.pow(2, 32); | ||
|
||
/** | ||
* Determine the shard number for a suite by hashing | ||
* its name and placing it based on the hash | ||
* | ||
* @param {number} shardTotal - the total number of shards | ||
* @param {string} suiteName - the suite name to hash | ||
* @return {number} shardNum - 1-based shard number | ||
*/ | ||
export function getShardNum(shardTotal, suiteName) { | ||
const hashIntsPerShard = MAX_HASH / shardTotal; | ||
|
||
const hashInt = murmurHash3.x86.hash32(suiteName); | ||
|
||
// murmur3 produces 32bit integers, so we devide it by the number of chunks | ||
// to determine which chunk the suite should fall in. +1 because the current | ||
// chunk is 1-based | ||
const shardNum = Math.floor(hashInt / hashIntsPerShard) + 1; | ||
|
||
// It's not clear if hash32 can produce the MAX_HASH or not, | ||
// but this just ensures that shard numbers don't go out of bounds | ||
// and cause tests to be ignored unnecessarily | ||
return Math.max(1, Math.min(shardNum, shardTotal)); | ||
} |
26 changes: 26 additions & 0 deletions
26
src/ui/public/test_harness/test_sharding/get_sharding_params_from_url.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { parse as parseUrl } from 'url'; | ||
|
||
/** | ||
* This function extracts the relevant "shards" and "shard_num" | ||
* params from the url. | ||
* | ||
* @param {string} testBundleUrl | ||
* @return {object} params | ||
* @property {number} params.shards - the total number of shards | ||
* @property {number} params.shard_num - the current shard number, 1 based | ||
*/ | ||
export function getShardingParamsFromUrl(url) { | ||
const parsedUrl = parseUrl(url, true); | ||
const parsedQuery = parsedUrl.query || {}; | ||
|
||
const params = {}; | ||
if (parsedQuery.shards) { | ||
params.shards = parseInt(parsedQuery.shards, 10); | ||
} | ||
|
||
if (parsedQuery.shard_num) { | ||
params.shard_num = parseInt(parsedQuery.shard_num, 10); | ||
} | ||
|
||
return params; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { setupTestSharding } from './setup_test_sharding'; |
48 changes: 48 additions & 0 deletions
48
src/ui/public/test_harness/test_sharding/setup_test_sharding.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { uniq, defaults } from 'lodash'; | ||
|
||
import { findTestBundleUrl } from './find_test_bundle_url'; | ||
import { getShardingParamsFromUrl } from './get_sharding_params_from_url'; | ||
import { setupTopLevelDescribeFilter } from './setup_top_level_describe_filter'; | ||
import { getShardNum } from './get_shard_num'; | ||
|
||
const DEFAULT_PARAMS = { | ||
shards: 1, | ||
shard_num: 1 | ||
}; | ||
|
||
export function setupTestSharding() { | ||
const pageUrl = window.location.href; | ||
const bundleUrl = findTestBundleUrl(); | ||
|
||
// supports overriding params via the debug page | ||
// url in dev mode | ||
const params = defaults( | ||
{}, | ||
getShardingParamsFromUrl(pageUrl), | ||
getShardingParamsFromUrl(bundleUrl), | ||
DEFAULT_PARAMS | ||
); | ||
|
||
const { shards: shardTotal, shard_num: shardNum } = params; | ||
if (shardNum < 1 || shardNum > shardTotal) { | ||
throw new TypeError(`shard_num param of ${shardNum} must be greater 0 and less than the total, ${shardTotal}`); | ||
} | ||
|
||
// track and log the number of ignored describe calls | ||
const ignoredDescribeShards = []; | ||
before(() => { | ||
const ignoredCount = ignoredDescribeShards.length; | ||
const ignoredFrom = uniq(ignoredDescribeShards).join(', '); | ||
console.log(`Ignored ${ignoredCount} top-level suites from ${ignoredFrom}`); | ||
}); | ||
|
||
// Filter top-level describe statements as they come | ||
setupTopLevelDescribeFilter(describeName => { | ||
const describeShardNum = getShardNum(shardTotal, describeName); | ||
if (describeShardNum === shardNum) return true; | ||
// track shard numbers that we ignore | ||
ignoredDescribeShards.push(describeShardNum); | ||
}); | ||
|
||
console.log(`ready to load tests for shard ${shardNum} of ${shardTotal}`); | ||
} |
97 changes: 97 additions & 0 deletions
97
src/ui/public/test_harness/test_sharding/setup_top_level_describe_filter.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/** | ||
* Intercept all calls to mocha.describe() and determine | ||
* which calls make it through using a filter function. | ||
* | ||
* The filter function is also only called for top-level | ||
* describe() calls; all describe calls nested within another | ||
* are allowed based on the filter value for the parent | ||
* describe | ||
* | ||
* ## example | ||
* | ||
* assume tests that look like this: | ||
* | ||
* ```js | ||
* describe('section 1', () => { | ||
* describe('item 1', () => { | ||
* | ||
* }) | ||
* }) | ||
* ``` | ||
* | ||
* If the filter function returned true for "section 1" then "item 1" | ||
* would automatically be defined. If it returned false for "section 1" | ||
* then "section 1" would be ignored and "item 1" would never be defined | ||
* | ||
* @param {function} test - a function that takes the first argument | ||
* passed to describe, the sections name, and | ||
* returns true if the describe call should | ||
* be delegated to mocha, any other value causes | ||
* the describe call to be ignored | ||
* @return {undefined} | ||
*/ | ||
export function setupTopLevelDescribeFilter(test) { | ||
const originalDescribe = window.describe; | ||
|
||
if (!originalDescribe) { | ||
throw new TypeError('window.describe must be defined by mocha before test sharding can be setup'); | ||
} | ||
|
||
/** | ||
* When describe is called it is likely to make additional, nested, | ||
* calls to describe. We track how deeply nested we are at any time | ||
* with a depth counter, `describeCallDepth`. | ||
* | ||
* Before delegating a describe call to mocha we increment | ||
* that counter, and once mocha is done we decrement it. | ||
* | ||
* This way, we can check if `describeCallDepth > 0` at any time | ||
* to know if we are already within a describe call. | ||
* | ||
* ```js | ||
* // +1 | ||
* describe('section 1', () => { | ||
* // describeCallDepth = 1 | ||
* // +1 | ||
* describe('item 1', () => { | ||
* // describeCallDepth = 2 | ||
* }) | ||
* // -1 | ||
* }) | ||
* // -1 | ||
* // describeCallDepth = 0 | ||
* ``` | ||
* | ||
* @type {Number} | ||
*/ | ||
let describeCallDepth = 0; | ||
const ignoredCalls = []; | ||
|
||
// ensure that window.describe isn't messed with by other code | ||
Object.defineProperty(window, 'describe', { | ||
configurable: false, | ||
enumerable: true, | ||
value: function describeInterceptor(describeName, describeBody) { | ||
const context = this; | ||
|
||
const isTopLevelCall = describeCallDepth === 0; | ||
const shouldIgnore = isTopLevelCall && Boolean(test(describeName)) === false; | ||
if (shouldIgnore) return; | ||
|
||
/** | ||
* we wrap the delegation to mocha in a try/finally block | ||
* to ensure that our describeCallDepth counter stays up | ||
* to date even if the call throws an error. | ||
* | ||
* note that try/finally won't actually catch the error, it | ||
* will continue to propogate up the call stack | ||
*/ | ||
try { | ||
describeCallDepth += 1; | ||
originalDescribe.call(context, describeName, describeBody); | ||
} finally { | ||
describeCallDepth -= 1; | ||
} | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters