Skip to content

Commit

Permalink
Support for Jasmine sessions for BrowserStack Test Observability (v8) (
Browse files Browse the repository at this point in the history
…#10421)

* added jasmine changes

* fixed bugs

* added handling for unknown test and beforeAll hooks

* fixed needToSendData args

* sending hook stats in correct field

* renamed performance env variable
  • Loading branch information
sriteja777 committed May 22, 2023
1 parent 26b447d commit f6238d7
Show file tree
Hide file tree
Showing 13 changed files with 691 additions and 126 deletions.
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))
}
}

0 comments on commit f6238d7

Please sign in to comment.