From 39b1582987f4b82d6da4775414f208a8433ec794 Mon Sep 17 00:00:00 2001 From: johnjbarton Date: Thu, 9 Jan 2020 17:02:01 -0800 Subject: [PATCH] feat(adapter): support spec=name URL and sharding (#243) Refactor spec selection to make multiple alternative selection mechanisms easier to implement. Print a debug URL on failures and focus on that single test if the URL is pasted. Add low-level sharding support. --- README.md | 12 +++- src/adapter.js | 164 +++++++++++++++++++++++++++++++++++++------ src/adapter.wrapper | 2 - test/adapter.spec.js | 143 ++++++++++++++++++++++--------------- 4 files changed, 242 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 1b74d47..769b004 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ ## Installation - ```bash $ npm install karma-jasmine --save-dev $ npm install jasmine-core --save-dev @@ -72,6 +71,17 @@ module.exports = function(config) { } ``` +## Debug by URL + +Failing tests print a debug URL with `?spec=`. Use it with `--no_single_run` +and paste it into your browser to focus on a single failing test. + +## Sharding + +By setting `config.client.shardIndex` and `config.client.totalShards`, you can +run a subset of the full set of specs. Complete sharding support needs to be +done in the process that calls karma, and would need to support test result +integration across shards. ---- For more information on Karma see the [homepage]. diff --git a/src/adapter.js b/src/adapter.js index cf49d87..4485355 100644 --- a/src/adapter.js +++ b/src/adapter.js @@ -101,6 +101,11 @@ function formatFailedStep (step) { return relevantStackFrames.join('\n') } +function debugUrl (description) { + // A link to re-run just one failed test case. + return window.location.origin + '/debug.html?spec=' + encodeURIComponent(description) +} + function SuiteNode (name, parent) { this.name = name this.parent = parent @@ -215,7 +220,6 @@ function KarmaReporter (tc, jasmineEnv) { this.specDone = function (specResult) { var skipped = specResult.status === 'disabled' || specResult.status === 'pending' || specResult.status === 'excluded' - var result = { fullName: specResult.fullName, description: specResult.description, @@ -242,6 +246,9 @@ function KarmaReporter (tc, jasmineEnv) { for (var i = 0, l = steps.length; i < l; i++) { result.log.push(formatFailedStep(steps[i])) } + + // Report the name of fhe failing spec so the reporter can emit a debug url. + result.debug_url = debugUrl(specResult.fullName) } // When failSpecWithNoExpectations is true, Jasmine will report specs without expectations as failed @@ -300,15 +307,133 @@ var createRegExp = function (filter) { return new RegExp(patternExpression, patternSwitches) } +function getGrepSpecsToRun (clientConfig, specs) { + var grepOption = getGrepOption(clientConfig.args) + if (grepOption) { + var regExp = createRegExp(grepOption) + return filter(specs, function specFilter (spec) { + return regExp.test(spec.getFullName()) + }) + } +} + +function parseQueryParams (location) { + var params = {} + if (location && Object.prototype.hasOwnProperty.call(location, 'search')) { + var pairs = location.search.substr(1).split('&') + for (var i = 0; i < pairs.length; i++) { + var keyValue = pairs[i].split('=') + params[decodeURIComponent(keyValue[0])] = + decodeURIComponent(keyValue[1]) + } + } + return params +} + +function getId (s) { + return s.id +} + +function getSpecsByName (specs, name) { + specs = specs.filter(function (s) { + return s.name === name + }) + if (specs.length === 0) { + throw new Error('No spec found with name: "' + name + '"') + } + return specs +} + +function getDebugSpecToRun (location, specs) { + var queryParams = parseQueryParams(location) + var spec = queryParams.spec + if (spec) { + // A single spec has been requested by name for debugging. + return getSpecsByName(specs, spec) + } +} + +function getSpecsToRunForCurrentShard (specs, shardIndex, totalShards) { + if (specs.length < totalShards) { + throw new Error( + 'More shards (' + totalShards + ') than test specs (' + specs.length + + ')') + } + + // Just do a simple sharding strategy of dividing the number of specs + // equally. + var firstSpec = Math.floor(specs.length * shardIndex / totalShards) + var lastSpec = Math.floor(specs.length * (shardIndex + 1) / totalShards) + return specs.slice(firstSpec, lastSpec) +} + +function getShardedSpecsToRun (specs, clientConfig) { + var shardIndex = clientConfig.shardIndex + var totalShards = clientConfig.totalShards + if (shardIndex != null && totalShards != null) { + // Sharded mode - Run only the subset of the specs corresponding to the + // current shard. + return getSpecsToRunForCurrentShard( + specs, Number(shardIndex), Number(totalShards)) + } +} + /** * Create jasmine spec filter - * @param {Object} options Spec filter options + * @param {Object} clientConfig karma config + * @param {!Object} jasmineEnv */ -var KarmaSpecFilter = function (options) { - var filterPattern = createRegExp(options && options.filterString()) +var KarmaSpecFilter = function (clientConfig, jasmineEnv) { + /** + * Walk the test suite tree depth first and collect all test specs + * @param {!Object} jasmineEnv + * @return {!Array} All possible tests. + */ + function getAllSpecs (jasmineEnv) { + var specs = [] + var stack = [jasmineEnv.topSuite()] + var currentNode + while ((currentNode = stack.pop())) { + if (currentNode.children) { + // jasmine.Suite + stack = stack.concat(currentNode.children) + } else if (currentNode.id) { + // jasmine.Spec + specs.unshift(currentNode) + } + } + + return specs + } + + /** + * Filter the specs with URL search params and config. + * @param {!Object} location property 'search' from URL. + * @param {!Object} clientConfig karma client config + * @param {!Object} jasmineEnv + * @return {!Array} + */ + function getSpecsToRun (location, clientConfig, jasmineEnv) { + var specs = getAllSpecs(jasmineEnv).map(function (spec) { + spec.name = spec.getFullName() + return spec + }) + + if (!specs || !specs.length) { + return [] + } + + return getGrepSpecsToRun(clientConfig, specs) || + getDebugSpecToRun(location, specs) || + getShardedSpecsToRun(specs, clientConfig) || + specs + } + + this.specIdsToRun = + getSpecsToRun(window.location, clientConfig, jasmineEnv).map(getId) - this.matches = function (specName) { - return filterPattern.test(specName) + this.matches = function (spec) { + return this.specIdsToRun.indexOf(spec.id) !== -1 } } @@ -322,17 +447,13 @@ var KarmaSpecFilter = function (options) { * @param {Object} jasmineEnv jasmine environment object */ var createSpecFilter = function (config, jasmineEnv) { - var karmaSpecFilter = new KarmaSpecFilter({ - filterString: function () { - return getGrepOption(config.args) - } - }) + var karmaSpecFilter = new KarmaSpecFilter(config, jasmineEnv) var specFilter = function (spec) { - return karmaSpecFilter.matches(spec.getFullName()) + return karmaSpecFilter.matches(spec) } - jasmineEnv.configure({ specFilter: specFilter }) + return specFilter } /** @@ -346,18 +467,19 @@ var createSpecFilter = function (config, jasmineEnv) { * @return {Function} Karma starter function. */ function createStartFn (karma, jasmineEnv) { - var clientConfig = karma.config || {} - var jasmineConfig = clientConfig.jasmine || {} + // This function will be assigned to `window.__karma__.start`: + return function () { + var clientConfig = karma.config || {} + var jasmineConfig = clientConfig.jasmine || {} - jasmineEnv = jasmineEnv || window.jasmine.getEnv() + jasmineEnv = jasmineEnv || window.jasmine.getEnv() - jasmineEnv.configure(jasmineConfig) + jasmineConfig.specFilter = createSpecFilter(clientConfig, jasmineEnv) - window.jasmine.DEFAULT_TIMEOUT_INTERVAL = jasmineConfig.timeoutInterval || - window.jasmine.DEFAULT_TIMEOUT_INTERVAL + jasmineEnv.configure(jasmineConfig) - // This function will be assigned to `window.__karma__.start`: - return function () { + window.jasmine.DEFAULT_TIMEOUT_INTERVAL = jasmineConfig.timeoutInterval || + window.jasmine.DEFAULT_TIMEOUT_INTERVAL jasmineEnv.addReporter(new KarmaReporter(karma, jasmineEnv)) jasmineEnv.execute() } diff --git a/src/adapter.wrapper b/src/adapter.wrapper index d444af5..b367723 100644 --- a/src/adapter.wrapper +++ b/src/adapter.wrapper @@ -2,8 +2,6 @@ %CONTENT% - -createSpecFilter(window.__karma__.config, jasmine.getEnv()) window.__karma__.start = createStartFn(window.__karma__) })(typeof window !== 'undefined' ? window : global); diff --git a/test/adapter.spec.js b/test/adapter.spec.js index fc66b76..1bc48dd 100644 --- a/test/adapter.spec.js +++ b/test/adapter.spec.js @@ -3,7 +3,8 @@ These tests are executed in browser. */ /* global getJasmineRequireObj, jasmineRequire, MockSocket, KarmaReporter */ -/* global formatFailedStep, , createStartFn, getGrepOption, createRegExp, KarmaSpecFilter, createSpecFilter */ +/* global formatFailedStep, , createStartFn, getGrepOption, createRegExp, createSpecFilter */ +/* global getGrepSpecsToRun, getDebugSpecToRun, getShardedSpecsToRun */ /* global getRelevantStackFrom: true, isExternalStackEntry: true */ 'use strict' @@ -536,74 +537,106 @@ describe('jasmine adapter', function () { }) }) - describe('KarmaSpecFilter(RegExp)', function () { - var specFilter - - beforeEach(function () { - specFilter = new KarmaSpecFilter({ - filterString: function () { - return '/test.*/' + describe('with mock jasmine specs, ', () => { + var mockSpecBar + var mockSpecTest + var specs + var mockJasmineEnv + + beforeEach(() => { + mockSpecBar = { + getFullName: () => 'bar', + name: 'bar', + id: 2 + } + mockSpecTest = { + getFullName: () => 'test', + name: 'test', + id: 1 + } + mockJasmineEnv = { + topSuite: () => { + return { + children: [mockSpecTest, mockSpecBar] + } } - }) - }) - - it('should create spec filter', function () { - expect(specFilter).toBeDefined() - }) - - it('should filter spec by name', function () { - expect(specFilter.matches('bar')).toEqual(false) - expect(specFilter.matches('test')).toEqual(true) + } + specs = mockJasmineEnv.topSuite().children }) - }) - describe('KarmaSpecFilter(non-RegExp)', function () { - var specFilter - - beforeEach(function () { - specFilter = new KarmaSpecFilter({ - filterString: function () { - return 'test' + describe(' getGrepSpecsToRun', function () { + it('should not match without grep arg', function () { + var karmaConfMock = { + args: [] } + var actualSpecs = getGrepSpecsToRun(karmaConfMock, specs) + expect(actualSpecs).not.toBeDefined() }) - }) - it('should create spec filter', function () { - expect(specFilter).toBeDefined() - }) + it('should filter spec by grep arg', function () { + var karmaConfMock = { + args: ['--grep', 'test'] + } - it('should filter spec by name', function () { - expect(specFilter.matches('bar')).toEqual(false) - expect(specFilter.matches('test')).toEqual(true) + var actualSpecs = getGrepSpecsToRun(karmaConfMock, specs) + expect(actualSpecs).toEqual([mockSpecTest]) + }) }) - }) - - describe('createSpecFilter', function () { - var jasmineEnv - beforeEach(function () { - jasmineEnv = new jasmine.Env() + describe('getDebugSpecToRun', function () { + it('should not match with no params', function () { + var location = {} + var actualSpecs = getDebugSpecToRun(location, specs) + expect(actualSpecs).not.toBeDefined() + }) + it('should match with param', function () { + var location = { search: '?spec=test' } + var actualSpecs = getDebugSpecToRun(location, specs) + expect(actualSpecs).toEqual([mockSpecTest]) + }) + it('should throw with param spec not found', function () { + var location = { search: '?spec=oops' } + expect(function () { + getDebugSpecToRun(location, specs) + }).toThrowError('No spec found with name: "oops"') + }) }) - it('should create spec filter in jasmine', function () { - var karmaConfMock = { - args: ['--grep', 'test'] - } - - createSpecFilter(karmaConfMock, jasmineEnv) - - var specFilter = jasmineEnv.configuration().specFilter + describe('getShard', function () { + it('should not match without shard data', function () { + var clientConfig = {} + var actualSpecs = getShardedSpecsToRun(specs, clientConfig) + expect(actualSpecs).not.toBeDefined() + }) + it('should match shard data', function () { + var clientConfig = { + shardIndex: 0, + totalShards: 2 + } + var actualSpecs = getShardedSpecsToRun(specs, clientConfig) + expect(actualSpecs).toEqual([mockSpecTest]) + clientConfig = { + shardIndex: 1, + totalShards: 2 + } + actualSpecs = getShardedSpecsToRun(specs, clientConfig) + expect(actualSpecs).toEqual([mockSpecBar]) + }) + }) - // Jasmine's default specFilter **always** returns true - // so test multiple possibilities + describe('createSpecFilter', function () { + it('should create spec filter in jasmine', function () { + var karmaConfMock = { + args: ['--grep', 'test'] + } + var specFilter = createSpecFilter(karmaConfMock, mockJasmineEnv) - expect(specFilter({ - getFullName: jasmine.createSpy('getFullName').and.returnValue('test') - })).toEqual(true) + // Jasmine's default specFilter **always** returns true + // so test multiple possibilities - expect(specFilter({ - getFullName: jasmine.createSpy('getFullName2').and.returnValue('foo') - })).toEqual(false) + expect(specFilter(mockSpecTest)).toEqual(true) + expect(specFilter(mockSpecBar)).toEqual(false) + }) }) }) })