refactor: extract artifact upload process from lib/modes/record.js (#29240)

* refactor record.js to extract upload logic into ts
- streamlines the main uploadArtifact fn
- extracts artifact specific logic to artifact classes
- fully defines types for upload processing and reporting

* tweak refactors so system tests produce same snapshots

* some todos, fixes exports/imports from api/index.ts

* fix api export so it can be imported by ts files

* cleans up types

* extracting artifact metadata from options logs to debug but does not throw if errors are encountered

* fix type imports in print-run

* fix debug formatting for artifacts

* fix reporting successful protocol uploads

* change inheritence to strategy

* rm empty file

* Update packages/server/lib/cloud/artifacts/upload_artifacts.ts

* makes protocolManager optional to uploadArtifacts, fixes conditional accessor in protocol fatal error report

* missed a potentially undef

* convert to frozen object / keyof instead of string composition for artifact kinds

---------

Co-authored-by: Ryan Manuel <ryanm@cypress.io>
Co-authored-by: Jennifer Shehane <jennifer@cypress.io>
This commit is contained in:
Cacie Prins
2024-04-12 09:20:54 -04:00
committed by GitHub
parent 5e8cc58f93
commit 79a267c7f8
20 changed files with 589 additions and 441 deletions

View File

@@ -58,6 +58,7 @@ export interface CypressRequestOptions extends OptionsWithUrl {
cacheable?: boolean
}
// TODO: migrate to fetch from @cypress/request
const rp = request.defaults((params: CypressRequestOptions, callback) => {
let resp
@@ -274,22 +275,23 @@ type CreateRunResponse = {
} | undefined
}
type ArtifactMetadata = {
export type ArtifactMetadata = {
url: string
fileSize?: number
fileSize?: number | bigint
uploadDuration?: number
success: boolean
error?: string
errorStack?: string
}
type ProtocolMetadata = ArtifactMetadata & {
export type ProtocolMetadata = ArtifactMetadata & {
specAccess?: {
size: bigint
offset: bigint
size: number
offset: number
}
}
type UpdateInstanceArtifactsPayload = {
export type UpdateInstanceArtifactsPayload = {
screenshots: ArtifactMetadata[]
video?: ArtifactMetadata
protocol?: ProtocolMetadata
@@ -298,7 +300,7 @@ type UpdateInstanceArtifactsPayload = {
type UpdateInstanceArtifactsOptions = {
runId: string
instanceId: string
timeout: number | undefined
timeout?: number
}
let preflightResult = {
@@ -307,7 +309,10 @@ let preflightResult = {
let recordRoutes = apiRoutes
module.exports = {
// Potential todos: Refactor to named exports, refactor away from `this.` in exports,
// move individual exports to their own files & convert this to barrelfile
export default {
rp,
// For internal testing
@@ -400,8 +405,10 @@ module.exports = {
let script
try {
if (captureProtocolUrl || process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
script = await this.getCaptureProtocolScript(captureProtocolUrl || process.env.CYPRESS_LOCAL_PROTOCOL_PATH)
const protocolUrl = captureProtocolUrl || process.env.CYPRESS_LOCAL_PROTOCOL_PATH
if (protocolUrl) {
script = await this.getCaptureProtocolScript(protocolUrl)
}
} catch (e) {
debugProtocol('Error downloading capture code', e)

View File

@@ -0,0 +1,113 @@
import Debug from 'debug'
import { performance } from 'perf_hooks'
const debug = Debug('cypress:server:cloud:artifact')
const isAggregateError = (err: any): err is AggregateError => {
return !!err.errors
}
export const ArtifactKinds = Object.freeze({
VIDEO: 'video',
SCREENSHOTS: 'screenshots',
PROTOCOL: 'protocol',
})
type ArtifactKind = typeof ArtifactKinds[keyof typeof ArtifactKinds]
export interface IArtifact {
reportKey: ArtifactKind
uploadUrl: string
filePath: string
fileSize: number | bigint
upload: () => Promise<ArtifactUploadResult>
}
export interface ArtifactUploadResult {
success: boolean
error?: Error | string
url: string
pathToFile: string
fileSize?: number | bigint
key: ArtifactKind
errorStack?: string
allErrors?: Error[]
specAccess?: {
offset: number
size: number
}
uploadDuration?: number
}
export type ArtifactUploadStrategy<T> = (filePath: string, uploadUrl: string, fileSize: number | bigint) => T
export class Artifact<T extends ArtifactUploadStrategy<UploadResponse>, UploadResponse extends Promise<any> = Promise<{}>> {
constructor (
public reportKey: ArtifactKind,
public readonly filePath: string,
public readonly uploadUrl: string,
public readonly fileSize: number | bigint,
private uploadStrategy: T,
) {
}
public async upload (): Promise<ArtifactUploadResult> {
const startTime = performance.now()
this.debug('upload starting')
try {
const response = await this.uploadStrategy(this.filePath, this.uploadUrl, this.fileSize)
this.debug('upload succeeded: %O', response)
return this.composeSuccessResult(response ?? {}, performance.now() - startTime)
} catch (e) {
this.debug('upload failed: %O', e)
return this.composeFailureResult(e, performance.now() - startTime)
}
}
private debug (formatter: string = '', ...args: (string | object | number)[]) {
if (!debug.enabled) return
debug(`%s: %s -> %s (%dB) ${formatter}`, this.reportKey, this.filePath, this.uploadUrl, this.fileSize, ...args)
}
private commonResultFields (): Pick<ArtifactUploadResult, 'url' | 'pathToFile' | 'fileSize' | 'key'> {
return {
key: this.reportKey,
url: this.uploadUrl,
pathToFile: this.filePath,
fileSize: this.fileSize,
}
}
protected composeSuccessResult<T extends Object = {}> (response: T, uploadDuration: number): ArtifactUploadResult {
return {
...response,
...this.commonResultFields(),
success: true,
uploadDuration,
}
}
protected composeFailureResult<T extends Error> (err: T, uploadDuration: number): ArtifactUploadResult {
const errorReport = isAggregateError(err) ? {
error: err.errors[err.errors.length - 1].message,
errorStack: err.errors[err.errors.length - 1].stack,
allErrors: err.errors,
} : {
error: err.message,
errorStack: err.stack,
}
return {
...errorReport,
...this.commonResultFields(),
success: false,
uploadDuration,
}
}
}

View File

@@ -0,0 +1,6 @@
import { sendFile } from '../upload/send_file'
import type { ArtifactUploadStrategy } from './artifact'
export const fileUploadStrategy: ArtifactUploadStrategy<Promise<any>> = (filePath, uploadUrl) => {
return sendFile(filePath, uploadUrl)
}

View File

@@ -0,0 +1,61 @@
import fs from 'fs/promises'
import type { ProtocolManager } from '../protocol'
import { IArtifact, ArtifactUploadStrategy, ArtifactUploadResult, Artifact, ArtifactKinds } from './artifact'
interface ProtocolUploadStrategyResult {
success: boolean
fileSize: number | bigint
specAccess: {
offset: number
size: number
}
}
const createProtocolUploadStrategy = (protocolManager: ProtocolManager) => {
const strategy: ArtifactUploadStrategy<Promise<ProtocolUploadStrategyResult | {}>> =
async (filePath, uploadUrl, fileSize) => {
const fatalError = protocolManager.getFatalError()
if (fatalError) {
throw fatalError.error
}
const res = await protocolManager.uploadCaptureArtifact({ uploadUrl, fileSize, filePath })
return res ?? {}
}
return strategy
}
export const createProtocolArtifact = async (filePath: string, uploadUrl: string, protocolManager: ProtocolManager): Promise<IArtifact> => {
const { size } = await fs.stat(filePath)
return new Artifact('protocol', filePath, uploadUrl, size, createProtocolUploadStrategy(protocolManager))
}
export const composeProtocolErrorReportFromOptions = async ({
protocolManager,
protocolCaptureMeta,
captureUploadUrl,
}: {
protocolManager?: ProtocolManager
protocolCaptureMeta: { url?: string, disabledMessage?: string }
captureUploadUrl?: string
}): Promise<ArtifactUploadResult> => {
const url = captureUploadUrl || protocolCaptureMeta.url
const pathToFile = protocolManager?.getArchivePath()
const fileSize = pathToFile ? (await fs.stat(pathToFile))?.size : 0
const fatalError = protocolManager?.getFatalError()
return {
key: ArtifactKinds.PROTOCOL,
url: url ?? 'UNKNOWN',
pathToFile: pathToFile ?? 'UNKNOWN',
fileSize,
success: false,
error: fatalError?.error.message || 'UNKNOWN',
errorStack: fatalError?.error.stack || 'UNKNOWN',
}
}

View File

@@ -0,0 +1,44 @@
import fs from 'fs/promises'
import Debug from 'debug'
import { Artifact, IArtifact, ArtifactKinds } from './artifact'
import { fileUploadStrategy } from './file_upload_strategy'
const debug = Debug('cypress:server:cloud:artifacts:screenshot')
const createScreenshotArtifact = async (filePath: string, uploadUrl: string): Promise<IArtifact | undefined> => {
try {
const { size } = await fs.stat(filePath)
return new Artifact(ArtifactKinds.SCREENSHOTS, filePath, uploadUrl, size, fileUploadStrategy)
} catch (e) {
debug('Error creating screenshot artifact: %O', e)
return
}
}
export const createScreenshotArtifactBatch = (
screenshotUploadUrls: {screenshotId: string, uploadUrl: string}[],
screenshotFiles: {screenshotId: string, path: string}[],
): Promise<IArtifact[]> => {
const correlatedPaths = screenshotUploadUrls.map(({ screenshotId, uploadUrl }) => {
const correlatedFilePath = screenshotFiles.find((pathPair) => {
return pathPair.screenshotId === screenshotId
})?.path
return correlatedFilePath ? {
filePath: correlatedFilePath,
uploadUrl,
} : undefined
}).filter((pair): pair is { filePath: string, uploadUrl: string } => {
return !!pair
})
return Promise.all(correlatedPaths.map(({ filePath, uploadUrl }) => {
return createScreenshotArtifact(filePath, uploadUrl)
})).then((artifacts) => {
return artifacts.filter((artifact): artifact is IArtifact => {
return !!artifact
})
})
}

View File

@@ -0,0 +1,204 @@
import Debug from 'debug'
import type ProtocolManager from '../protocol'
import api from '../api'
import { logUploadManifest, logUploadResults, beginUploadActivityOutput } from '../../util/print-run'
import type { UpdateInstanceArtifactsPayload, ArtifactMetadata, ProtocolMetadata } from '../api'
import * as errors from '../../errors'
import exception from '../exception'
import { IArtifact, ArtifactUploadResult, ArtifactKinds } from './artifact'
import { createScreenshotArtifactBatch } from './screenshot_artifact'
import { createVideoArtifact } from './video_artifact'
import { createProtocolArtifact, composeProtocolErrorReportFromOptions } from './protocol_artifact'
const debug = Debug('cypress:server:cloud:artifacts')
const toUploadReportPayload = (acc: {
screenshots: ArtifactMetadata[]
video?: ArtifactMetadata
protocol?: ProtocolMetadata
}, { key, ...report }: ArtifactUploadResult): UpdateInstanceArtifactsPayload => {
if (key === ArtifactKinds.PROTOCOL) {
let { error, errorStack, allErrors } = report
if (allErrors) {
error = `Failed to upload Test Replay after ${allErrors.length} attempts. Errors: ${allErrors.map((error) => error.message).join(', ')}`
errorStack = allErrors.map((error) => error.stack).join(', ')
} else if (error) {
error = `Failed to upload Test Replay: ${error}`
}
debug('protocol report %O', report)
return {
...acc,
protocol: {
...report,
error,
errorStack,
},
}
}
return {
...acc,
[key]: (key === 'screenshots') ? [...acc.screenshots, report] : report,
}
}
type UploadArtifactOptions = {
protocolManager?: ProtocolManager
videoUploadUrl?: string
video?: string // filepath to the video artifact
screenshots?: {
screenshotId: string
path: string
}[]
screenshotUploadUrls?: {
screenshotId: string
uploadUrl: string
}[]
captureUploadUrl?: string
protocolCaptureMeta: {
url?: string
disabledMessage?: string
}
quiet?: boolean
runId: string
instanceId: string
spec: any
platform: any
projectId: any
}
const extractArtifactsFromOptions = async ({
video, videoUploadUrl, screenshots, screenshotUploadUrls, captureUploadUrl, protocolCaptureMeta, protocolManager,
}: Pick<UploadArtifactOptions,
'video' | 'videoUploadUrl' |
'screenshots' | 'screenshotUploadUrls' |
'captureUploadUrl' | 'protocolManager' | 'protocolCaptureMeta'
>): Promise<IArtifact[]> => {
const artifacts: IArtifact[] = []
if (videoUploadUrl && video) {
try {
artifacts.push(await createVideoArtifact(video, videoUploadUrl))
} catch (e) {
debug('Error creating video artifact: %O', e)
}
}
debug('screenshot metadata: %O', { screenshotUploadUrls, screenshots })
debug('found screenshot filenames: %o', screenshots)
if (screenshots?.length && screenshotUploadUrls?.length) {
const screenshotArtifacts = await createScreenshotArtifactBatch(screenshotUploadUrls, screenshots)
screenshotArtifacts.forEach((screenshot) => {
artifacts.push(screenshot)
})
}
try {
const protocolFilePath = protocolManager?.getArchivePath()
const protocolUploadUrl = captureUploadUrl || protocolCaptureMeta.url
debug('should add protocol artifact? %o, %o, %O', protocolFilePath, protocolUploadUrl, protocolManager)
if (protocolManager && protocolFilePath && protocolUploadUrl) {
artifacts.push(await createProtocolArtifact(protocolFilePath, protocolUploadUrl, protocolManager))
}
} catch (e) {
debug('Error creating protocol artifact: %O', e)
}
return artifacts
}
export const uploadArtifacts = async (options: UploadArtifactOptions) => {
const { protocolManager, protocolCaptureMeta, quiet, runId, instanceId, spec, platform, projectId } = options
const priority = {
[ArtifactKinds.VIDEO]: 0,
[ArtifactKinds.SCREENSHOTS]: 1,
[ArtifactKinds.PROTOCOL]: 2,
}
const artifacts = (await extractArtifactsFromOptions(options)).sort((a, b) => {
return priority[a.reportKey] - priority[b.reportKey]
})
let uploadReport: UpdateInstanceArtifactsPayload
if (!quiet) {
logUploadManifest(artifacts, protocolCaptureMeta, protocolManager?.getFatalError())
}
debug('preparing to upload artifacts: %O', artifacts)
let stopUploadActivityOutput: () => void | undefined
if (!quiet && artifacts.length) {
stopUploadActivityOutput = beginUploadActivityOutput()
}
try {
const uploadResults = await Promise.all(artifacts.map((artifact) => artifact.upload())).finally(() => {
if (stopUploadActivityOutput) {
stopUploadActivityOutput()
}
})
if (!quiet && uploadResults.length) {
logUploadResults(uploadResults, protocolManager?.getFatalError())
}
const protocolFatalError = protocolManager?.getFatalError()
/**
* Protocol instances with fatal errors prior to uploading will not have an uploadResult,
* but we still want to report them to updateInstanceArtifacts
*/
if (!uploadResults.find((result: ArtifactUploadResult) => {
return result.key === ArtifactKinds.PROTOCOL
}) && protocolFatalError) {
uploadResults.push(await composeProtocolErrorReportFromOptions(options))
}
uploadReport = uploadResults.reduce(toUploadReportPayload, { video: undefined, screenshots: [], protocol: undefined })
} catch (err) {
errors.warning('CLOUD_CANNOT_UPLOAD_ARTIFACTS', err)
return exception.create(err)
}
debug('checking for protocol errors', protocolManager?.hasErrors())
if (protocolManager) {
try {
await protocolManager.reportNonFatalErrors({
specName: spec.name,
osName: platform.osName,
projectSlug: projectId,
})
} catch (err) {
debug('Failed to send protocol errors %O', err)
}
}
try {
debug('upload report: %O', uploadReport)
const res = await api.updateInstanceArtifacts({
runId, instanceId,
}, uploadReport)
return res
} catch (err) {
debug('failed updating artifact status %o', {
stack: err.stack,
})
errors.warning('CLOUD_CANNOT_UPLOAD_ARTIFACTS_PROTOCOL', err)
if (err.statusCode !== 503) {
return exception.create(err)
}
}
}

View File

@@ -0,0 +1,9 @@
import fs from 'fs/promises'
import { Artifact, IArtifact, ArtifactKinds } from './artifact'
import { fileUploadStrategy } from './file_upload_strategy'
export const createVideoArtifact = async (filePath: string, uploadUrl: string): Promise<IArtifact> => {
const { size } = await fs.stat(filePath)
return new Artifact(ArtifactKinds.VIDEO, filePath, uploadUrl, size, fileUploadStrategy)
}

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'
const Promise = require('bluebird')
const pkg = require('@packages/root')
const api = require('./api')
const api = require('./api').default
const user = require('./user')
const system = require('../util/system')

View File

@@ -259,6 +259,10 @@ export class ProtocolManager implements ProtocolManagerShape {
return this._errors.filter((e) => !e.fatal)
}
getArchivePath (): string | undefined {
return this._archivePath
}
async getArchiveInfo (): Promise<{ filePath: string, fileSize: number } | void> {
const archivePath = this._archivePath
@@ -275,9 +279,9 @@ export class ProtocolManager implements ProtocolManagerShape {
async uploadCaptureArtifact ({ uploadUrl, fileSize, filePath }: CaptureArtifact): Promise<{
success: boolean
fileSize: number
specAccess?: ReturnType<AppCaptureProtocolInterface['getDbMetadata']>
} | void> {
fileSize: number | bigint
specAccess: ReturnType<AppCaptureProtocolInterface['getDbMetadata']>
} | undefined> {
if (!this._protocol || !filePath || !this._db) {
debug('not uploading due to one of the following being falsy: %O', {
_protocol: !!this._protocol,
@@ -296,7 +300,7 @@ export class ProtocolManager implements ProtocolManagerShape {
return {
fileSize,
success: true,
specAccess: this._protocol?.getDbMetadata(),
specAccess: this._protocol.getDbMetadata(),
}
} catch (e) {
if (CAPTURE_ERRORS) {
@@ -304,10 +308,13 @@ export class ProtocolManager implements ProtocolManagerShape {
error: e,
captureMethod: 'uploadCaptureArtifact',
fatal: true,
isUploadError: true,
})
throw e
}
return
} finally {
if (DELETE_DB) {
await fs.unlink(filePath).catch((e) => {

View File

@@ -1,16 +0,0 @@
const rp = require('@cypress/request-promise')
const { fs } = require('../util/fs')
export = {
send (pathToFile: string, url: string) {
return fs
.readFileAsync(pathToFile)
.then((buf) => {
return rp({
url,
method: 'PUT',
body: buf,
})
})
},
}

View File

@@ -0,0 +1,14 @@
const rp = require('@cypress/request-promise')
const { fs } = require('../../util/fs')
export const sendFile = (filePath: string, uploadUrl: string) => {
return fs
.readFileAsync(filePath)
.then((buf) => {
return rp({
url: uploadUrl,
method: 'PUT',
body: buf,
})
})
}

View File

@@ -1,4 +1,4 @@
const api = require('./api')
const api = require('./api').default
const cache = require('../cache')
import type { CachedUser } from '@packages/types'
@@ -25,6 +25,8 @@ export = {
if (authToken) {
return api.postLogout(authToken)
}
return undefined
})
})
},

View File

@@ -1,7 +1,6 @@
const _ = require('lodash')
const path = require('path')
const la = require('lazy-ass')
const chalk = require('chalk')
const check = require('check-more-types')
const debug = require('debug')('cypress:server:record')
const debugCiInfo = require('debug')('cypress:server:record:ci-info')
@@ -12,21 +11,19 @@ const { telemetry } = require('@packages/telemetry')
const { hideKeys } = require('@packages/config')
const api = require('../cloud/api')
const api = require('../cloud/api').default
const exception = require('../cloud/exception')
const upload = require('../cloud/upload')
const errors = require('../errors')
const capture = require('../capture')
const Config = require('../config')
const env = require('../util/env')
const terminal = require('../util/terminal')
const ciProvider = require('../util/ci_provider')
const { printPendingArtifactUpload, printCompletedArtifactUpload, beginUploadActivityOutput } = require('../util/print-run')
const testsUtils = require('../util/tests_utils')
const specWriter = require('../util/spec_writer')
const { fs } = require('../util/fs')
const { performance } = require('perf_hooks')
const { uploadArtifacts } = require('../cloud/artifacts/upload_artifacts')
// dont yell about any errors either
const runningInternalTests = () => {
@@ -138,365 +135,6 @@ returns:
]
*/
const uploadArtifactBatch = async (artifacts, protocolManager, quiet) => {
const priority = {
'video': 0,
'screenshots': 1,
'protocol': 2,
}
const labels = {
'video': 'Video',
'screenshots': 'Screenshot',
'protocol': 'Test Replay',
}
artifacts.sort((a, b) => {
return priority[a.reportKey] - priority[b.reportKey]
})
const preparedArtifacts = await Promise.all(artifacts.map(async (artifact) => {
if (artifact.skip) {
return artifact
}
if (artifact.reportKey === 'protocol') {
try {
if (protocolManager.hasFatalError()) {
const error = protocolManager.getFatalError().error
debug('protocol fatal error encountered', {
message: error.message,
captureMethod: error.captureMethod,
stack: error.stack,
})
return {
...artifact,
skip: true,
error: error.message || 'Unknown Error',
errorStack: error.stack || 'Unknown Stack',
}
}
const archiveInfo = await protocolManager.getArchiveInfo()
if (archiveInfo === undefined) {
return {
...artifact,
skip: true,
error: 'No test data recorded',
}
}
return {
...artifact,
...archiveInfo,
}
} catch (err) {
debug('failed to prepare protocol artifact', {
error: err.message,
stack: err.stack,
})
return {
...artifact,
skip: true,
error: err.message,
errorStack: err.stack,
}
}
}
if (artifact.filePath) {
try {
const { size } = await fs.statAsync(artifact.filePath)
return {
...artifact,
fileSize: size,
}
} catch (err) {
debug('failed to get stats for upload artifact %o', {
file: artifact.filePath,
stack: err.stack,
})
return {
...artifact,
skip: true,
error: err.message,
errorStack: err.stack,
}
}
}
return artifact
}))
if (!quiet) {
// eslint-disable-next-line no-console
console.log('')
terminal.header('Uploading Cloud Artifacts', {
color: ['blue'],
})
// eslint-disable-next-line no-console
console.log('')
}
preparedArtifacts.forEach((artifact) => {
debug('preparing to upload artifact %O', {
...artifact,
payload: typeof artifact.payload,
})
if (!quiet) {
printPendingArtifactUpload(artifact, labels)
}
})
let stopUploadActivityOutput
if (!quiet && preparedArtifacts.filter(({ skip }) => !skip).length) {
stopUploadActivityOutput = beginUploadActivityOutput()
}
const uploadResults = await Promise.all(
preparedArtifacts.map(async (artifact) => {
if (artifact.skip) {
debug('nothing to upload for artifact %O', artifact)
return {
key: artifact.reportKey,
skipped: true,
url: artifact.uploadUrl,
...(artifact.error && {
error: artifact.error,
errorStack: artifact.errorStack,
success: false,
}),
}
}
const startTime = performance.now()
debug('uploading artifact %O', {
...artifact,
payload: typeof artifact.payload,
})
try {
if (artifact.reportKey === 'protocol') {
const res = await protocolManager.uploadCaptureArtifact(artifact)
return {
...res,
pathToFile: 'Test Replay',
url: artifact.uploadUrl,
fileSize: artifact.fileSize,
key: artifact.reportKey,
uploadDuration: performance.now() - startTime,
}
}
const res = await upload.send(artifact.filePath, artifact.uploadUrl)
return {
...res,
success: true,
url: artifact.uploadUrl,
pathToFile: artifact.filePath,
fileSize: artifact.fileSize,
key: artifact.reportKey,
uploadDuration: performance.now() - startTime,
}
} catch (err) {
debug('failed to upload artifact %o', {
file: artifact.filePath,
url: artifact.uploadUrl,
stack: err.stack,
})
if (err.errors) {
const lastError = _.last(err.errors)
return {
key: artifact.reportKey,
success: false,
error: lastError.message,
allErrors: err.errors,
url: artifact.uploadUrl,
pathToFile: artifact.filePath,
uploadDuration: performance.now() - startTime,
}
}
return {
key: artifact.reportKey,
success: false,
error: err.message,
errorStack: err.stack,
url: artifact.uploadUrl,
pathToFile: artifact.filePath,
uploadDuration: performance.now() - startTime,
}
}
}),
).finally(() => {
if (stopUploadActivityOutput) {
stopUploadActivityOutput()
}
})
const attemptedUploadResults = uploadResults.filter(({ skipped }) => {
return !skipped
})
if (!quiet && attemptedUploadResults.length) {
// eslint-disable-next-line no-console
console.log('')
terminal.header('Uploaded Cloud Artifacts', {
color: ['blue'],
})
// eslint-disable-next-line no-console
console.log('')
attemptedUploadResults.forEach(({ key, skipped, ...report }, i, { length }) => {
printCompletedArtifactUpload({ key, ...report }, labels, chalk.grey(`${i + 1}/${length}`))
})
}
return uploadResults.reduce((acc, { key, skipped, ...report }) => {
if (key === 'protocol') {
let { error, errorStack, allErrors } = report
if (allErrors) {
error = `Failed to upload Test Replay after ${allErrors.length} attempts. Errors: ${allErrors.map((error) => error.message).join(', ')}`
errorStack = allErrors.map((error) => error.stack).join(', ')
} else if (error) {
error = `Failed to upload Test Replay: ${error}`
}
return skipped && !report.error ? acc : {
...acc,
[key]: {
...report,
error,
errorStack,
},
}
}
return skipped ? acc : {
...acc,
[key]: (key === 'screenshots') ? [...acc.screenshots, report] : report,
}
}, {
video: undefined,
screenshots: [],
protocol: undefined,
})
}
const uploadArtifacts = async (options = {}) => {
const { protocolManager, video, screenshots, videoUploadUrl, captureUploadUrl, protocolCaptureMeta, screenshotUploadUrls, quiet, runId, instanceId, spec, platform, projectId } = options
const artifacts = []
if (videoUploadUrl) {
artifacts.push({
reportKey: 'video',
uploadUrl: videoUploadUrl,
filePath: video,
})
} else {
artifacts.push({
reportKey: 'video',
skip: true,
})
}
if (screenshotUploadUrls.length) {
screenshotUploadUrls.map(({ screenshotId, uploadUrl }) => {
const screenshot = _.find(screenshots, { screenshotId })
debug('screenshot: %o', screenshot)
return {
reportKey: 'screenshots',
uploadUrl,
filePath: screenshot.path,
}
}).forEach((screenshotArtifact) => {
artifacts.push(screenshotArtifact)
})
} else {
artifacts.push({
reportKey: 'screenshots',
skip: true,
})
}
debug('capture manifest: %O', { captureUploadUrl, protocolCaptureMeta, protocolManager })
if (protocolManager && (captureUploadUrl || (protocolCaptureMeta && protocolCaptureMeta.url))) {
artifacts.push({
reportKey: 'protocol',
uploadUrl: captureUploadUrl || protocolCaptureMeta.url,
})
} else if (protocolCaptureMeta && protocolCaptureMeta.disabledMessage) {
artifacts.push({
reportKey: 'protocol',
message: protocolCaptureMeta.disabledMessage,
skip: true,
})
}
let uploadReport
try {
uploadReport = await uploadArtifactBatch(artifacts, protocolManager, quiet)
} catch (err) {
errors.warning('CLOUD_CANNOT_UPLOAD_ARTIFACTS', err)
return exception.create(err)
}
debug('checking for protocol errors', protocolManager?.hasErrors())
if (protocolManager) {
try {
await protocolManager.reportNonFatalErrors({
specName: spec.name,
osName: platform.osName,
projectSlug: projectId,
})
} catch (err) {
debug('Failed to send protocol errors %O', err)
}
}
try {
debug('upload report: %O', uploadReport)
const res = await api.updateInstanceArtifacts({
runId, instanceId,
}, uploadReport)
return res
} catch (err) {
debug('failed updating artifact status %o', {
stack: err.stack,
})
errors.warning('CLOUD_CANNOT_UPLOAD_ARTIFACTS_PROTOCOL', err)
if (err.statusCode !== 503) {
return exception.create(err)
}
}
}
const updateInstanceStdout = async (options = {}) => {
const { runId, instanceId, captured } = options

View File

@@ -12,11 +12,12 @@ import env from './env'
import terminal from './terminal'
import { getIsCi } from './ci_provider'
import * as experiments from '../experiments'
import type { SpecFile } from '@packages/types'
import type { SpecFile, ProtocolError } from '@packages/types'
import type { Cfg } from '../project-base'
import type { Browser } from '../browsers/types'
import type { Table } from 'cli-table3'
import type { CypressRunResult } from '../modes/results'
import type { IArtifact, ArtifactUploadResult } from '../cloud/artifacts/artifact'
type Screenshot = {
width: number
@@ -572,30 +573,9 @@ const formatFileSize = (bytes: number) => {
return prettyBytes(bytes)
}
type ArtifactLike = {
reportKey: 'protocol' | 'screenshots' | 'video'
filePath?: string
fileSize?: number | BigInt
message?: string
skip?: boolean
error: string
}
export const printPendingArtifactUpload = <T extends ArtifactLike> (artifact: T, labels: Record<'protocol' | 'screenshots' | 'video', string>): void => {
export const printPendingArtifactUpload = (artifact: IArtifact, labels: Record<'protocol' | 'screenshots' | 'video', string>): void => {
process.stdout.write(` - ${labels[artifact.reportKey]} `)
if (artifact.skip) {
if (artifact.reportKey === 'protocol' && artifact.error) {
process.stdout.write(`- Failed Capturing - ${artifact.error}`)
} else {
process.stdout.write('- Nothing to upload ')
}
}
if (artifact.reportKey === 'protocol' && artifact.message) {
process.stdout.write(`- ${artifact.message}`)
}
if (artifact.fileSize) {
process.stdout.write(`- ${formatFileSize(Number(artifact.fileSize))}`)
}
@@ -607,25 +587,69 @@ export const printPendingArtifactUpload = <T extends ArtifactLike> (artifact: T,
process.stdout.write('\n')
}
type ArtifactUploadResultLike = {
pathToFile?: string
key: string
fileSize?: number | BigInt
success: boolean
error?: string
skipped?: boolean
uploadDuration?: number
export const printSkippedArtifact = (label: string, message: string = 'Nothing to upload', error?: string) => {
process.stdout.write(` - ${label} - ${message} `)
if (error) {
process.stdout.write(`- ${error}`)
}
process.stdout.write('\n')
}
export const printCompletedArtifactUpload = <T extends ArtifactUploadResultLike> (artifactUploadResult: T, labels: Record<'protocol' | 'screenshots' | 'video', string>, num: string): void => {
const { pathToFile, key, fileSize, success, error, skipped, uploadDuration } = artifactUploadResult
export const logUploadManifest = (artifacts: IArtifact[], protocolCaptureMeta: {
url?: string
disabledMessage?: string
}, protocolFatalError?: ProtocolError) => {
const labels = {
'video': 'Video',
'screenshots': 'Screenshot',
'protocol': 'Test Replay',
}
// eslint-disable-next-line no-console
console.log('')
terminal.header('Uploading Cloud Artifacts', {
color: ['blue'],
})
// eslint-disable-next-line no-console
console.log('')
const video = artifacts.find(({ reportKey }) => reportKey === 'video')
const screenshots = artifacts.filter(({ reportKey }) => reportKey === 'screenshots')
const protocol = artifacts.find(({ reportKey }) => reportKey === 'protocol')
if (video) {
printPendingArtifactUpload(video, labels)
} else {
printSkippedArtifact('Video')
}
if (screenshots.length) {
screenshots.forEach(((screenshot) => {
printPendingArtifactUpload(screenshot, labels)
}))
} else {
printSkippedArtifact('Screenshot')
}
// if protocolFatalError exists here, there is not a protocol artifact to attempt to upload
if (protocolFatalError) {
printSkippedArtifact('Test Replay', 'Failed Capturing', protocolFatalError.error.message)
} else if (protocol) {
if (!protocolFatalError) {
printPendingArtifactUpload(protocol, labels)
}
} else if (protocolCaptureMeta.disabledMessage) {
printSkippedArtifact('Test Replay', 'Nothing to upload', protocolCaptureMeta.disabledMessage)
}
}
export const printCompletedArtifactUpload = ({ pathToFile, key, fileSize, success, error, uploadDuration }: ArtifactUploadResult, labels: Record<'protocol' | 'screenshots' | 'video', string>, num: string): void => {
process.stdout.write(` - ${labels[key]} `)
if (success) {
process.stdout.write(`- Done Uploading ${formatFileSize(Number(fileSize))}`)
} else if (skipped) {
process.stdout.write(`- Nothing to Upload`)
} else {
process.stdout.write(`- Failed Uploading`)
}
@@ -649,6 +673,40 @@ export const printCompletedArtifactUpload = <T extends ArtifactUploadResultLike>
process.stdout.write('\n')
}
export const logUploadResults = (results: ArtifactUploadResult[], protocolFatalError: ProtocolError | undefined) => {
const labels = {
'video': 'Video',
'screenshots': 'Screenshot',
'protocol': 'Test Replay',
}
// if protocol did not attempt an upload due to a fatal error, there will still be an upload result - this is
// so we can report the failure properly to instance/artifacts. But, we do not want to display it here.
const trimmedResults = protocolFatalError && protocolFatalError.captureMethod !== 'uploadCaptureArtifact' ?
results.filter(((result) => {
return result.key !== 'protocol'
})) :
results
if (!trimmedResults.length) {
return
}
// eslint-disable-next-line no-console
console.log('')
terminal.header('Uploaded Cloud Artifacts', {
color: ['blue'],
})
// eslint-disable-next-line no-console
console.log('')
trimmedResults.forEach(({ key, ...report }, i, { length }) => {
printCompletedArtifactUpload({ key, ...report }, labels, chalk.grey(`${i + 1}/${length}`))
})
}
const UPLOAD_ACTIVITY_INTERVAL = typeof env.get('CYPRESS_UPLOAD_ACTIVITY_INTERVAL') === 'undefined' ? 15000 : env.get('CYPRESS_UPLOAD_ACTIVITY_INTERVAL')
export const beginUploadActivityOutput = () => {

View File

@@ -24,7 +24,7 @@ const ciProvider = require(`../../lib/util/ci_provider`)
const settings = require(`../../lib/util/settings`)
const Windows = require(`../../lib/gui/windows`)
const interactiveMode = require(`../../lib/modes/interactive`)
const api = require(`../../lib/cloud/api`)
const api = require(`../../lib/cloud/api`).default
const cwd = require(`../../lib/cwd`)
const user = require(`../../lib/cloud/user`)
const cache = require(`../../lib/cache`)

View File

@@ -13,7 +13,7 @@ const {
agent,
} = require('@packages/network')
const pkg = require('@packages/root')
const api = require('../../../../lib/cloud/api')
const api = require('../../../../lib/cloud/api').default
const cache = require('../../../../lib/cache')
const errors = require('../../../../lib/errors')
const machineId = require('../../../../lib/cloud/machine_id')
@@ -237,7 +237,7 @@ describe('lib/cloud/api', () => {
if (!prodApi) {
prodApi = stealthyRequire(require.cache, () => {
return require('../../../../lib/cloud/api')
return require('../../../../lib/cloud/api').default
}, () => {
require('../../../../lib/cloud/encryption')
}, module)

View File

@@ -2,7 +2,7 @@ require('../../spec_helper')
delete global.fs
const api = require('../../../lib/cloud/api')
const api = require('../../../lib/cloud/api').default
const user = require('../../../lib/cloud/user')
const exception = require('../../../lib/cloud/exception')
const system = require('../../../lib/util/system')

View File

@@ -1,6 +1,6 @@
require('../../spec_helper')
const api = require('../../../lib/cloud/api')
const api = require('../../../lib/cloud/api').default
const cache = require('../../../lib/cache')
const user = require('../../../lib/cloud/user')

View File

@@ -6,7 +6,7 @@ const commitInfo = require('@cypress/commit-info')
const mockedEnv = require('mocked-env')
const errors = require(`../../../lib/errors`)
const api = require(`../../../lib/cloud/api`)
const api = require(`../../../lib/cloud/api`).default
const exception = require(`../../../lib/cloud/exception`)
const recordMode = require(`../../../lib/modes/record`)
const ciProvider = require(`../../../lib/util/ci_provider`)

View File

@@ -49,6 +49,7 @@ export interface ProtocolError {
captureMethod: ProtocolCaptureMethod
fatal?: boolean
runnableId?: string
isUploadError?: boolean
}
type ProtocolErrorReportEntry = Omit<ProtocolError, 'fatal' | 'error'> & {
@@ -74,7 +75,7 @@ export type ProtocolErrorReport = {
export type CaptureArtifact = {
uploadUrl: string
fileSize: number
fileSize: number | bigint
filePath: string
}
@@ -90,7 +91,7 @@ export interface ProtocolManagerShape extends AppCaptureProtocolCommon {
setupProtocol(script: string, options: ProtocolManagerOptions): Promise<void>
beforeSpec (spec: { instanceId: string }): void
reportNonFatalErrors (clientMetadata: any): Promise<void>
uploadCaptureArtifact(artifact: CaptureArtifact, timeout?: number): Promise<{ fileSize: number, success: boolean, error?: string } | void>
uploadCaptureArtifact(artifact: CaptureArtifact, timeout?: number): Promise<{ fileSize: number | bigint, success: boolean, error?: string } | void>
}
type Response = {