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

chore: initial protocol api work #26080

Merged
merged 18 commits into from
Mar 13, 2023
Merged
18 changes: 9 additions & 9 deletions packages/app/src/runner/event-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,6 @@ export class EventManager {
})
})

Cypress.on('test:before:run:async', (test, _runnable) => {
this.reporterBus.emit('test:before:run:async', test)
})

Cypress.on('test:after:run', (test, _runnable) => {
this.reporterBus.emit('test:after:run', test, Cypress.config('isInteractive'))
})
Expand Down Expand Up @@ -591,7 +587,11 @@ export class EventManager {
this.localBus.emit('script:error', err)
})

Cypress.on('test:before:run:async', async (_attr, test) => {
Cypress.on('test:before:run:async', async (...args) => {
const [attr, test] = args

this.reporterBus.emit('test:before:run:async', attr)

this.studioStore.interceptTest(test)

// if the experimental flag is on and we are in a chromium based browser,
Expand All @@ -601,6 +601,10 @@ export class EventManager {
test: { title: test.title, order: test.order, currentRetry: test.currentRetry() },
})
}

await Cypress.backend('protocol:test:before:run:async', { id: test.id, title: test.title, wallClockStartedAt: test.wallClockStartedAt.getTime() })

Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run:async', ...args)
})

