Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 27 additions & 24 deletions bin/accessibility-automation/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,32 +378,35 @@ exports.setAccessibilityEventListeners = (bsConfig) => {
}

const globPattern = process.cwd() + supportFilesData.supportFile;
glob(globPattern, {}, (err, files) => {
if(err) {
logger.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files');
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are removing this, how do we know if exception caused issue in build start?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception visibility is preserved — actually improved. With glob.sync, any glob failure now throws instead of being handed to a callback err arg, and that throw is caught by the outer try…catch(e) wrapping the whole function:

} catch(e) {
  logger.debug(`Unable to parse support files to set event listeners with error ${e}`, true, e);
}

So a build-start parse failure is still logged — and with more signal than before: the removed callback only logged a static string (EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files) with no error object, whereas the outer catch logs the actual ${e} and forwards true, e to the reporter.

This also brings it in line with testObservability.setEventListeners, which is the pattern this PR mirrors — it uses the same glob.sync + single outer catch and never had a separate glob-error branch either.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in code (commit 3de3f02): wrapped glob.sync in its own try/catch that restores the distinctive EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files log on a parse failure (now with the error object attached) and returns early — keeping the call synchronous to avoid the archive race.


files.forEach(file => {
try {
const fileName = path.basename(file);
if(['e2e.js', 'e2e.ts', 'component.ts', 'component.js'].includes(fileName) && !file.includes('node_modules')) {

const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
let cypressCommandEventListener = getAccessibilityCypressCommandEventListener(path.extname(file));
if(!defaultFileContent.includes(cypressCommandEventListener)) {
let newFileContent = defaultFileContent +
'\n' +
cypressCommandEventListener +
'\n';
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
}
// Synchronous for the same reason as testObservability setEventListeners
// (SDK-7121): the caller archives the suite right after this returns, so an
// async glob callback would race the archive and ship un-instrumented specs.
let files;
try {
files = glob.sync(globPattern, {});
} catch(err) {
logger.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files', true, err);
return;
}
files.forEach(file => {
try {
const fileName = path.basename(file);
if(['e2e.js', 'e2e.ts', 'component.ts', 'component.js'].includes(fileName) && !file.includes('node_modules')) {

const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
let cypressCommandEventListener = getAccessibilityCypressCommandEventListener(path.extname(file));
if(!defaultFileContent.includes(cypressCommandEventListener)) {
let newFileContent = defaultFileContent +
'\n' +
cypressCommandEventListener +
'\n';
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
}
} catch(e) {
logger.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
}
});
} catch(e) {
logger.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
}
});
} catch(e) {
logger.debug(`Unable to parse support files to set event listeners with error ${e}`, true, e);
Expand Down
40 changes: 21 additions & 19 deletions bin/testObservability/helper/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,27 +301,29 @@ exports.setEventListeners = (bsConfig) => {
try {
const supportFilesData = helper.getSupportFiles(bsConfig, false);
if(!supportFilesData.supportFile) return;
glob(process.cwd() + supportFilesData.supportFile, {}, (err, files) => {
if(err) return exports.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files');
files.forEach(file => {
try {
if (isE2ESupportFile(file) || !files.some(f => isE2ESupportFile(f))) {
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});

let cypressCommandEventListener = getCypressCommandEventListener(file.includes('js'));
if(!defaultFileContent.includes(cypressCommandEventListener)) {
let newFileContent = defaultFileContent +
'\n' +
cypressCommandEventListener +
'\n'
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
}
// Must be synchronous: runs.js proceeds to md5 hashing and zip archiving
// immediately after this returns. An async glob callback races the archive
// (SDK-7121) — a lost race ships an un-instrumented suite, and md5 caching
// makes it sticky, so TRA receives no test events.
const files = glob.sync(process.cwd() + supportFilesData.supportFile, {});
files.forEach(file => {
try {
if (isE2ESupportFile(file) || !files.some(f => isE2ESupportFile(f))) {
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});

let cypressCommandEventListener = getCypressCommandEventListener(file.includes('js'));
if(!defaultFileContent.includes(cypressCommandEventListener)) {
let newFileContent = defaultFileContent +
'\n' +
cypressCommandEventListener +
'\n'
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
}
} catch(e) {
exports.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
}
});
} catch(e) {
exports.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
}
});
} catch(e) {
exports.debug(`Unable to parse support files to set event listeners with error ${e}`, true, e);
Expand Down
80 changes: 80 additions & 0 deletions test/unit/bin/testObservability/setEventListeners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use strict';
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const fs = require('fs');
const os = require('os');
const path = require('path');

const o11yHelper = require('../../../../bin/testObservability/helper/helper');
const a11yHelper = require('../../../../bin/accessibility-automation/helper');
const baseHelper = require('../../../../bin/helpers/helper');

// Regression guard for SDK-7121: the support-file instrumentation MUST land
// synchronously. runs.js calls setEventListeners(bsConfig) and then proceeds
// immediately to md5 hashing + zip archiving. When the injection was deferred to
// an async glob callback, it raced the archive — a lost race shipped an
// un-instrumented suite, and md5 caching made it sticky, so the new Automate
// dashboard (TRA) received zero test events.
describe('SDK-7121 synchronous support-file instrumentation', () => {
let tmpDir, supportPath, cwdStub, getSupportFilesStub;

const setupTmpProject = () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk7121-'));
fs.mkdirSync(path.join(tmpDir, 'cypress', 'support'), { recursive: true });
supportPath = path.join(tmpDir, 'cypress', 'support', 'e2e.js');
fs.writeFileSync(supportPath, '// user original support file\n');
cwdStub = sinon.stub(process, 'cwd').returns(tmpDir);
};

afterEach(() => {
if (cwdStub) cwdStub.restore();
if (getSupportFilesStub) getSupportFilesStub.restore();
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
cwdStub = getSupportFilesStub = tmpDir = undefined;
});

describe('testObservability setEventListeners', () => {
beforeEach(() => {
setupTmpProject();
process.env.BS_TESTOPS_BUILD_COMPLETED = 'true';
// non-magic path -> glob.sync resolves the exact file
getSupportFilesStub = sinon.stub(baseHelper, 'getSupportFiles').returns({
supportFile: '/cypress/support/e2e.js',
cleanupParams: {}
});
});

it('injects the observability require synchronously before returning', () => {
o11yHelper.setEventListeners({ run_settings: {} });
// Read exactly as md5/archive would — synchronously, right after the call.
const content = fs.readFileSync(supportPath, 'utf-8');
expect(content).to.include('browserstack-cypress-cli/bin/testObservability/cypress');
});

it('does not double-inject when called twice (idempotent)', () => {
o11yHelper.setEventListeners({ run_settings: {} });
o11yHelper.setEventListeners({ run_settings: {} });
const content = fs.readFileSync(supportPath, 'utf-8');
const occurrences = content.split('browserstack-cypress-cli/bin/testObservability/cypress').length - 1;
expect(occurrences).to.equal(1);
});
});

describe('accessibility setAccessibilityEventListeners (glob-pattern branch)', () => {
beforeEach(() => {
setupTmpProject();
// magic pattern -> exercises the glob.sync branch fixed for SDK-7121
getSupportFilesStub = sinon.stub(baseHelper, 'getSupportFiles').returns({
supportFile: '/cypress/support/**/*.js',
cleanupParams: {}
});
});

it('injects the accessibility require synchronously before returning', () => {
a11yHelper.setAccessibilityEventListeners({ run_settings: {} });
const content = fs.readFileSync(supportPath, 'utf-8');
expect(content).to.include('browserstack-cypress-cli/bin/accessibility-automation/cypress');
});
});
});
Loading