diff --git a/packages/cypress-cloud/lib/cypress/cypress.ts b/packages/cypress-cloud/lib/cypress/cypress.ts index 5bb1aa10..5ad3c30e 100644 --- a/packages/cypress-cloud/lib/cypress/cypress.ts +++ b/packages/cypress-cloud/lib/cypress/cypress.ts @@ -8,6 +8,7 @@ import Debug from "debug"; import _ from "lodash"; import { getCypressRunAPIParams } from "../config"; import { safe } from "../lang"; +import { warn } from "../log"; import { getWSSPort } from "../ws"; const debug = Debug("currents:cypress"); @@ -55,24 +56,44 @@ export async function runSpecFile( debug("running cypress with options %o", options); const result = await cypress.run(options); + if (result.status === "failed") { + warn('Cypress runner failed with message: "%s"', result.message); + warn( + "The following spec files will be marked as failed: %s", + spec + .split(",") + .map((i) => `\n - ${i}`) + .join("") + ); + } debug("cypress run result %o", result); return result; } export const runSpecFileSafe = ( - ...args: Parameters + spec: RunCypressSpecFile, + cypressRunOptions: ValidatedCurrentsParameters ): Promise => safe( runSpecFile, (error) => { + const message = `Cypress runnner crashed with an error:\n${ + (error as Error).message + }\n${(error as Error).stack}}`; debug("cypress run exception %o", error); + warn('Cypress runner crashed: "%s"', message); + warn( + "The following spec files will be marked as failed: %s", + spec.spec + .split(",") + .map((i) => `\n - ${i}`) + .join("") + ); return { status: "failed" as const, failures: 1, - message: `Cypress process crashed with an error:\n${ - (error as Error).message - }\n${(error as Error).stack}}`, + message, }; }, () => {} - )(...args); + )(spec, cypressRunOptions); diff --git a/packages/cypress-cloud/lib/results/results.ts b/packages/cypress-cloud/lib/results/results.ts index dbc1a4ea..60db0143 100644 --- a/packages/cypress-cloud/lib/results/results.ts +++ b/packages/cypress-cloud/lib/results/results.ts @@ -8,6 +8,7 @@ import { UpdateInstanceResultsPayload, } from "../api"; import { MergedConfig } from "../config"; +import { getConfig } from "../runner"; const debug = Debug("currents:results"); @@ -53,10 +54,6 @@ export const getTestAttempt = (attempt: CypressCommandLine.AttemptResult) => { export const getInstanceResultPayload = ( runResult: CypressCommandLine.RunResult ): UpdateInstanceResultsPayload => { - const altTests = []; - if (runResult.error && !runResult.tests?.length) { - altTests.push(getFakeTestFromException(runResult.error, runResult.stats)); - } return { stats: getStats(runResult.stats), reporterStats: runResult.reporterStats, @@ -70,11 +67,11 @@ export const getInstanceResultPayload = ( hooks: runResult.hooks, attempts: test.attempts?.map(getTestAttempt) ?? [], clientId: `r${i}`, - })) ?? altTests, + })) ?? [], }; }; -function getFakeTestFromException( +export function getFakeTestFromException( error: string, stats: CypressCommandLine.RunResult["stats"] ) { @@ -106,10 +103,6 @@ export const getInstanceTestsPayload = ( runResult: CypressCommandLine.RunResult, config: Cypress.ResolvedConfigOptions ): SetInstanceTestsPayload => { - const altTests = []; - if (runResult.error && !runResult.tests?.length) { - altTests.push(getFakeTestFromException(runResult.error, runResult.stats)); - } return { config, tests: @@ -119,7 +112,7 @@ export const getInstanceTestsPayload = ( body: test.body, clientId: `r${i}`, hookIds: [], - })) ?? altTests, + })) ?? [], hooks: runResult.hooks, }; }; @@ -201,19 +194,39 @@ const emptyStats = { totalTests: 0, }; +const getDummyFailedTest = (start: string, error: string) => ({ + title: ["Unknown"], + state: "failed", + body: "// This test is automatically generated due to execution failure", + displayError: error, + attempts: [ + { + state: "failed", + startedAt: start, + duration: 0, + videoTimestamp: 0, + screenshots: [], + error: { + name: "CypressExecutionError", + message: error, + stack: "", + }, + }, + ], +}); + export function getFailedDummyResult({ specs, error, - config, }: { specs: string[]; error: string; - config: any; // TODO tighten this up }): CypressCommandLine.CypressRunResult { const start = new Date().toISOString(); const end = new Date().toISOString(); return { - config, + // @ts-ignore + config: getConfig() ?? {}, status: "finished", startedTestsAt: new Date().toISOString(), endedTestsAt: new Date().toISOString(), @@ -253,28 +266,7 @@ export function getFailedDummyResult({ absolute: s, relativeToCommonRoot: s, }, - tests: [ - { - title: ["Unknown"], - state: "failed", - body: "// This test is automatically generated due to execution failure", - displayError: error, - attempts: [ - { - state: "failed", - startedAt: start, - duration: 0, - videoTimestamp: 0, - screenshots: [], - error: { - name: "CloudExecutionError", - message: error, - stack: "", - }, - }, - ], - }, - ], + tests: [getDummyFailedTest(start, error)], shouldUploadVideo: false, skippedSpec: false, })), diff --git a/packages/cypress-cloud/lib/run.ts b/packages/cypress-cloud/lib/run.ts index 0f59b840..39ef03b4 100644 --- a/packages/cypress-cloud/lib/run.ts +++ b/packages/cypress-cloud/lib/run.ts @@ -3,7 +3,7 @@ import "./init"; import Debug from "debug"; import { CurrentsRunParameters } from "../types"; import { createRun } from "./api"; -import { cutInitialOutput } from "./capture"; +import { cutInitialOutput, getCapturedOutput } from "./capture"; import { getCI } from "./ciProvider"; import { getMergedConfig, @@ -26,6 +26,7 @@ import { setConfig, setSpecAfter, setSpecBefore, + setSpecOutput, } from "./runner"; import { shutdown } from "./shutdown"; import { getSpecFiles } from "./specMatcher"; @@ -155,6 +156,7 @@ function listenToSpecEvents() { async ({ spec, results }: { spec: Cypress.Spec; results: any }) => { debug("after:spec %o %o", spec, results); setSpecAfter(spec.relative, results); + setSpecOutput(spec.relative, getCapturedOutput()); createReportTaskSpec(spec.relative); } ); diff --git a/packages/cypress-cloud/lib/runner/mapResult.ts b/packages/cypress-cloud/lib/runner/mapResult.ts index bdf22b42..d9f0647b 100644 --- a/packages/cypress-cloud/lib/runner/mapResult.ts +++ b/packages/cypress-cloud/lib/runner/mapResult.ts @@ -67,7 +67,7 @@ export function specResultsToCypressResults( duration: specAfterResult.stats.wallClockDuration, }, reporter: specAfterResult.reporter, - reporterStats: specAfterResult.reporterStats, + reporterStats: specAfterResult.reporterStats ?? {}, spec: specAfterResult.spec, error: specAfterResult.error, video: specAfterResult.video, @@ -76,7 +76,7 @@ export function specResultsToCypressResults( // wrong typedef for CypressCommandLine.CypressRunResult // actual HookName is "before all" | "before each" | "after all" | "after each" hooks: specAfterResult.hooks, - tests: specAfterResult.tests.map((t) => + tests: (specAfterResult.tests ?? []).map((t) => getTest(t, specAfterResult.screenshots) ), }, diff --git a/packages/cypress-cloud/lib/runner/spec.type.ts b/packages/cypress-cloud/lib/runner/spec.type.ts index 10a11eaf..ab827b80 100644 --- a/packages/cypress-cloud/lib/runner/spec.type.ts +++ b/packages/cypress-cloud/lib/runner/spec.type.ts @@ -2,13 +2,13 @@ export interface SpecResult { error: string | null; exception: null | string; - hooks: TestHook[]; + hooks: TestHook[] | null; reporter: string; - reporterStats: ReporterStats; + reporterStats: ReporterStats | null; screenshots: Screenshot[]; spec: Spec; stats: Stats; - tests: Test[]; + tests: Test[] | null; video: string | null; } diff --git a/packages/cypress-cloud/lib/runner/state.ts b/packages/cypress-cloud/lib/runner/state.ts index 75bae8b9..0e77fd4f 100644 --- a/packages/cypress-cloud/lib/runner/state.ts +++ b/packages/cypress-cloud/lib/runner/state.ts @@ -1,9 +1,12 @@ -import { InstanceId } from "cypress-cloud/types"; +import { CypressRun, InstanceId } from "cypress-cloud/types"; +import Debug from "debug"; import { error, warn } from "../log"; -import { getFailedDummyResult } from "../results"; +import { getFailedDummyResult, getFakeTestFromException } from "../results"; import { specResultsToCypressResults } from "./mapResult"; import { SpecResult } from "./spec.type"; +const debug = Debug("currents:state"); + // Careful here - it is a global mutable state 🐲 type InstanceExecutionState = { instanceId: InstanceId; @@ -76,12 +79,25 @@ export const setInstanceResult = ( i.runResultsReportedAt = new Date(); }; +export const setSpecOutput = (spec: string, output: string) => { + const i = getExecutionStateSpec(spec); + if (!i) { + warn('Cannot find execution state for spec "%s"', spec); + return; + } + setInstanceOutput(i.instanceId, output); +}; + export const setInstanceOutput = (instanceId: string, output: string) => { const i = executionState[instanceId]; if (!i) { warn('Cannot find execution state for instance "%s"', instanceId); return; } + if (i.output) { + debug('Instance "%s" already has output', instanceId); + return; + } i.output = output; }; @@ -101,27 +117,44 @@ export const getInstanceResults = ( return getFailedDummyResult({ specs: ["unknown"], - error: "cypress-cloud: Cannot find execution state for instance", - config: {}, + error: "Cannot find execution state for instance", }); } // use spec:after results - it can become available before run results if (i.specAfterResults) { - return specResultsToCypressResults(i.specAfterResults); + return backfillException(specResultsToCypressResults(i.specAfterResults)); } if (i.runResults) { - return i.runResults; + return backfillException(i.runResults); } + debug('No results detected for "%s"', i.spec); return getFailedDummyResult({ specs: [i.spec], - error: "cypress-cloud: Cannot find execution state for instance", - config: {}, + error: `No results detected for the spec file. That usually happens because of cypress crash. See the console output for details.`, }); }; +const backfillException = (result: CypressCommandLine.CypressRunResult) => { + return { + ...result, + runs: result.runs.map(backfillExceptionRun), + }; +}; + +const backfillExceptionRun = (run: CypressRun) => { + if (!run.error) { + return run; + } + + return { + ...run, + tests: [getFakeTestFromException(run.error, run.stats)], + }; +}; + let _config: Cypress.ResolvedConfigOptions | undefined = undefined; export const setConfig = (c: typeof _config) => (_config = c); export const getConfig = () => _config;