Skip to content

Commit

Permalink
feat: Attach test run report to job if fixture was executed remotely (#…
Browse files Browse the repository at this point in the history
…56)

* feat: Attach test run report to job if fixture was executed remotely

* Improves interoperability with the saucelabs browser provider by
  attaching the test report to the job that executed the tests.

* formatting

* Fix tests

* Cleanup unused function
  • Loading branch information
mhan83 authored Jun 27, 2024
1 parent 0f099ff commit c166037
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 6 deletions.
43 changes: 40 additions & 3 deletions src/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,35 @@ export class BrowserTestRun {
(this.testRun.metadata as { userAgent: string })['userAgent'] = userAgent;
this.assets = [];

const [browser, platform] = userAgent.split('/').map((ua) => ua.trim());
// NOTE: example userAgents:
// * Chrome 126.0.0.0 / Sonoma 14
// * Chrome 126.0.0.0 / Windows 10 (https://app.saucelabs.com/tests/000aa4ffc86d40bdbeebfcf165dab402)
const matches = userAgent.match(/^([\w .]+)(?:\/([\w .]+))?/);

if (matches) {
this.#browser = matches[1]?.trim() ?? '';
this.platform = matches[2]?.trim() ?? '';
} else {
this.#browser = '';
this.platform = '';
}
}

this.#browser = browser;
this.platform = platform;
/**
* Returns the job id for the remotely executed test run.
*
* Job link is provided in the user agent by the saucelabs browser provider.
*/
get jobId() {
// NOTE: example match:
// * Chrome 126.0.0.0 / Windows 10 (https://app.saucelabs.com/tests/000aa4ffc86d40bdbeebfcf165dab402)
const matches = this.userAgent.match(
/https:\/\/.*saucelabs\.com\/tests\/(\w+)/,
);
if (!matches || matches.length < 2) {
return null;
}
return matches[1];
}

