diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index a356a5b8..2cc7f133 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -491,10 +491,20 @@ exports.setNodeVersion = (bsConfig, args) => { } // specs can be passed from bstack configuration file +// True when the spec list for this run comes from BROWSERSTACK_RERUN_TESTS rather than +// the user's config/CLI args. Shared by setUserSpecs and getNumberOfSpecFiles so the two +// cannot drift apart — getNumberOfSpecFiles rewrites specs only for runs setUserSpecs +// actually sourced from the re-run env. +exports.isReRunSpecsSession = () => { + return o11yHelpers.isBrowserstackInfra() + && o11yHelpers.isTestObservabilitySession() + && o11yHelpers.shouldReRunObservabilityTests(); +} + // specs can be passed via command line args as a string // command line args takes precedence over config exports.setUserSpecs = (bsConfig, args) => { - if(o11yHelpers.isBrowserstackInfra() && o11yHelpers.isTestObservabilitySession() && o11yHelpers.shouldReRunObservabilityTests()) { + if(this.isReRunSpecsSession()) { // BROWSERSTACK_RERUN_TESTS arrives comma+space separated (e.g. "a.ts, b.ts"); normalise // like the other spec sources below, else sanitizeSpecsPattern builds "{a.ts, b.ts}" whose // space-prefixed brace alternatives never match and the failed-spec filter collapses. @@ -1232,6 +1242,16 @@ exports.getNumberOfSpecFiles = (bsConfig, args, cypressConfig, turboScaleSession files = files.map((x) => { return x.replaceAll("\\", "/") }) // setting specs for turboScale as we don't have patched API for turboscale so we will rely on info from CLI bsConfig.run_settings.specs = files; + } else if (this.isReRunSpecsSession() && files.length) { + // BROWSERSTACK_RERUN_TESTS arrives as bare filenames ("a.ts,b.ts"). run_settings is + // forwarded to the remote machines verbatim, and a bare filename does not resolve + // against the project root there, so Cypress exits having run no spec at all. The + // glob above already resolved them against cypressProjectDir — persist those + // project-relative paths so the remote receives something it can resolve. + bsConfig.run_settings.specs = files + .map((file) => path.relative(bsConfig.run_settings.cypressProjectDir, file).replaceAll("\\", "/")) + .join(","); + logger.debug(`Re-run specs resolved to project-relative paths: ${bsConfig.run_settings.specs}`); } return files; } catch (err) { diff --git a/test/unit/bin/helpers/utils.js b/test/unit/bin/helpers/utils.js index 2705d770..480cb5f4 100644 --- a/test/unit/bin/helpers/utils.js +++ b/test/unit/bin/helpers/utils.js @@ -2327,6 +2327,76 @@ describe('utils', () => { }); describe('getNumberOfSpecFiles', () => { + context('when re-running observability failed tests (SDK-7124)', () => { + let rerunStubs = []; + const specPattern = `cypress/e2e/**/*.+(${constant.specFileTypes.join('|')})`; + + const stubGlob = () => { + const globStub = sinon.stub(glob, 'sync'); + globStub.withArgs(specPattern).returns([ + 'cypress/e2e/FlightOffer/FO-E2E-02.ts', + 'cypress/e2e/FlightOffer/FO-E2E-05.ts', + 'cypress/e2e/FlightOffer/FO-E2E-09.ts', + ]); + globStub.withArgs('{FO-E2E-02.ts,FO-E2E-05.ts}').returns([ + 'cypress/e2e/FlightOffer/FO-E2E-02.ts', + 'cypress/e2e/FlightOffer/FO-E2E-05.ts', + ]); + return globStub; + }; + + const buildConfig = (specs) => ({ + run_settings: { + cypressTestSuiteType: CYPRESS_V10_AND_ABOVE_TYPE, + specs: specs, + cypressProjectDir: '/repo', + }, + }); + + beforeEach(() => { + rerunStubs.push(sinon.stub(o11yHelpers, 'isBrowserstackInfra').returns(true)); + rerunStubs.push(sinon.stub(o11yHelpers, 'isTestObservabilitySession').returns(true)); + rerunStubs.push(sinon.stub(o11yHelpers, 'shouldReRunObservabilityTests').returns(true)); + }); + + afterEach(() => { + rerunStubs.forEach((s) => s.restore()); + rerunStubs = []; + if (glob.sync.restore) glob.sync.restore(); + }); + + it('rewrites the bare rerun filenames to project-relative paths for the remote machines', () => { + stubGlob(); + // shape setUserSpecs leaves behind for a rerun: bare filenames, comma separated + let bsConfig = buildConfig('FO-E2E-02.ts,FO-E2E-05.ts'); + + const result = utils.getNumberOfSpecFiles(bsConfig, {}, {}); + + expect(result.length).to.eql(2); + // run_settings is forwarded to the remote verbatim, so it must carry resolvable paths + expect(bsConfig.run_settings.specs).to.be.eq( + 'cypress/e2e/FlightOffer/FO-E2E-02.ts,cypress/e2e/FlightOffer/FO-E2E-05.ts' + ); + }); + + it('leaves specs untouched when this is not a rerun session', () => { + rerunStubs.forEach((s) => s.restore()); + rerunStubs = []; + sinon.stub(o11yHelpers, 'isBrowserstackInfra').returns(false); + sinon.stub(o11yHelpers, 'isTestObservabilitySession').returns(false); + sinon.stub(o11yHelpers, 'shouldReRunObservabilityTests').returns(false); + rerunStubs.push(o11yHelpers.isBrowserstackInfra); + rerunStubs.push(o11yHelpers.isTestObservabilitySession); + rerunStubs.push(o11yHelpers.shouldReRunObservabilityTests); + stubGlob(); + let bsConfig = buildConfig('FO-E2E-02.ts,FO-E2E-05.ts'); + + utils.getNumberOfSpecFiles(bsConfig, {}, {}); + + expect(bsConfig.run_settings.specs).to.be.eq('FO-E2E-02.ts,FO-E2E-05.ts'); + }); + }); + it('should return files matching with run_settings.specs and under default folder if cypress v <= 9 and no integration/testFiles patterm provided', () => { let globStub = sinon.stub(glob, 'sync') globStub.withArgs('cypress/integration/foo*.js')