@revoloo/cypress6
Version:
Cypress.io end to end testing tool
409 lines (332 loc) • 10.9 kB
JavaScript
require('../spec_helper')
const cp = require('child_process')
const fse = require('fs-extra')
const os = require('os')
const path = require('path')
const _ = require('lodash')
const { expect } = require('chai')
const debug = require('debug')('test:proxy-performance')
const DebuggingProxy = require('@cypress/debugging-proxy')
const HarCapturer = require('chrome-har-capturer')
const performance = require('../support/helpers/performance')
const Promise = require('bluebird')
const sanitizeFilename = require('sanitize-filename')
process.env.CYPRESS_INTERNAL_ENV = 'development'
const CA = require('@packages/https-proxy').CA
const Config = require('../../lib/config')
const { ServerE2E } = require('../../lib/server-e2e')
const { _getArgs } = require('../../lib/browsers/chrome')
const CHROME_PATH = 'google-chrome'
const URLS_UNDER_TEST = [
'https://test-page-speed.cypress.io/index1000.html',
'http://test-page-speed.cypress.io/index1000.html',
]
const start = (new Date()) / 1000
const PROXY_PORT = process.env.PROXY_PORT || 45678
const HTTPS_PROXY_PORT = process.env.HTTPS_PROXY_PORT || 45681
const CDP_PORT = 45679 /** port range starts here, not the actual port */
const CY_PROXY_PORT = 45680
const TEST_CASES = [
// these first 4 cases don't involve Cypress, don't need to run every time
// {
// name: 'Chrome w/o HTTP/2',
// disableHttp2: true,
// },
// {
// name: 'Chrome',
// },
// {
// name: 'With proxy',
// upstreamProxy: true,
// },
// {
// name: 'With HTTPS proxy',
// httpsUpstreamProxy: true,
// },
// baseline test that all other tests are compared to
{
name: 'Chrome w/ proxy w/o HTTP/2 (baseline)',
disableHttp2: true,
upstreamProxy: true,
},
{
name: 'With Cypress proxy, Intercepted',
cyProxy: true,
cyIntercept: true,
},
{
name: 'With Cypress proxy, Not Intercepted',
cyProxy: true,
},
{
name: 'With Cypress proxy w/o HTTP/2, Not Intercepted',
cyProxy: true,
disableHttp2: true,
},
{
name: 'With Cypress proxy and upstream, Intercepted',
cyProxy: true,
upstreamProxy: true,
cyIntercept: true,
},
{
name: 'With Cypress proxy and HTTPS upstream, Intercepted',
cyProxy: true,
httpsUpstreamProxy: true,
cyIntercept: true,
},
{
name: 'With Cypress proxy and upstream, Not Intercepted',
cyProxy: true,
upstreamProxy: true,
},
{
name: 'With Cypress proxy and HTTPS upstream, Not Intercepted',
cyProxy: true,
httpsUpstreamProxy: true,
},
].map((v) => {
// fill in all the fields so the keys are in the correct order for readability
return _.defaults(v, {
disableHttp2: false,
upstreamProxy: false,
httpsUpstreamProxy: false,
cyProxy: false,
cyIntercept: false,
})
})
const average = (arr) => {
return _.sum(arr) / arr.length
}
const percentile = (sortedArr, p) => {
const i = Math.floor(p / 100 * (sortedArr.length - 1))
return Math.round(sortedArr[i])
}
const getResultsFromHar = (har) => {
// HAR 1.2 Spec: http://www.softwareishard.com/blog/har-12-spec/
const { entries } = har.log
const results = {}
const first = entries[0]
const last = entries[entries.length - 1]
const elapsed = Number(new Date(last.startedDateTime)) + last.time - Number(new Date(first.startedDateTime))
results['Total'] = Math.round(elapsed)
let mins = {}
let maxes = {}
const timings = {
'receive': [],
'wait': [],
'send': [],
'total': [],
}
entries.forEach((entry) => {
const blockedTime = _.get(entry.timings, 'blocked', -1) === -1 ? 0 : entry.timings.blocked
const totalTime = entry.time - blockedTime
timings.total.push(totalTime)
Object.keys(entry.timings).forEach((timingKey) => {
if (entry.timings[timingKey] === -1 || !entry.timings[timingKey]) return
const ms = Math.round(entry.timings[timingKey])
if (timings[timingKey]) timings[timingKey].push(ms)
})
})
for (const key in timings) {
const arr = timings[key]
arr.sort((a, b) => {
return a - b
})
mins[key] = Math.round(arr[0])
maxes[key] = Math.round(arr[arr.length - 1])
_.merge(results, {
[`Avg ${_.upperFirst(key)}`]: Math.round(average(arr)),
})
}
results['Min'] = mins.total
expect(timings.total.length).to.be.at.least(1000)
;[1, 5, 25, 50, 75, 95, 99, 99.7].forEach((p) => {
results[`${p}% <=`] = percentile(timings.total, p)
})
results['Max'] = maxes.total
return results
}
const runBrowserTest = (urlUnderTest, testCase) => {
const cdpPort = CDP_PORT + Math.round(Math.random() * 10000)
const browser = {
isHeadless: true,
}
const options = {}
const args = _getArgs(browser, options, cdpPort).concat([
// additionally...
'--disable-background-networking',
'--no-sandbox', // allows us to run as root, for CI
`--user-data-dir=${fse.mkdtempSync(path.join(os.tmpdir(), 'cy-perf-'))}`,
])
if (testCase.disableHttp2) {
args.push('--disable-http2')
}
if (testCase.cyProxy) {
args.push(`--proxy-server=http://localhost:${CY_PROXY_PORT}`)
}
if (testCase.upstreamProxy && !testCase.cyProxy) {
args.push(`--proxy-server=http://localhost:${PROXY_PORT}`)
} else if (testCase.httpsUpstreamProxy && !testCase.cyProxy) {
args.push(`--proxy-server=https://localhost:${HTTPS_PROXY_PORT}`)
}
if (testCase.upstreamProxy && testCase.cyProxy) {
process.env.HTTP_PROXY = process.env.HTTPS_PROXY = `http://localhost:${PROXY_PORT}`
} else if (testCase.httpsUpstreamProxy && testCase.cyProxy) {
process.env.HTTP_PROXY = process.env.HTTPS_PROXY = `https://localhost:${HTTPS_PROXY_PORT}`
} else {
delete process.env.HTTPS_PROXY
delete process.env.HTTP_PROXY
}
if (testCase.cyIntercept) {
cyServer._onDomainSet(urlUnderTest)
} else {
cyServer._onDomainSet('<root>')
}
let cmd = CHROME_PATH
debug('Launching Chrome: ', cmd, args.join(' '))
const proc = cp.spawn(cmd, args, {
stdio: 'ignore',
})
const storeHar = Promise.method((name, har) => {
const artifacts = process.env.CIRCLE_ARTIFACTS
if (artifacts) {
return fse.ensureDir(artifacts)
.then(() => {
const pathToFile = path.join(artifacts, sanitizeFilename(`${name}.har`))
debug('saving har to path:', pathToFile)
return fse.writeJson(pathToFile, har)
})
}
})
const runHar = () => {
// wait for Chrome to open, then start capturing
return Promise.delay(500).then(() => {
debug('Trying to connect to Chrome...')
const harCapturer = HarCapturer.run([
urlUnderTest,
], {
port: cdpPort,
// disable SSL verification on older Chrome versions, copied from the HAR CLI
// https://github.com/cyrus-and/chrome-har-capturer/blob/587550508bddc23b7f4b4328c158322be4749298/bin/cli.js#L60
preHook: (_, cdp) => {
const { Security } = cdp
return Security.enable().then(() => {
return Security.setOverrideCertificateErrors({ override: true })
})
.then(() => {
return Security.certificateError(({ eventId }) => {
debug('EVENT ID', eventId)
return Security.handleCertificateError({ eventId, action: 'continue' })
})
})
},
// wait til all data is done before finishing
// https://github.com/cyrus-and/chrome-har-capturer/issues/59
postHook: (_, cdp) => {
let timeout
return new Promise((resolve) => {
cdp.on('event', (message) => {
if (message.method === 'Network.dataReceived') {
// reset timer
clearTimeout(timeout)
timeout = setTimeout(resolve, 1000)
}
})
})
},
})
return new Promise((resolve, reject) => {
harCapturer.on('fail', (_, err) => {
return reject(err)
})
harCapturer.on('har', resolve)
})
.then((har) => {
proc.kill(9)
debug('Received HAR from Chrome')
const results = getResultsFromHar(har)
_.merge(testCase, results)
return storeHar(testCase.name, har)
.return(results)
})
.catch({ code: 'ECONNREFUSED' }, (err) => {
// sometimes chrome takes surprisingly long, just reconn
debug('Chrome connection failed: ', err)
return runHar()
})
})
}
return runHar()
}
let cyServer
describe('Proxy Performance', function () {
this.timeout(60 * 1000)
this.retries(3)
beforeEach(function () {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
nock.enableNetConnect()
})
before(function () {
return CA.create()
.then((ca) => {
return ca.generateServerCertificateKeys('localhost')
})
.spread((cert, key) => {
return Promise.join(
new DebuggingProxy().start(PROXY_PORT),
new DebuggingProxy({
https: { cert, key },
}).start(HTTPS_PROXY_PORT),
Config.set({
projectRoot: '/tmp/a',
}).then((config) => {
config.port = CY_PROXY_PORT
// turn off morgan
config.morgan = false
cyServer = new ServerE2E()
return cyServer.open(config)
}),
)
})
})
URLS_UNDER_TEST.map((urlUnderTest) => {
describe(urlUnderTest, function () {
let baseline
const testCases = _.cloneDeep(TEST_CASES)
before(function () {
// run baseline test
return runBrowserTest(urlUnderTest, testCases[0])
.then((runtime) => {
debug('baseline runtime is: ', runtime)
baseline = runtime
})
})
// slice(1) since first test is used as baseline above
testCases.slice(1).map((testCase) => {
let multiplier = 3
if (testCase.httpsUpstreamProxy) {
// there is extra slowdown when the HTTPS upstream is used, so slightly increase the multiplier
// maybe from higher CPU utilization with debugging-proxy and HTTPS
multiplier *= 1.5
}
it(`${testCase.name} loads 1000 images less than ${multiplier}x as slowly as Chrome`, function () {
debug('Current test: ', testCase.name)
return runBrowserTest(urlUnderTest, testCase)
.then((results) => {
expect(results['Total']).to.be.lessThan(multiplier * baseline['Total'])
})
})
})
after(() => {
debug(`Done in ${Math.round((new Date() / 1000) - start)}s`)
process.stdout.write('Note: All times are in milliseconds.\n')
console.table(testCases)
return Promise.map(testCases, (testCase) => {
testCase['URL'] = urlUnderTest
return performance.track('Proxy Performance', testCase)
})
})
})
})
})