get browserName() {
Expand Down Expand Up @@ -102,4 +127,16 @@ export class Fixture {
collectTestRuns() {
return [...this.browserTestRuns.values()].map((bc) => bc.testRun);
}

get localBrowserTestRuns() {
return [...this.browserTestRuns.values()].filter(
(run) => run.jobId === null,
);
}

get remoteBrowserTestRuns() {
return [...this.browserTestRuns.values()].filter(
(run) => run.jobId !== null,
);
}
}
46 changes: 43 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { TestRun } from '@saucelabs/sauce-json-reporter';

const path = require('path');
const fs = require('fs');
const stream = require('node:stream');

const { SauceJsonReporter } = require('./json-reporter');
const { JobReporter } = require('./job-reporter');

/**
* @typedef {import("./fixture").Fixture} Fixture
*/

module.exports = function () {
return {
noColors: !!process.env.SAUCE_NO_COLORS || !!process.env.SAUCE_VM,
Expand Down Expand Up @@ -91,6 +99,30 @@ module.exports = function () {

fs.writeFileSync(this.sauceReportJsonPath, mergedTestRun.stringify());

const remoteTestRuns = this.sauceJsonReporter.remoteTestRuns();
const tasks = [];
for (const [jobId, runs] of remoteTestRuns) {
const p = async () => {
const merged = new TestRun();
runs.forEach((run) => {
for (const suite of run.suites) {
suite.metadata = run.metadata;
merged.addSuite(suite);
}
});
merged.computeStatus();

const reportReadable = new stream.Readable();
reportReadable.push(merged.stringify());
reportReadable.push(null);

await this.reporter.attachTestRun(jobId, merged);
};
tasks.push(p());
}

await Promise.allSettled(tasks);

if (this.disableUpload) {
return;
}
Expand Down Expand Up @@ -123,18 +155,26 @@ module.exports = function () {
});
},

/**
* @param {Fixture} fixture
*/
async reportFixture(fixture) {
if (!this.reporter.isAccountSet()) {
return;
}

const browserTestRuns = fixture.localBrowserTestRuns;
if (browserTestRuns.length === 0) {
return;
}

this.setIndent(this.indentWidth * 3)
.newline()
.write(this.chalk.bold.underline('Sauce Labs Test Report'))
.newline();

const reportTasks = [];
for (const [userAgent, browserTestRun] of fixture.browserTestRuns) {
for (const browserTestRun of browserTestRuns) {
const task = new Promise((resolve, reject) => {
(async () => {
const session = {
Expand All @@ -146,7 +186,7 @@ module.exports = function () {
browserVersion: browserTestRun.browserVersion,
platformName: browserTestRun.platform,
assets: browserTestRun.assets,
userAgent: userAgent,
userAgent: browserTestRun.userAgent,
};
try {
const job = await this.reporter.reportSession(session);
Expand All @@ -170,7 +210,7 @@ module.exports = function () {
browserTestRun,
job.id,
);
resolve(job.id);
resolve();
} catch (e) {
reject(e);
}
Expand Down
23 changes: 23 additions & 0 deletions src/job-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,29 @@ class JobReporter {
return tests;
}

async attachTestRun(jobId, testRun) {
const reportReadable = new stream.Readable();
reportReadable.push(testRun.stringify());
reportReadable.push(null);

try {
const resp = await this.testComposer.uploadAssets(jobId, [
{
filename: 'sauce-test-report.json',
data: reportReadable,
},
]);

if (resp.errors) {
for (const err of resp.errors) {
console.error('Failed to upload asset:', err);
}
}
} catch (e) {
console.error('Failed to upload test result:', e.message);
}
}

async reportSession(session) {
if (!this.isAccountSet()) {
return;
Expand Down
19 changes: 19 additions & 0 deletions src/json-reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,25 @@ function reporterFactory() {
}, mergedTestRun);
},

remoteTestRuns() {
const remoteBrowserTestRuns = this.fixtures.flatMap(
(f) => f.remoteBrowserTestRuns,
);

/**
* @type Map<string, TestRun[]>
*/
const remoteRunsById = new Map();
remoteBrowserTestRuns.forEach((run) => {
if (!remoteRunsById.has(run.jobId)) {
remoteRunsById.set(run.jobId, []);
}
remoteRunsById.get(run.jobId).push(run.testRun);
});

return remoteRunsById;
},

/**
* @param {string} assetPath
* @param {string} testName
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/fixture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ describe('BrowserTestRun', () => {
['Firefox 97 / macOS 10.15.7', 'Firefox', '97'],
['Firefox 97', 'Firefox', '97'],
['Firefox', 'Firefox', 'unknown'],
[
'Chrome 126.0.0.0 / Windows 10 (https://app.saucelabs.com/tests/000aa4ffc86d40bdbeebfcf165dab402)',
'Chrome',
'126.0.0.0',
],
].forEach(([userAgent, expectedBrowserName, expectedBrowserVersion]) => {
test(`can parse browser from userAgent (${userAgent})`, async () => {
const sut = new BrowserTestRun(userAgent);
Expand All @@ -19,13 +24,34 @@ describe('BrowserTestRun', () => {
[
['Chrome 97.0.4692.71 / macOS 10.15.7', 'macOS 10.15.7'],
['Firefox 97 / macOS', 'macOS'],
[
'Chrome 126.0.0.0 / Windows 10 (https://app.saucelabs.com/tests/000aa4ffc86d40bdbeebfcf165dab402)',
'Windows 10',
],
].forEach(([userAgent, expectedPlatform]) => {
test(`can parse platform from userAgent (${userAgent})`, async () => {
const sut = new BrowserTestRun(userAgent);

expect(sut.platform).toBe(expectedPlatform);
});
});

[
[
'Chrome 126.0.0.0 / Windows 10 (https://app.saucelabs.com/tests/000aa4ffc86d40bdbeebfcf165dab402)',
'000aa4ffc86d40bdbeebfcf165dab402',
],
[
'Chrome 126.0.0.0 / Windows 10 (https://app.eu-central-1.saucelabs.com/tests/000aa4ffc86d40bdbeebfcf165dab402)',
'000aa4ffc86d40bdbeebfcf165dab402',
],
].forEach(([userAgent, expectedJobId]) => {
test(`can parse jobId from userAgent (${userAgent})`, async () => {
const sut = new BrowserTestRun(userAgent);

expect(sut.jobId).toBe(expectedJobId);
});
});
});

describe('Fixture', () => {
Expand Down

0 comments on commit c166037

Please sign in to comment.