diff --git a/package-lock.json b/package-lock.json index 8fc32fd1..52e6f34e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9997,6 +9997,14 @@ "dev": true, "license": "MIT" }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-absolute": { "version": "1.0.0", "license": "MIT", @@ -16136,6 +16144,7 @@ "lil-http-terminator": "^1.2.3", "lodash": "^4.17.21", "nanoid": "^3.3.4", + "plur": "^4.0.0", "pretty-ms": "^7.0.1", "source-map-support": "^0.5.21", "table": "^6.8.1", @@ -17293,6 +17302,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cypress-cloud/node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cypress-cloud/node_modules/proxy-agent": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", @@ -22129,6 +22152,7 @@ "lodash": "^4.17.21", "nanoid": "^3.3.4", "nock": "^13.2.9", + "plur": "4", "pretty-ms": "^7.0.1", "release-it": "^16.1.5", "rimraf": "^3.0.2", @@ -22948,6 +22972,14 @@ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true }, + "plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "requires": { + "irregular-plurals": "^3.2.0" + } + }, "proxy-agent": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", @@ -24765,6 +24797,11 @@ "version": "1.1.8", "dev": true }, + "irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==" + }, "is-absolute": { "version": "1.0.0", "requires": { diff --git a/packages/cypress-cloud/lib/artifacts.ts b/packages/cypress-cloud/lib/artifacts.ts index ed6accc5..ca3dc478 100644 --- a/packages/cypress-cloud/lib/artifacts.ts +++ b/packages/cypress-cloud/lib/artifacts.ts @@ -2,10 +2,12 @@ import Debug from "debug"; import { ScreenshotArtifact, ScreenshotUploadInstruction } from "../types"; import { updateInstanceStdout } from "./api"; import { safe } from "./lang"; -import { warn } from "./log"; +import { dim } from "./log"; +import { ExecutionState } from "./state"; import { uploadImage, uploadJson, uploadVideo } from "./upload"; const debug = Debug("currents:artifacts"); interface UploadArtifacts { + executionState: ExecutionState; videoPath: string | null; videoUploadUrl?: string | null; screenshots: ScreenshotArtifact[]; @@ -14,6 +16,7 @@ interface UploadArtifacts { coverageFilePath?: string | null; } export async function uploadArtifacts({ + executionState, videoPath, videoUploadUrl, screenshots, @@ -21,8 +24,6 @@ export async function uploadArtifacts({ coverageFilePath, coverageUploadUrl, }: UploadArtifacts) { - // title("blue", "Uploading Results"); - debug("uploading artifacts: %o", { videoPath, videoUploadUrl, @@ -33,9 +34,8 @@ export async function uploadArtifacts({ }); const totalUploads = - (videoPath ? 1 : 0) + screenshots.length + (coverageFilePath ? 1 : 0); + (videoPath ? 1 : 0) + screenshots.length + (coverageUploadUrl ? 1 : 0); if (totalUploads === 0) { - // info("Nothing to upload"); return; } @@ -43,7 +43,12 @@ export async function uploadArtifacts({ if (videoUploadUrl && videoPath) { await safe( uploadVideo, - (e) => debug("failed uploading video %s. Error: %o", videoPath, e), + (e) => { + debug("failed uploading video %s. Error: %o", videoPath, e); + executionState.addWarning( + `Failed uploading video ${videoPath}.\n${dim(e)}` + ); + }, () => debug("success uploading", videoPath) )(videoPath, videoUploadUrl); } @@ -60,17 +65,23 @@ export async function uploadArtifacts({ screenshot, screenshotUploadUrls ); - warn("Cannot find upload url for screenshot: %s", screenshot.path); + executionState.addWarning( + `No upload URL for screenshot ${screenshot.path}` + ); return Promise.resolve(); } return safe( uploadImage, - (e) => + (e) => { debug( "failed uploading screenshot %s. Error: %o", screenshot.path, e - ), + ); + executionState.addWarning( + `Failed uploading screenshot ${screenshot.path}.\n${dim(e)}` + ); + }, () => debug("success uploading", screenshot.path) )(screenshot.path, url); }) @@ -80,12 +91,18 @@ export async function uploadArtifacts({ if (coverageUploadUrl && coverageFilePath) { await safe( uploadJson, - (e) => + (e) => { debug( "failed uploading coverage file %s. Error: %o", coverageFilePath, e - ), + ); + + executionState.addWarning( + `Failed uploading coverage file ${coverageFilePath}.\n${dim(e)}` + ); + }, + () => debug("success uploading", coverageFilePath) )(coverageFilePath, coverageUploadUrl); } diff --git a/packages/cypress-cloud/lib/coverage/index.ts b/packages/cypress-cloud/lib/coverage/index.ts index 5ad7cc86..be881ae6 100644 --- a/packages/cypress-cloud/lib/coverage/index.ts +++ b/packages/cypress-cloud/lib/coverage/index.ts @@ -1,6 +1,5 @@ -import { join } from "path"; import fs from "fs/promises"; -import { warn } from "../log"; +import { join } from "path"; export const getCoverageFilePath = async ( coverageFile = "./.nyc_output/out.json" @@ -9,12 +8,14 @@ export const getCoverageFilePath = async ( try { await fs.access(path); - return path; + return { + path, + error: false, + }; } catch (error) { - warn( - 'Coverage file was not found at "%s". Coverage recording will be skipped.', - path - ); - return null; + return { + path, + error, + }; } }; diff --git a/packages/cypress-cloud/lib/log.ts b/packages/cypress-cloud/lib/log.ts index 6f0d6ac3..87d1f70e 100644 --- a/packages/cypress-cloud/lib/log.ts +++ b/packages/cypress-cloud/lib/log.ts @@ -4,6 +4,7 @@ import util from "util"; const log = (...args: unknown[]) => console.log(util.format(...args)); export const info = log; +export const format = util.format; export const withError = (msg: string) => chalk.bgRed.white(" ERROR ") + " " + msg; @@ -37,3 +38,5 @@ export const gray = chalk.gray; export const white = chalk.white; export const magenta = chalk.magenta; export const bold = chalk.bold; +export const yellow = chalk.yellow; +export const dim = chalk.dim; diff --git a/packages/cypress-cloud/lib/results/uploadResults.ts b/packages/cypress-cloud/lib/results/uploadResults.ts index 65955d7c..61443eac 100644 --- a/packages/cypress-cloud/lib/results/uploadResults.ts +++ b/packages/cypress-cloud/lib/results/uploadResults.ts @@ -10,15 +10,18 @@ import { uploadArtifacts, uploadStdoutSafe } from "../artifacts"; import { setCancellationReason } from "../cancellation"; import { getInitialOutput } from "../capture"; import { isCurrents } from "../env"; +import { ConfigState, ExecutionState } from "../state"; import { getInstanceResultPayload, getInstanceTestsPayload } from "./results"; const debug = Debug("currents:results"); export async function getReportResultsTask( instanceId: string, - results: CypressCommandLine.CypressRunResult, + configState: ConfigState, + executionState: ExecutionState, stdout: string, coverageFilePath?: string ) { + const results = executionState.getInstanceResults(configState, instanceId); const run = results.runs[0]; if (!run) { throw new Error("No run found in Cypress results"); @@ -41,6 +44,7 @@ export async function getReportResultsTask( return Promise.all([ uploadArtifacts({ + executionState, videoUploadUrl, videoPath: run.video, screenshotUploadUrls, diff --git a/packages/cypress-cloud/lib/run.ts b/packages/cypress-cloud/lib/run.ts index 5e7cf209..c0e48cfe 100644 --- a/packages/cypress-cloud/lib/run.ts +++ b/packages/cypress-cloud/lib/run.ts @@ -1,6 +1,7 @@ import "./init"; import Debug from "debug"; +import plur from "plur"; import { getLegalNotice } from "../legal"; import { CurrentsRunParameters } from "../types"; import { createRun } from "./api"; @@ -12,12 +13,23 @@ import { preprocessParams, validateParams, } from "./config"; +import { getCoverageFilePath } from "./coverage"; import { runBareCypress } from "./cypress"; import { activateDebug } from "./debug"; import { isCurrents } from "./env"; import { getGitInfo } from "./git"; import { setAPIBaseUrl, setRunId } from "./httpClient"; -import { bold, divider, info, spacer, title } from "./log"; +import { + bold, + dim, + divider, + format, + info, + spacer, + title, + warn, + yellow, +} from "./log"; import { getPlatform } from "./platform"; import { pubsub } from "./pubsub"; import { summarizeTestResults, summaryTable } from "./results"; @@ -30,7 +42,6 @@ import { shutdown } from "./shutdown"; import { getSpecFiles } from "./specMatcher"; import { ConfigState, ExecutionState } from "./state"; import { startWSS } from "./ws"; -import { getCoverageFilePath } from "./coverage"; const debug = Debug("currents:run"); @@ -144,7 +155,10 @@ export async function run(params: CurrentsRunParameters = {}) { title("white", "Cloud Run Finished"); console.log(summaryTable(_summary)); - info("šŸ Recorded Run:", bold(run.runUrl)); + + printWarnings(executionState); + + info("\nšŸ Recorded Run:", bold(run.runUrl)); await shutdown(); @@ -176,15 +190,34 @@ function listenToSpecEvents( debug("after:spec %o %o", spec, results); executionState.setSpecAfter(spec.relative, results); executionState.setSpecOutput(spec.relative, getCapturedOutput()); + if (experimentalCoverageRecording) { - const coverageFilePath = await getCoverageFilePath( + const { path, error } = await getCoverageFilePath( config?.env?.coverageFile ); - if (coverageFilePath) { - executionState.setSpecCoverage(spec.relative, coverageFilePath); + if (!error) { + executionState.setSpecCoverage(spec.relative, path); + } else { + executionState.addWarning( + format(`Could not process coverage file "%s"\n${dim(error)}`, path) + ); } } createReportTaskSpec(configState, executionState, spec.relative); } ); } + +function printWarnings(executionState: ExecutionState) { + const warnings = Array.from(executionState.getWarnings()); + if (warnings.length > 0) { + warn( + `${warnings.length} ${plur( + "Warning", + warnings.length + )} encountered during the execution:\n${warnings + .map((w, i) => `\n${yellow(`[${i + 1}/${warnings.length}]`)} ${w}`) + .join("\n")}` + ); + } +} diff --git a/packages/cypress-cloud/lib/runner/reportTask.ts b/packages/cypress-cloud/lib/runner/reportTask.ts index 5f7dcc0d..6be3bae8 100644 --- a/packages/cypress-cloud/lib/runner/reportTask.ts +++ b/packages/cypress-cloud/lib/runner/reportTask.ts @@ -29,7 +29,8 @@ export const createReportTask = ( reportTasks.push( getReportResultsTask( instanceId, - executionState.getInstanceResults(configState, instanceId), + configState, + executionState, instance.output ?? "no output captured", instance.coverageFilePath ).catch(error) diff --git a/packages/cypress-cloud/lib/state/execution.ts b/packages/cypress-cloud/lib/state/execution.ts index 50a8a083..e149112b 100644 --- a/packages/cypress-cloud/lib/state/execution.ts +++ b/packages/cypress-cloud/lib/state/execution.ts @@ -26,8 +26,17 @@ type InstanceExecutionState = { }; export class ExecutionState { + private warnings = new Set(); private state: Record = {}; + public getWarnings() { + return this.warnings; + } + + public addWarning(warning: string) { + this.warnings.add(warning); + } + public getResults(configState: ConfigState) { return Object.values(this.state).map((i) => this.getInstanceResults(configState, i.instanceId) diff --git a/packages/cypress-cloud/package.json b/packages/cypress-cloud/package.json index e357709f..fe779e51 100644 --- a/packages/cypress-cloud/package.json +++ b/packages/cypress-cloud/package.json @@ -69,6 +69,7 @@ "lil-http-terminator": "^1.2.3", "lodash": "^4.17.21", "nanoid": "^3.3.4", + "plur": "^4.0.0", "pretty-ms": "^7.0.1", "source-map-support": "^0.5.21", "table": "^6.8.1",