Cypress.on('test:after:run', (test) => {
Expand All @@ -615,10 +619,6 @@ export class EventManager {
Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run', ...args)
})

Cypress.on('test:before:run:async', (...args) => {
Cypress.primaryOriginCommunicator.toAllSpecBridges('test:before:run:async', ...args)
})

// Inform all spec bridges that the primary origin has begun to unload.
Cypress.on('window:before:unload', () => {
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload', window.origin)
Expand Down
2 changes: 1 addition & 1 deletion packages/server/lib/browsers/browser-cri-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const retryWithIncreasingDelay = async <T>(retryable: () => Promise<T>, browserN

export class BrowserCriClient {
currentlyAttachedTarget: CriClient | undefined
private constructor (private browserClient: CriClient, private versionInfo, private host: string, private port: number, private browserName: string, private onAsynchronousError: Function) {}
private constructor (private browserClient: CriClient, private versionInfo, public host: string, public port: number, private browserName: string, private onAsynchronousError: Function) {}

/**
* Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the
Expand Down
6 changes: 6 additions & 0 deletions packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,12 @@ export = {
this._handleDownloads(pageCriClient, options.downloadsFolder, automation),
])

await options.protocolManager?.connectToBrowser({
target: pageCriClient.targetId,
host: browserCriClient.host,
port: browserCriClient.port,
})

await this._navigateUsingCRI(pageCriClient, url)

await this._handlePausedRequests(pageCriClient)
Expand Down
7 changes: 7 additions & 0 deletions packages/server/lib/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export type CreateRunOptions = {
tags: string[]
testingType: 'e2e' | 'component'
timeout?: number
protocolManager?: any
mschile marked this conversation as resolved.
Show resolved Hide resolved
}

let preflightResult = {
Expand Down Expand Up @@ -330,6 +331,12 @@ module.exports = {
})
})
})
.then(async (result) => {
// TODO: Get url for the protocol code and pass it down to download
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggest namespacing all the TODOs so we can easily verify that all the TODOs have been completed.

Suggested change
// TODO: Get url for the protocol code and pass it down to download
// TODO(protocol): Get url for the protocol code and pass it down to download

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good call.

await options.protocolManager?.setupProtocol()

return result
})
.catch(RequestErrors.StatusCodeError, formatResponseBody)
.catch(tagError)
},
Expand Down
77 changes: 77 additions & 0 deletions packages/server/lib/cloud/protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import fs from 'fs-extra'
import { NodeVM } from 'vm2'
import Debug from 'debug'
import CDP from 'chrome-remote-interface'
import type { ProtocolManager } from '@packages/types'

interface AppCaptureProtocolInterface {
setupProtocol (url?: string): Promise<void>
connectToBrowser (options: any): void
beforeSpec (spec: any): void
afterSpec (): void
beforeTest (test: any): void
mschile marked this conversation as resolved.
Show resolved Hide resolved
}

const debug = Debug('cypress:server:protocol')

const setupProtocol = async (url?: string): Promise<AppCaptureProtocolInterface | undefined> => {
let script: string | undefined

// TODO: We will need to remove this option in production
if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
script = await fs.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8')
} else if (url) {
// TODO: Download the protocol script from the cloud
}

if (script) {
const vm = new NodeVM({
console: 'inherit',
sandbox: { Debug, CDP },
})

const { AppCaptureProtocol } = vm.run(script)

return new AppCaptureProtocol()
}

return
}

class ProtocolManagerImpl implements ProtocolManager {
private protocol: AppCaptureProtocolInterface | undefined

protocolEnabled (): boolean {
return !!this.protocol
}

async setupProtocol (url?: string) {
debug('setting up protocol')
mschile marked this conversation as resolved.
Show resolved Hide resolved

this.protocol = await setupProtocol(url)
}

connectToBrowser (options) {
debug('connecting to browser for new spec')
this.protocol?.connectToBrowser(options)
}

beforeSpec (spec) {
debug('initializing new spec %O', spec.relative)
this.protocol?.beforeSpec(spec)

// Initialize DB here
}

afterSpec () {
debug('after spec')
this.protocol?.afterSpec()
}

beforeTest (test) {
debug('initialize new test %O', test)
this.protocol?.beforeTest(test)
}
}

export default ProtocolManagerImpl
4 changes: 0 additions & 4 deletions packages/server/lib/modes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import { makeDataContext } from '../makeDataContext'
import random from '../util/random'

export = (mode, options) => {
if (mode === 'record') {
mschile marked this conversation as resolved.
Show resolved Hide resolved
return require('./record').run(options)
}

if (mode === 'smokeTest') {
return require('./smoke_test').run(options)
}
Expand Down
6 changes: 5 additions & 1 deletion packages/server/lib/modes/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ const createRun = Promise.method((options = {}) => {
ciBuildId: null,
})

let { projectRoot, projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures } = options
let { projectRoot, projectId, recordKey, platform, git, specPattern, specs, parallel, ciBuildId, group, tags, testingType, autoCancelAfterFailures, protocolManager } = options

if (recordKey == null) {
recordKey = env.get('CYPRESS_RECORD_KEY')
Expand Down Expand Up @@ -324,6 +324,7 @@ const createRun = Promise.method((options = {}) => {
ci,
commit,
autoCancelAfterFailures,
protocolManager,
})
.tap((response) => {
if (!(response && response.warnings && response.warnings.length)) {
Expand Down Expand Up @@ -597,6 +598,7 @@ const createRunAndRecordSpecs = (options = {}) => {
testingType,
quiet,
autoCancelAfterFailures,
protocolManager,
} = options
const recordKey = options.key

Expand Down Expand Up @@ -632,6 +634,7 @@ const createRunAndRecordSpecs = (options = {}) => {
testingType,
configFile: config ? config.configFile : null,
autoCancelAfterFailures,
protocolManager,
})
.then((resp) => {
if (!resp) {
Expand Down Expand Up @@ -672,6 +675,7 @@ const createRunAndRecordSpecs = (options = {}) => {
.pick('spec', 'claimedInstances', 'totalInstances')
.extend({
estimated: resp.estimatedWallClockDuration,
instanceId,
})
.value()
})
Expand Down
49 changes: 38 additions & 11 deletions packages/server/lib/modes/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { Cfg } from '../project-base'
import type { Browser } from '../browsers/types'
import { debugElapsedTime } from '../util/performance_benchmark'
import * as printResults from '../util/print-run'
import ProtocolManager from '../cloud/protocol'

type SetScreenshotMetadata = (data: TakeScreenshotProps) => void
type ScreenshotMetadata = ReturnType<typeof screenshotMetadata>
Expand Down Expand Up @@ -55,8 +56,8 @@ const relativeSpecPattern = (projectRoot, pattern) => {
return pattern.map((x) => x.replace(`${projectRoot}/`, ''))
}

const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: RunEachSpec, beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, config: Cfg }) {
const { specs, runEachSpec, beforeSpecRun, afterSpecRun, config } = options
const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec: RunEachSpec, beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, config: Cfg, protocolManager?: any }) {
mschile marked this conversation as resolved.
Show resolved Hide resolved
const { specs, runEachSpec, beforeSpecRun, afterSpecRun, config, protocolManager } = options

const serial = () => {
return Bluebird.mapSeries(specs, runEachSpec)
Expand All @@ -65,7 +66,7 @@ const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec:
const ranSpecs: SpecFile[] = []

async function parallelAndSerialWithRecord (runs) {
const { spec, claimedInstances, totalInstances, estimated } = await beforeSpecRun()
const { spec, claimedInstances, totalInstances, estimated, instanceId } = await beforeSpecRun()

// no more specs to run? then we're done!
if (!spec) return runs
Expand All @@ -77,6 +78,13 @@ const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec:

if (!specObject) throw new Error(`Unable to find specObject for spec '${spec}'`)

if (protocolManager) {
protocolManager.beforeSpec({
...specObject,
instanceId,
})
}

ranSpecs.push(specObject)

const results = await runEachSpec(
Expand All @@ -88,6 +96,10 @@ const iterateThroughSpecs = function (options: { specs: SpecFile[], runEachSpec:

runs.push(results)

if (protocolManager) {
protocolManager.afterSpec()
}

await afterSpecRun(specObject, results, config)

// recurse
Expand Down Expand Up @@ -158,6 +170,7 @@ const openProjectCreate = (projectRoot, socketId, args) => {
onWarning,
spec: args.spec,
onError: args.onError,
protocolManager: args.protocolManager,
}

return openProject.create(projectRoot, args, options)
Expand Down Expand Up @@ -345,12 +358,13 @@ async function postProcessRecording (options: { quiet: boolean, videoCompression
return continueProcessing(onProgress)
}

function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, setScreenshotMetadata: SetScreenshotMetadata, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void, videoRecording?: VideoRecording }) {
const { browser, spec, setScreenshotMetadata, screenshots, projectRoot, shouldLaunchNewTab, onError } = options
function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, setScreenshotMetadata: SetScreenshotMetadata, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, onError: (err: Error) => void, videoRecording?: VideoRecording, protocolManager?: any }) {
mschile marked this conversation as resolved.
Show resolved Hide resolved
const { browser, spec, setScreenshotMetadata, screenshots, projectRoot, shouldLaunchNewTab, onError, protocolManager } = options

const warnings = {}

const browserOpts: OpenProjectLaunchOpts = {
protocolManager,
projectRoot,
shouldLaunchNewTab,
onError,
Expand Down Expand Up @@ -443,7 +457,7 @@ function listenForProjectEnd (project, exit): Bluebird<any> {
})
}

async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording }) {
async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpec: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording, protocolManager?: any }) {
mschile marked this conversation as resolved.
Show resolved Hide resolved
if (globalThis.CY_TEST_MOCK?.waitForBrowserToConnect) return Promise.resolve()

const { project, socketId, onError, spec } = options
Expand Down Expand Up @@ -692,10 +706,10 @@ function screenshotMetadata (data, resp) {
}
}

async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, headed: boolean, outputPath: string, specs: SpecWithRelativeRoot[], specPattern: string | RegExp | string[], beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean, group?: string, tag?: string, autoCancelAfterFailures?: number | false, testingType: TestingType, quiet: boolean, project: Project, onError: (err: Error) => void, exit: boolean, socketId: string, webSecurity: boolean, projectRoot: string } & Pick<Cfg, 'video' | 'videoCompression' | 'videosFolder' | 'videoUploadOnPasses'>) {
async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, headed: boolean, outputPath: string, specs: SpecWithRelativeRoot[], specPattern: string | RegExp | string[], beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean, group?: string, tag?: string, autoCancelAfterFailures?: number | false, testingType: TestingType, quiet: boolean, project: Project, onError: (err: Error) => void, exit: boolean, socketId: string, webSecurity: boolean, projectRoot: string, protocolManager?: any } & Pick<Cfg, 'video' | 'videoCompression' | 'videosFolder' | 'videoUploadOnPasses'>) {
mschile marked this conversation as resolved.
Show resolved Hide resolved
if (globalThis.CY_TEST_MOCK?.runSpecs) return globalThis.CY_TEST_MOCK.runSpecs

const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag, autoCancelAfterFailures } = options
const { config, browser, sys, headed, outputPath, specs, specPattern, beforeSpecRun, afterSpecRun, runUrl, parallel, group, tag, autoCancelAfterFailures, protocolManager } = options

const isHeadless = !headed

Expand Down Expand Up @@ -754,6 +768,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea
runEachSpec,
afterSpecRun,
beforeSpecRun,
protocolManager,
})

const results: CypressCommandLine.CypressRunResult = {
Expand Down Expand Up @@ -822,7 +837,7 @@ async function runSpecs (options: { config: Cfg, browser: Browser, sys: any, hea
return results
}

async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: Project, browser: Browser, onError: (err: Error) => void, config: Cfg, quiet: boolean, exit: boolean, testingType: TestingType, socketId: string, webSecurity: boolean, projectRoot: string } & Pick<Cfg, 'video' | 'videosFolder' | 'videoCompression' | 'videoUploadOnPasses'>, estimated, isFirstSpec, isLastSpec) {
async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: Project, browser: Browser, onError: (err: Error) => void, config: Cfg, quiet: boolean, exit: boolean, testingType: TestingType, socketId: string, webSecurity: boolean, projectRoot: string, protocolManager?: ProtocolManager } & Pick<Cfg, 'video' | 'videosFolder' | 'videoCompression' | 'videoUploadOnPasses'>, estimated, isFirstSpec, isLastSpec) {
const { project, browser, onError } = options

const { isHeadless } = browser
Expand Down Expand Up @@ -889,6 +904,7 @@ async function runSpec (config, spec: SpecWithRelativeRoot, options: { project:
isFirstSpec,
experimentalSingleTabRunMode: config.experimentalSingleTabRunMode,
shouldLaunchNewTab: !isFirstSpec,
protocolManager: options.protocolManager,
}),
])

Expand All @@ -910,6 +926,12 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri

const { projectRoot, record, key, ciBuildId, parallel, group, browser: browserName, tag, testingType, socketId, autoCancelAfterFailures } = options

let protocolManager: ProtocolManager | undefined

if (record) {
protocolManager = new ProtocolManager()
}

assert(socketId)

// this needs to be a closure over `exitEarly` and not a reference
Expand All @@ -928,7 +950,10 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri
// TODO: refactor this so we don't need to extend options
options.browsers = browsers

const { project, projectId, config, configFile } = await createAndOpenProject(options)
const { project, projectId, config, configFile } = await createAndOpenProject({
...options,
protocolManager,
})

debug('project created and opened with config %o', config)

Expand Down Expand Up @@ -977,7 +1002,7 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri
chromePolicyCheck.run(onWarning)
}

async function runAllSpecs ({ beforeSpecRun, afterSpecRun, runUrl, parallel }: { beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean}) {
async function runAllSpecs ({ beforeSpecRun, afterSpecRun, runUrl, parallel }: { beforeSpecRun?: BeforeSpecRun, afterSpecRun?: AfterSpecRun, runUrl?: string, parallel?: boolean }) {
const results = await runSpecs({
autoCancelAfterFailures,
beforeSpecRun,
Expand Down Expand Up @@ -1007,6 +1032,7 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri
testingType: options.testingType,
exit: options.exit,
webSecurity: options.webSecurity,
protocolManager,
})

if (!options.quiet) {
Expand Down Expand Up @@ -1040,6 +1066,7 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri
runAllSpecs,
onError,
quiet: options.quiet,
protocolManager,
})
}

Expand Down
Loading