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
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
10 changes: 10 additions & 0 deletions guides/protocol-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Protocol Development

In production, the capture code used to capture and communicate test data will be retrieved from the Cloud. However, in order to develop the capture code locally, developers will:

* Clone the `cypress-services` repo
* Run `yarn`
* Run `yarn watch` in `packages/app-capture-protocol`
* Clone the `cypress` repo
* Run `yarn`
* Execute `CYPRESS_LOCAL_PROTOCOL_PATH=path/to/cypress-services/packages/app-capture-protocol/dist/index.js CYPRESS_INTERNAL_ENV=staging yarn cypress:run --record --key <record_key> --project <path/to/project>` on a project in record mode
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
8 changes: 8 additions & 0 deletions packages/server/lib/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as enc from './encryption'
import getEnvInformationForProjectRoot from './environment'

import type { OptionsWithUrl } from 'request-promise'
import type { ProtocolManager } from '@packages/types'
const THIRTY_SECONDS = humanInterval('30 seconds')
const SIXTY_SECONDS = humanInterval('60 seconds')
const TWO_MINUTES = humanInterval('2 minutes')
Expand Down Expand Up @@ -241,6 +242,7 @@ export type CreateRunOptions = {
tags: string[]
testingType: 'e2e' | 'component'
timeout?: number
protocolManager?: ProtocolManager
}

let preflightResult = {
Expand Down Expand Up @@ -330,6 +332,12 @@ module.exports = {
})
})
})
.then(async (result) => {
// TODO(protocol): Get url for the protocol code and pass it down to download
await options.protocolManager?.setupProtocol()

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

// TODO(protocol): This is basic for now but will evolve as we progress with the protocol wor

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

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

// TODO(protocol): 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(protocol): 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 via url %s', url)

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)
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
Loading