Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Jasmine sessions for BrowserStack Test Observability (v8) #10421

Merged
merged 7 commits into from May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/wdio-browserstack-service/package.json
Expand Up @@ -35,6 +35,7 @@
"@wdio/reporter": "8.10.4",
"@wdio/types": "8.10.4",
"browserstack-local": "^1.5.1",
"csv-writer": "^1.6.0",
"form-data": "^4.0.0",
"git-repo-info": "^2.1.1",
"gitconfiglocal": "^2.1.0",
Expand Down
18 changes: 11 additions & 7 deletions packages/wdio-browserstack-service/src/insights-handler.ts
Expand Up @@ -5,6 +5,7 @@ import type { BeforeCommandArgs, AfterCommandArgs } from '@wdio/reporter'

import { v4 as uuidv4 } from 'uuid'
import type { Pickle, ITestCaseHookParameter } from './cucumber-types.js'
import TestReporter from './reporter.js'

import {
frameworkSupportsHook,
Expand Down Expand Up @@ -101,10 +102,6 @@ class _InsightsHandler {
}
await this.sendTestRunEvent(test, 'HookRunFinished', result)

if (this._framework !== 'mocha') {
return
}

const hookType = getHookType(test.title)
/*
If any of the `beforeAll`, `beforeEach`, `afterEach` then the tests after the hook won't run in mocha (https://github.com/mochajs/mocha/issues/4392)
Expand Down Expand Up @@ -143,6 +140,9 @@ class _InsightsHandler {
}

async beforeTest (test: Frameworks.Test) {
if (this._framework !== 'mocha') {
return
}
const fullTitle = getUniqueIdentifier(test, this._framework)
this._tests[fullTitle] = {
uuid: uuidv4(),
Expand All @@ -152,6 +152,9 @@ class _InsightsHandler {
}

async afterTest (test: Frameworks.Test, result: Frameworks.TestResult) {
if (this._framework !== 'mocha') {
return
}
const fullTitle = getUniqueIdentifier(test, this._framework)
this._tests[fullTitle] = {
...(this._tests[fullTitle] || {}),
Expand Down Expand Up @@ -270,8 +273,9 @@ class _InsightsHandler {
return
}
const identifier = this.getIdentifier(test)
const testMeta = this._tests[identifier] || TestReporter.getTests()[identifier]

if (!this._tests[identifier]) {
if (!testMeta) {
return
}

Expand All @@ -280,7 +284,7 @@ class _InsightsHandler {
await uploadEventData([{
event_type: 'LogCreated',
logs: [{
test_run_uuid: this._tests[identifier].uuid,
test_run_uuid: testMeta.uuid,
timestamp: new Date().toISOString(),
message: args.result.value,
kind: 'TEST_SCREENSHOT'
Expand All @@ -297,7 +301,7 @@ class _InsightsHandler {
const req = this._requestQueueHandler.add({
event_type: 'LogCreated',
logs: [{
test_run_uuid: this._tests[identifier].uuid,
test_run_uuid: testMeta.uuid,
timestamp: new Date().toISOString(),
kind: 'HTTP',
http_response: {
Expand Down
16 changes: 16 additions & 0 deletions packages/wdio-browserstack-service/src/launcher.ts
Expand Up @@ -11,6 +11,7 @@ import * as BrowserstackLocalLauncher from 'browserstack-local'

import logger from '@wdio/logger'
import type { Capabilities, Services, Options } from '@wdio/types'
import PerformanceTester from './performance-tester.js'

import type { BrowserstackConfig, App, AppConfig, AppUploadResponse } from './types.js'
import { BSTACK_SERVICE_VERSION, VALID_APP_EXTENSION } from './constants.js'
Expand Down Expand Up @@ -91,6 +92,10 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
})
}

if (process.env.BROWSERSTACK_O11Y_PERF_MEASUREMENT) {
PerformanceTester.startMonitoring('performance-report-launcher.csv')
}

// by default observability will be true unless specified as false
this._options.testObservability = this._options.testObservability === false ? false : true

Expand Down Expand Up @@ -222,6 +227,17 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
if (process.env.BS_TESTOPS_BUILD_HASHED_ID) {
console.log(`\nVisit https://observability.browserstack.com/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID} to view build report, insights, and many more debugging information all at one place!\n`)
}

if (process.env.BROWSERSTACK_O11Y_PERF_MEASUREMENT) {
await PerformanceTester.stopAndGenerate('performance-launcher.html')
PerformanceTester.calculateTimes(['launchTestSession', 'stopBuildUpstream'])

if (!process.env.START_TIME) {
return
}
const duration = (new Date()).getTime() - (new Date(process.env.START_TIME)).getTime()
log.info(`Total duration is ${duration / 1000 } s`)
}
}

if (!this.browserstackLocal || !this.browserstackLocal.isRunning()) {
Expand Down
105 changes: 105 additions & 0 deletions packages/wdio-browserstack-service/src/performance-tester.ts
@@ -0,0 +1,105 @@
import { createObjectCsvWriter } from 'csv-writer'
import fs from 'node:fs'
import { performance, PerformanceObserver } from 'node:perf_hooks'
import logger from '@wdio/logger'
import { sleep } from './util.js'

const log = logger('@wdio/browserstack-service')

export default class PerformanceTester {
static _observer: PerformanceObserver
static _csvWriter: any
private static _events: PerformanceEntry[] = []
static started = false

static startMonitoring(csvName: string = 'performance-report.csv') {
this._observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
this._events.push(entry)
})
})
this._observer.observe({ buffered: true, entryTypes: ['function'] })
this.started = true
this._csvWriter = createObjectCsvWriter({
path: csvName,
header: [
{ id: 'name', title: 'Function Name' },
{ id: 'time', title: 'Execution Time (ms)' }
]
})
}

static getPerformance() {
return performance
}

static calculateTimes(methods: string[]) : number {
const times: { [key: string]: number } = {}
this._events.map((entry) => {
if (!times[entry.name]) {
times[entry.name] = 0
}
times[entry.name] += entry.duration
})
const timeTaken = methods.reduce((a, c) => {
return times[c] + (a || 0)
}, 0)
log.info(`Time for ${methods} is `, timeTaken)
return timeTaken
}

static async stopAndGenerate(filename: string = 'performance-own.html') {
if (!this.started) {return}

await sleep(2000) // Wait to 2s just to finish any running callbacks for timerify
this._observer.disconnect()
this.started = false

this.generateCSV(this._events)

const content = this.generateReport(this._events)
const path = process.cwd() + '/' + filename
fs.writeFile(path, content, err => {
if (err) {
log.error('Error in writing html', err)
return
}
log.info('Performance report is at ', path)
})
}

static generateReport(entries: PerformanceEntry[]) {
let html = '<!DOCTYPE html><html><head><title>Performance Report</title></head><body>'
html += '<h1>Performance Report</h1>'
html += '<table><thead><tr><th>Function Name</th><th>Duration (ms)</th></tr></thead><tbody>'
entries.forEach((entry) => {
html += `<tr><td>${entry.name}</td><td>${entry.duration}</td></tr>`
})
html += '</tbody></table></body></html>'
return html
}

static generateCSV(entries: PerformanceEntry[]) {
const times: { [key: string]: number } = {}
entries.map((entry) => {
if (!times[entry.name]) {
times[entry.name] = 0
}
times[entry.name] += entry.duration

return {
name: entry.name,
time: entry.duration
}
})
const dat = Object.entries(times).map(([key, value]) => {
return {
name: key,
time: value
}
})
this._csvWriter.writeRecords(dat)
.then(() => log.info('Performance CSV report generated successfully'))
.catch((error: any) => console.error(error))
}
}